prezent/dompdf-bundle
A Symfony bundle that integrates the Dompdf library, letting you generate PDFs from HTML and CSS inside your Symfony applications with simple, bundle-style configuration and service access.
Reality check first: this is a Symfony bundle, not a Laravel package. In a Laravel app composer require prezent/dompdf-bundle will pull the code but nothing registers it — Laravel has no bundle bootstrapping, so the DI extension never runs and no service appears in the container. For Laravel, the equivalent is barryvdh/laravel-dompdf (same dompdf/dompdf engine underneath). The patterns below still apply if you end up using dompdf directly in Laravel; only the wiring differs.
Assuming a Symfony 4/5 codebase:
composer require prezent/dompdf-bundlePrezent\DompdfBundle\PrezentDompdfBundle::class => ['all' => true] in config/bundles.php.DependencyInjection/Configuration.php — the config tree, i.e. every option you can set.Resources/config/services.xml (or services.yaml) — the service id and constructor arguments. That id is your entry point.DependencyInjection/PrezentDompdfExtension.php — how config maps onto Dompdf\Options.config/packages/prezent_dompdf.yaml with the options you want as defaults (paper size, orientation, DPI, font dir, chroot). These are passed through to Dompdf\Options::set().Response:public function invoice(Invoice $invoice, Environment $twig, /* dompdf service */ $dompdf): Response
{
$html = $twig->render('pdf/invoice.html.twig', ['invoice' => $invoice]);
$dompdf->loadHtml($html);
$dompdf->setPaper('A4', 'portrait');
$dompdf->render();
return new Response($dompdf->output(), 200, [
'Content-Type' => 'application/pdf',
'Content-Disposition' => 'inline; filename="invoice-'.$invoice->getId().'.pdf"',
]);
}
Wrap it, don't sprinkle it. Inject the dompdf service into one small PdfRenderer service and expose render(string $html, array $overrides = []): string. Every controller/job then depends on your class, not the bundle — which is what makes it trivial to drop the bundle later.
One document per instance. Dompdf is stateful (canvas, font metrics, loaded images, base path). If the bundle registers it as a shared service, reusing it across documents in a worker is asking for bleed-through. Either request the service fresh per render or, better, wrap a DompdfFactory-style closure that news up a Dompdf with the configured Options each time.
Template layout. Keep PDF templates in templates/pdf/, separate from web templates. Inline the CSS — either a <style> block in the template, or {% apply inline_css %} (needs twig/cssinliner-extra) if you want to share a stylesheet with the web UI. No JavaScript, no external CDN links (see gotchas).
Response strategy. Small PDFs: new Response($dompdf->output(), ...) with Content-Disposition: inline to preview, attachment to download. Large ones: write to tempnam() and return a BinaryFileResponse so you're not holding the whole string plus the response in memory.
Config layering. Put organisation-wide defaults (A4, portrait, 96 DPI, font dir) in the bundle config. Override per document with $dompdf->setPaper() / $options->set() — e.g. landscape for wide reports, A3 for schedules. Don't fork the bundle config for one-off cases.
Generate off the request cycle. Anything over a page or two, or anything batched (statements, tickets, exports), belongs in a Messenger handler: render → store via Flysystem/S3 → email or expose a signed download link. PDF generation is the classic request-timeout culprit.
Testing. Cheap and effective: assert str_starts_with($pdf, '%PDF-'), assert a plausible byte length, and assert the response Content-Type. If you need content assertions, run the output through a text extractor rather than regexing raw bytes.
composer why-not prezent/dompdf-bundle before blaming your app; its Symfony/dompdf constraints will likely conflict with a current stack. If you keep it, pin dompdf/dompdf explicitly in your root composer.json and fork the bundle into src/ rather than relying on upstream fixes that aren't coming.Options behaviour). Verify which version resolves before writing code against a tutorial.fontDir/fontCache point into a read-only container filesystem you get silent fallbacks to Helvetica or outright failures. Point them at %kernel.cache_dir%/dompdf (dev) or var/dompdf (prod) and make sure the directory exists and is writable at deploy time.chroot silently kills images. It defaults to the dompdf install directory, so <img src="/build/logo.png"> or relative paths quietly render as nothing. Set chroot to your project root, or use absolute paths / embedded base64 data URIs for logos — base64 is the most portable option.isRemoteEnabled is off by default. Remote logos and avatars disappear. Turning it on means dompdf makes outbound HTTP requests from user-influenced URLs — an SSRF vector. Enable knowingly, and never feed it untrusted src values.isHtml5ParserEnabled. Needed for decent HTML5 parsing. Even with it on, there is no flexbox and no grid. Lay out with tables and floats, the way you would for a 2009 email client.page-break-before/after work reliably. page-break-inside: avoid is unreliable on <tr> — use repeating <thead>, fixed row heights, and split long tables yourself in the template if pagination must be exact.memory_limit around the point you stop expecting it. Raise limits inside the Messenger worker or job, not globally, and consider chunking rows into multiple documents.render() the output is empty — don't bother calling output() early. Set logOutputFile in Options to capture dompdf's own log, set debugPng, and dump the rendered HTML from your Twig template to disk to confirm dompdf is seeing what you think it is. Nine times out of ten the bug is CSS dompdf doesn't support, not the bundle.services.yaml (or decorate it). If you find yourself needing a third extension point, that's the signal to delete the bundle and register Dompdf\Dompdf + Dompdf\Options yourself in ~20 lines of services.yaml — you lose nothing but the config tree, and you gain a dependency you can reason about.How can I help you explore Laravel packages today?