mirror of
https://github.com/InvoiceShelf/InvoiceShelf.git
synced 2026-04-20 03:34:06 +00:00
Feat(Gotenberg): Opt-in alternative pdf generation for modern CSS (#184)
* WIP(gotenberg): add pdf generation abstraction and UI * feat(pdf): settings validate(clien+server) & save * fix(gotenberg): Use correct default papersize chore(gotengberg): Remove unused GOTENBERG_MARGINS env from .env * style(gotenberg): fix linter/styling issues * fix(pdf): use pdf config policy * fix: revert accidental capitalization in mail config vue * Update composer, remove whitespace typo * Fix small typos * fix cookie/env issue * Add gotenberg to .dev, move admin menu item up
This commit is contained in:
16
app/Facades/PDF.php
Normal file
16
app/Facades/PDF.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Facades;
|
||||
|
||||
use Illuminate\Support\Facades\Facade;
|
||||
|
||||
/**
|
||||
* @method static \Psr\Http\Message\ResponseInterface loadView(string $template)
|
||||
*/
|
||||
class PDF extends Facade
|
||||
{
|
||||
protected static function getFacadeAccessor()
|
||||
{
|
||||
return 'pdf.driver';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Admin\Settings;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\PDFConfigurationRequest;
|
||||
use App\Space\EnvironmentManager;
|
||||
|
||||
class PDFConfigurationController extends Controller
|
||||
{
|
||||
/**
|
||||
* @var EnvironmentManager
|
||||
*/
|
||||
protected $environmentManager;
|
||||
|
||||
public function __construct(EnvironmentManager $environmentManager)
|
||||
{
|
||||
$this->environmentManager = $environmentManager;
|
||||
}
|
||||
|
||||
public function getDrivers()
|
||||
{
|
||||
$this->authorize('manage pdf config');
|
||||
|
||||
$drivers = [
|
||||
'dompdf',
|
||||
'gotenberg',
|
||||
];
|
||||
|
||||
return response()->json($drivers);
|
||||
}
|
||||
|
||||
public function getEnvironment()
|
||||
{
|
||||
$this->authorize('manage pdf config');
|
||||
|
||||
$config = [
|
||||
'pdf_driver' => config('pdf.driver'),
|
||||
'gotenberg_host' => config('pdf.gotenberg.host'),
|
||||
'gotenberg_margins' => config('pdf.gotenberg.margins'),
|
||||
'gotenberg_papersize' => config('pdf.gotenberg.papersize'),
|
||||
];
|
||||
|
||||
return response()->json($config);
|
||||
}
|
||||
|
||||
public function saveEnvironment(PDFConfigurationRequest $request)
|
||||
{
|
||||
$this->authorize('manage pdf config');
|
||||
$results = $this->environmentManager->savePDFVariables($request);
|
||||
|
||||
return response()->json($results);
|
||||
}
|
||||
}
|
||||
57
app/Http/Requests/PDFConfigurationRequest.php
Normal file
57
app/Http/Requests/PDFConfigurationRequest.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class PDFConfigurationRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
switch ($this->get('pdf_driver')) {
|
||||
case 'dompdf':
|
||||
return [
|
||||
'pdf_driver' => [
|
||||
'required',
|
||||
'string',
|
||||
],
|
||||
];
|
||||
break;
|
||||
|
||||
case 'gotenberg':
|
||||
return [
|
||||
'pdf_driver' => [
|
||||
'required',
|
||||
'string',
|
||||
],
|
||||
'gotenberg_host' => [
|
||||
'required',
|
||||
'url',
|
||||
],
|
||||
'gotenberg_papersize' => [
|
||||
function ($attribute, $value, $fail) {
|
||||
($attribute); // unused
|
||||
$reg = "/^\d+(pt|px|pc|mm|cm|in) \d+(pt|px|pc|mm|cm|in)$/";
|
||||
if (! preg_match($reg, $value)) {
|
||||
$fail('Invalid papersize, must be in format "210mm 297mm". Accepts: pt,px,pc,mm,cm,in');
|
||||
}
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
break;
|
||||
}
|
||||
throw new \InvalidArgumentException('Invalid PDFDriver requested');
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,9 @@ use App;
|
||||
use App\Mail\SendEstimateMail;
|
||||
use App\Services\SerialNumberFormatter;
|
||||
use App\Space\PdfTemplateUtils;
|
||||
use App\Facades\PDF;
|
||||
use App\Traits\GeneratesPdfTrait;
|
||||
use App\Traits\HasCustomFieldsTrait;
|
||||
use Barryvdh\DomPDF\Facade\Pdf as PDF;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App;
|
||||
use App\Facades\PDF;
|
||||
use App\Mail\SendInvoiceMail;
|
||||
use App\Services\SerialNumberFormatter;
|
||||
use App\Space\PdfTemplateUtils;
|
||||
use App\Traits\GeneratesPdfTrait;
|
||||
use App\Traits\HasCustomFieldsTrait;
|
||||
use Barryvdh\DomPDF\Facade\Pdf as PDF;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
@@ -46,6 +46,15 @@ class SettingsPolicy
|
||||
return false;
|
||||
}
|
||||
|
||||
public function managePDFConfig(User $user)
|
||||
{
|
||||
if ($user->isOwner()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function manageSettings(User $user)
|
||||
{
|
||||
if ($user->isOwner()) {
|
||||
|
||||
@@ -127,6 +127,7 @@ class AppServiceProvider extends ServiceProvider
|
||||
Gate::define('manage backups', [SettingsPolicy::class, 'manageBackups']);
|
||||
Gate::define('manage file disk', [SettingsPolicy::class, 'manageFileDisk']);
|
||||
Gate::define('manage email config', [SettingsPolicy::class, 'manageEmailConfig']);
|
||||
Gate::define('manage pdf config', [SettingsPolicy::class, 'managePDFConfig']);
|
||||
Gate::define('manage notes', [NotePolicy::class, 'manageNotes']);
|
||||
Gate::define('view notes', [NotePolicy::class, 'viewNotes']);
|
||||
|
||||
|
||||
13
app/Providers/PDFServiceProvider.php
Normal file
13
app/Providers/PDFServiceProvider.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Services\PDFService;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class PDFServiceProvider extends ServiceProvider
|
||||
{
|
||||
public $bindings = [
|
||||
'pdf.driver' => PDFService::class,
|
||||
];
|
||||
}
|
||||
59
app/Services/PDFDrivers/GotenbergPDFDriver.php
Normal file
59
app/Services/PDFDrivers/GotenbergPDFDriver.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\PDFDrivers;
|
||||
|
||||
use Gotenberg\Gotenberg;
|
||||
use Gotenberg\Stream;
|
||||
use Illuminate\Http\Response;
|
||||
|
||||
class GotenbergPDFResponse
|
||||
{
|
||||
/** @var \Psr\Http\Message\ResponseInterface */
|
||||
protected $response;
|
||||
|
||||
public function __construct($stream)
|
||||
{
|
||||
$this->response = $stream;
|
||||
}
|
||||
|
||||
public function stream(string $filename = 'document.pdf'): Response
|
||||
{
|
||||
$output = $this->response->getBody();
|
||||
|
||||
return new Response($output, 200, [
|
||||
'Content-Type' => 'application/pdf',
|
||||
'Content-Disposition' => 'inline; filename="'.$filename.'"',
|
||||
]);
|
||||
}
|
||||
|
||||
public function output(): string
|
||||
{
|
||||
return $this->response->getBody()->getContents();
|
||||
}
|
||||
}
|
||||
|
||||
class GotenbergPDFDriver
|
||||
{
|
||||
public function loadView(string $viewname): GotenbergPDFResponse
|
||||
{
|
||||
$papersize = explode(' ', config('pdf.gotenberg.papersize'));
|
||||
if (count($papersize) != 2) {
|
||||
throw new \InvalidArgumentException('Invalid Gotenberg Papersize specified');
|
||||
}
|
||||
|
||||
$host = config('pdf.gotenberg.host');
|
||||
$request = Gotenberg::chromium($host)
|
||||
->pdf()
|
||||
->margins(0, 0, 0, 0) // Margins can be set using CSS
|
||||
->paperSize($papersize[0], $papersize[1])
|
||||
->html(
|
||||
Stream::string(
|
||||
'document.html',
|
||||
view($viewname)->render(),
|
||||
)
|
||||
);
|
||||
$result = Gotenberg::send($request);
|
||||
|
||||
return new GotenbergPDFResponse($result);
|
||||
}
|
||||
}
|
||||
47
app/Services/PDFService.php
Normal file
47
app/Services/PDFService.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
/*
|
||||
* Two options:
|
||||
* - Barryvdh\DomPDF\Facade\Pdf
|
||||
* - Gotenberg
|
||||
*/
|
||||
|
||||
use App;
|
||||
use App\Services\PDFDrivers\GotenbergPDFDriver;
|
||||
use Illuminate\Http\Response;
|
||||
|
||||
interface ResponseStream
|
||||
{
|
||||
public function stream(string $filename): Response;
|
||||
|
||||
public function output(): string;
|
||||
}
|
||||
|
||||
interface PDFDriver
|
||||
{
|
||||
public function loadView(string $template): ResponseStream;
|
||||
}
|
||||
|
||||
class PDFDriverFactory
|
||||
{
|
||||
public static function create(string $driver)
|
||||
{
|
||||
return match ($driver) {
|
||||
'dompdf' => App::make('dompdf.wrapper'),
|
||||
'gotenberg' => new GotenbergPDFDriver,
|
||||
default => throw new \InvalidArgumentException('Invalid PDFDriver requested')
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class PDFService
|
||||
{
|
||||
public static function loadView(string $template)
|
||||
{
|
||||
$driver = config('pdf.driver');
|
||||
|
||||
return PDFDriverFactory::create($driver)->loadView($template);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ use App\Http\Requests\DatabaseEnvironmentRequest;
|
||||
use App\Http\Requests\DiskEnvironmentRequest;
|
||||
use App\Http\Requests\DomainEnvironmentRequest;
|
||||
use App\Http\Requests\MailEnvironmentRequest;
|
||||
use App\Http\Requests\PDFConfigurationRequest;
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
@@ -94,7 +95,6 @@ class EnvironmentManager
|
||||
}
|
||||
|
||||
return $str;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -225,7 +225,6 @@ class EnvironmentManager
|
||||
try {
|
||||
|
||||
$this->updateEnv($mailEnv);
|
||||
|
||||
} catch (Exception $e) {
|
||||
return [
|
||||
'error' => 'mail_variables_save_error',
|
||||
@@ -237,6 +236,60 @@ class EnvironmentManager
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the pdf generation content to the .env file.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function savePDFVariables(PDFConfigurationRequest $request)
|
||||
{
|
||||
$pdfEnv = $this->getPDFConfiguration($request);
|
||||
|
||||
try {
|
||||
|
||||
$this->updateEnv($pdfEnv);
|
||||
} catch (Exception $e) {
|
||||
return [
|
||||
'error' => 'pdf_variables_save_error',
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => 'pdf_variables_save_successfully',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the pdf configuration
|
||||
*
|
||||
* @param PDFConfigurationRequest $request
|
||||
* @return array
|
||||
*/
|
||||
private function getPDFConfiguration($request)
|
||||
{
|
||||
$pdfEnv = [];
|
||||
|
||||
$driver = $request->get('pdf_driver');
|
||||
|
||||
switch ($driver) {
|
||||
case 'dompdf':
|
||||
$pdfEnv = [
|
||||
'PDF_DRIVER' => $request->get('pdf_driver'),
|
||||
];
|
||||
break;
|
||||
case 'gotenberg':
|
||||
$pdfEnv = [
|
||||
'PDF_DRIVER' => $request->get('pdf_driver'),
|
||||
'GOTENBERG_HOST' => $request->get('gotenberg_host'),
|
||||
'GOTENBERG_MARGINS' => $request->get('gotenberg_margins'),
|
||||
'GOTENBERG_PAPERSIZE' => $request->get('gotenberg_papersize'),
|
||||
];
|
||||
break;
|
||||
}
|
||||
|
||||
return $pdfEnv;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the mail configuration
|
||||
*
|
||||
@@ -316,7 +369,6 @@ class EnvironmentManager
|
||||
];
|
||||
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
return $mailEnv;
|
||||
@@ -334,7 +386,6 @@ class EnvironmentManager
|
||||
try {
|
||||
|
||||
$this->updateEnv($diskEnv);
|
||||
|
||||
} catch (Exception $e) {
|
||||
return [
|
||||
'error' => 'disk_variables_save_error',
|
||||
@@ -450,7 +501,6 @@ class EnvironmentManager
|
||||
}
|
||||
$formatted .= $current.$this->delimiter;
|
||||
$previous = $current;
|
||||
|
||||
}
|
||||
|
||||
file_put_contents($this->envPath, trim($formatted));
|
||||
|
||||
Reference in New Issue
Block a user