slik/dompdf-bundle
Symfony bundle that integrates the Dompdf library to generate PDFs from HTML. It provides a service for rendering HTML templates into PDF documents in Symfony applications, with configurable options and straightforward setup.
Step 0 — the reality check. This is a Symfony 2 bundle, not a Laravel package. It ships a Bundle subclass, a DependencyInjection/*Extension, and service-container XML/YAML wiring. Laravel has no bundle/kernel-bundle concept, so there is nothing to composer require into a Laravel app and nothing to register in config/app.php. If you try, expect a platform-requirement wall (PHP 5.x/7.0–7.1, Symfony ~2.3|~2.7) or a cascade of symfony/* conflicts. Do not use --ignore-platform-reqs to force it — you would be pinning an unpatched PDF engine.
Where to look first (as a specification, not a dependency). Clone the repo and read exactly three things:
DependencyInjection/SlikDompdfExtension.php and the bundle's config tree — this is the list of options the wrapper exposes (paper size, orientation, font_dir, temp_dir, chroot, remote-asset flags, DPI, font cache).slik_dompdf — this tells you how Dompdf + Options are constructed and which defaults are applied.composer.json — the dompdf/dompdf constraint, which is what actually determines your security exposure if you were to reuse anything.That is the entire reusable surface: a dozen options and a service definition. Everything else is Symfony kernel glue you will re-implement.
First use case — the honest Laravel equivalent. Your first task is not "install the bundle," it's "recreate its config as a Laravel service provider." Two viable paths:
barryvdh/laravel-dompdf — maintained wrapper over dompdf/dompdf 3.x, publishes a config file, facade + DI. Use this if you need dompdf output fidelity.dompdf/dompdf direct — bind your own singleton in a service provider (~30 lines), maximum control.// app/Providers/PdfServiceProvider.php
$this->app->singleton(PdfRenderer::class, fn () => new DompdfRenderer(
config('pdf') // ported from the bundle's extension config
));
Then port the handful of keys the legacy Symfony app actually set into config/pdf.php. That config file, plus a PdfRenderer interface, is the only thing worth carrying forward from this bundle.
interface PdfRenderer {
public function render(string $html, array $options = []): string;
}
Application code depends on that interface, never on Dompdf directly — that is the one durable design decision and it keeps the door open to spatie/laravel-pdf (Browsershot) later.
If you are genuinely on Symfony 2.x (maintaining, not building), the getting-started is: add new Slik\DompdfBundle\SlikDompdfBundle() to AppKernel::registerBundles(), set options under slik_dompdf: in config.yml, and pull $this->get('slik_dompdf') from a controller. But treat that as a legacy-maintenance note, not a recommendation.
Interface-first rendering. Every call site should look like this, regardless of engine:
class InvoiceController {
public function __construct(private PdfRenderer $pdf) {}
public function download(Invoice $invoice) {
$html = view('pdf.invoice', compact('invoice'))->render();
return response($this->pdf->render($html), 200, [
'Content-Type' => 'application/pdf',
'Content-Disposition' => 'attachment; filename="invoice-'.$invoice->id.'.pdf"',
]);
}
}
The controller never knows whether the bytes came from DOMPDF or Chromium. That is what lets a design requirement (flexbox, web fonts, JS charts) force an engine swap without touching feature code.
Render in a job, serve from storage. DOMPDF is CPU- and memory-bound and single-threaded per render. Do not do it in the request cycle for anything non-trivial.
class GenerateInvoicePdf implements ShouldQueue {
public $timeout = 60;
public function handle(PdfRenderer $pdf) {
$bytes = $pdf->render(view('pdf.invoice', ['invoice' => $this->invoice])->render());
Storage::disk('pdfs')->put($this->path(), $bytes);
}
}
Push this onto a dedicated pdf queue, keep the worker memory_limit at 512M+, and set an explicit timeout so a pathological document cannot pin a worker. Serve through a signed URL. Invoice-style documents are usually immutable once issued — cache rendered PDFs keyed on a hash of input + template version + config version.
Twig/invoice templates translate 1:1 to Blade. The bundle's selling point is "render Twig, feed the HTML to dompdf." That is unchanged in Laravel: view('pdf.invoice')->render() produces the same string. The only things that don't translate are Twig extensions — replicate them as Blade directives or helpers.
Pin the security controls explicitly. The wrapper will not provide these; they are yours to own:
chroot to a dedicated asset directory; never the app root.enable_remote / isRemoteEnabled = false) and pre-inline assets instead.Fixture-driven migration. If you are porting from a legacy Symfony+dompdf system, collect 10–20 real production documents and render them with the old system as golden fixtures. Diff the replacement on page count, extracted text, and visual output. Fonts and table pagination are exactly where dompdf version drift and dompdf→Chromium drift show up. Do this before committing to an engine — half a day against a real invoice, not a hello-world.
Wrap, don't leak. Even if you choose barryvdh/laravel-dompdf, bind it behind PdfRenderer. That keeps the option to move to spatie/laravel-pdf (Browsershot) when a design actually demands modern CSS.
It will not install. This is the headline gotcha. Symfony 2 reached EOL in 2018, the bundle's constraints are era-appropriate, and Composer will refuse to resolve into a modern Laravel app. If you try to force it, you get an old, unpatched dompdf — an unacceptable posture for anything touching user HTML. There is no supported workaround; there is only a fork, which is strictly dominated by using dompdf/dompdf directly.
Zero dependents means zero safety net. 9 stars, 0 dependents, no vendor, no community, no CVE feed. If a dompdf advisory drops against your pinned version, you cannot patch without unpinning the Symfony 2 constraint — which cascades into a full replacement anyway. If you must adopt DOMPDF specifically, pin dompdf/dompdf on a current release yourself, and treat the bundle as a reference implementation you skimmed.
chroot escapes and SSRF. Multiple advisories exist against pre-2.x dompdf for local file disclosure via resource handling and remote-resource fetching. The wrapper exposes these as ordinary config options and provides no guardrails. Set chroot narrowly, disable remote loading, and treat any "enable remote" style flag as a security decision that needs review.
Font resolution fails silently. A missing or unregistered font produces fallback glyphs with no error — you discover it in a customer's invoice PDF. Assert expected fonts in CI with a fixture test and compare against a reference render.
Page-break CSS is where the lies live. dompdf has weak or absent flexbox/grid support, spotty @page handling, and unreliable table pagination across long rows. If design expects flexbox, custom web fonts, or JS-driven charts, DOMPDF will disappoint and no wrapper fixes that. Spike against a representative template before committing.
Memory exhaustion on long tables is the most common dompdf production incident. Emit a metric on peak memory per render; if you see it climbing, you usually have deeply nested markup or an unbounded table, not a config problem.
Timeout/hang on malformed HTML. Enforce worker-level timeouts (30–60s) and a max page count. A single pathological document should not pin a pdf queue worker.
Don't port the bundle; port the config. The durable artifacts are the extracted option set (orientation, paper size, font_dir, temp_dir, base path, DPI, font cache) and any committed custom fonts. Re-create those as config/pdf.php and a PdfRenderer service, and archive the bundle reference with a note on why it was not adopted.
Extension point that matters. There isn't one worth using — the bundle offers DI registration and a config tree, both reproducible in an afternoon. The one extension point you should build is your own PdfRenderer interface; that is what lets you swap engines later without rewriting call sites.
How can I help you explore Laravel packages today?