Files
InvoiceShelf/app/Http/Controllers/Admin/Settings/PDFConfigurationController.php
Darko Gjorgjijoski 773670c18f feat(pdf): archival PDF/A output and document properties (#732)
Generated files carried no document properties at all, so an archive of them
showed a column of blank titles and no author. Title, Subject, Author and
Creator are now written from the document number and company, on both drivers:
dompdf via addInfo(), Gotenberg via metadata().

dompdf needed more than the API call. It reads Title from the <title> element
during render(), which happens after addInfo(), so metadata set through the API
alone was silently overwritten by whatever the template put there and the two
drivers disagreed about what the file was called. The title is written into the
markup as well, escaped.

Also adds an archival format setting for Gotenberg: off, PDF/A-1b, -2b or -3b.
PDF/A-3 is what the EU e-invoicing formats expect. Verified against a stock
gotenberg:8 -- LibreOffice inside the image does the conversion and the output
carries the right pdfaid:part in its XMP -- so no extra components are needed.

A fixed list rather than free text, because the SDK forwards whatever it is
given and an unsupported value would surface only as an HTTP error from the
service at render time. Empty is a real choice meaning an ordinary PDF, so it
overrides an env default rather than falling through it.

Gotenberg only: dompdf cannot produce PDF/A.

Claude-Session: https://claude.ai/code/session_01QmECndmNZwzN65Zz9P87dF
2026-08-01 14:37:22 +02:00

153 lines
4.6 KiB
PHP

<?php
namespace App\Http\Controllers\Admin\Settings;
use App\Http\Controllers\Controller;
use App\Http\Requests\PDFConfigurationRequest;
use App\Models\Setting;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse;
class PDFConfigurationController extends Controller
{
/**
* Driver-neutral page geometry. Each maps onto `pdf.page.*` in config, and is
* read and written for every driver rather than being nested under one.
*/
private const PAGE_SETTINGS = [
'pdf_paper_width',
'pdf_paper_height',
'pdf_orientation',
'pdf_margin_top',
'pdf_margin_right',
'pdf_margin_bottom',
'pdf_margin_left',
];
/**
* Stored in the same string-valued settings table, so kept apart from the
* lengths above: it needs an explicit cast in both directions rather than
* being passed through.
*/
private const PAGE_BOOLEANS = [
'pdf_page_numbers',
];
/**
* Returns the available drivers
*
* @throws AuthorizationException
*/
public function getDrivers(): JsonResponse
{
$this->authorize('manage pdf config');
$drivers = [
'dompdf',
'gotenberg',
];
return response()->json($drivers);
}
/**
* Return the PDF settings
*
* @throws AuthorizationException
*/
public function getEnvironment(): JsonResponse
{
$this->authorize('manage pdf config');
$pdfSettings = Setting::getSettings(array_merge(
['pdf_driver', 'gotenberg_host', 'gotenberg_pdfa'],
self::PAGE_SETTINGS,
self::PAGE_BOOLEANS,
));
$config = [
'pdf_driver' => $pdfSettings['pdf_driver'] ?? config('pdf.driver'),
'gotenberg_host' => $pdfSettings['gotenberg_host'] ?? config('pdf.connections.gotenberg.host'),
'gotenberg_pdfa' => $pdfSettings['gotenberg_pdfa'] ?? config('pdf.connections.gotenberg.pdfa') ?? '',
];
// Page geometry applies to whichever driver is selected, so it is always
// returned rather than nested under a driver branch.
foreach (self::PAGE_SETTINGS as $setting) {
$config[$setting] = $pdfSettings[$setting] ?? config(self::configKeyFor($setting));
}
foreach (self::PAGE_BOOLEANS as $setting) {
$config[$setting] = filter_var(
$pdfSettings[$setting] ?? config(self::configKeyFor($setting)),
FILTER_VALIDATE_BOOLEAN
);
}
return response()->json($config);
}
/**
* Saves the settings
*
* @throws AuthorizationException
*/
public function saveEnvironment(PDFConfigurationRequest $request): JsonResponse
{
$this->authorize('manage pdf config');
// Prepare PDF settings for database storage
$pdfSettings = $this->preparePDFSettingsForDatabase($request);
// Save PDF settings to database
Setting::setSettings($pdfSettings);
return response()->json([
'success' => 'pdf_variables_save_successfully',
]);
}
/**
* Prepare PDF settings for database storage
*/
private function preparePDFSettingsForDatabase(PDFConfigurationRequest $request): array
{
$driver = $request->get('pdf_driver');
$settings = ['pdf_driver' => $driver];
// Page geometry is saved for every driver: switching between them should
// not lose the paper size, which is what happened while it was a
// Gotenberg-only setting.
foreach (self::PAGE_SETTINGS as $setting) {
$settings[$setting] = $request->get($setting);
}
// Only written when the form actually submitted it. Page numbers are a
// Gotenberg capability and the dompdf form does not render the control,
// so an unconditional write would clear the operator's choice every time
// they saved from the other driver.
foreach (self::PAGE_BOOLEANS as $setting) {
if ($request->has($setting)) {
$settings[$setting] = $request->boolean($setting) ? '1' : '0';
}
}
if ($driver === 'gotenberg') {
$settings['gotenberg_host'] = $request->get('gotenberg_host');
$settings['gotenberg_pdfa'] = $request->get('gotenberg_pdfa') ?? '';
}
return $settings;
}
/**
* Maps a settings key onto its config counterpart, e.g.
* `pdf_margin_top` -> `pdf.page.margin_top`.
*/
private static function configKeyFor(string $setting): string
{
return 'pdf.page.'.substr($setting, strlen('pdf_'));
}
}