es/dompdf-bundle
Symfony bundle that wraps the dompdf library, letting you convert HTML into PDFs from your controllers. Stream PDFs to the browser or grab the binary content, and configure options like DPI and paper size via YAML.
First reality check: this package will not install in a Laravel app. es/dompdf-bundle is a Symfony 2 bundle — it registers through AppKernel::registerBundles(), reads an es_dompdf YAML extension, and exposes a dompdf service from Symfony's container. Laravel has no AppKernel, no bundle loader, and its container contract doesn't accept Symfony bundle bootstrapping. There is no --ignore-platform-reqs trick that makes this useful; the last release was Feb 2017 and its symfony/symfony ~2.x constraint will fight your lockfile.
So the honest "getting started" is: read this package's source (it's ~50 lines) to understand the pattern, then reproduce it in Laravel two ways.
Path A — the maintained Laravel equivalent (5 minutes):
composer require barryvdh/laravel-dompdf
php artisan vendor:publish --provider="Barryvdh\Dompdf\ServiceProvider" --tag=config
use Barryvdh\DomPDF\Facade\Pdf;
$pdf = Pdf::loadView('invoices.show', ['invoice' => $invoice]);
return $pdf->stream('invoice.pdf'); // streamHtml() equivalent
$bytes = $pdf->output(); // getPdf() equivalent
Path B — bind dompdf yourself (20 lines, full control):
// app/Providers/DompdfServiceProvider.php
use Dompdf\Dompdf;
use Dompdf\Options;
public function register(): void
{
$this->app->bind('dompdf', function () {
$opts = new Options();
$opts->set('dpi', config('dompdf.dpi', 150));
$opts->set('defaultPaperSize', config('dompdf.paper', 'A4'));
$opts->set('defaultFont', config('dompdf.font', 'DejaVu Sans'));
$opts->set('isRemoteEnabled', false); // see Gotchas
$opts->set('chroot', base_path('resources/pdf'));
return new Dompdf($opts);
});
}
The source of truth is dompdf/dompdf itself — pin and monitor that package for advisories. es/dompdf-bundle adds nothing you can't reproduce in a service provider.
First use case to build: a single invoice/receipt endpoint — GET /invoices/{invoice}/pdf — rendering a Twig-equivalent Blade view (resources/views/pdf/invoice.blade.php) with inline CSS. Ship that, confirm the file opens, then expand. Don't start with a report builder.
The two primitives, mapped. The bundle's entire surface is streamHtml($html, $filename) and getPdf($html). In Laravel those become:
| Bundle | Laravel equivalent | When |
|---|---|---|
streamHtml($html, 'doc.pdf') |
Pdf::loadHTML($html)->stream('doc.pdf') |
Interactive download ("Download invoice") |
getPdf($html) |
Pdf::loadHTML($html)->output() |
Store to disk or attach to mail |
| — | ->download() |
Force-download with attachment disposition |
Pattern 1 — controller stays thin, renderer lives in a service.
final class InvoicePdf
{
public function __construct(private Invoice $invoice) {}
public function render(): string
{
return Pdf::loadView('pdf.invoice', [
'invoice' => $this->invoice->load('lines'),
'logo' => $this->logoDataUri(),
])->setPaper('a4', 'portrait')
->setOption('isRemoteEnabled', false)
->output();
}
}
Call it from the controller (return response($pdf)->header('Content-Type', 'application/pdf')) and from a queued job. This is the whole point: keep the "renderer" swappable so a Browsershot migration later is a class swap, not a rewrite.
Pattern 2 — store, don't stream, for anything recurring. stream() re-renders on every hit and pure-PHP rendering is 200 ms–2 s per document. Persist the PDF and serve the stored file:
Storage::put("invoices/{$invoice->id}.pdf", $service->render());
return Storage::download("invoices/{$invoice->id}.pdf");
Regenerate only when the underlying data changes (use a hash of line items in the filename or a pdf_generated_at column).
Pattern 3 — move generation off the request path. dompdf is CPU- and memory-bound and synchronous. For statements, packing slips, or any batch:
GenerateInvoicePdf::dispatch($invoice); // queued job writes to disk, notifies user
Set a queue worker with a raised memory_limit (dompdf loads the whole doc) and a timeout. Never loop-render in a controller for N > ~5 documents.
Pattern 4 — inline assets, always. This is the biggest day-to-day gotcha. Blade images via asset() or {{ asset(...) }} won't resolve — dompdf has no HTTP client and no public webroot path unless you point chroot at it. Use base64 data URIs for logos:
'logo' => 'data:image/png;base64,' . base64_encode(file_get_contents(resource_path('pdf/logo.png'))),
CSS should be inline in the Blade view (or <style> in the document head). External stylesheet links fail silently.
Pattern 5 — testing. Assert on the binary, not the visual output:
$response = $this->get("/invoices/{$invoice->id}/pdf");
$response->assertOk();
$response->assertHeader('Content-Type', 'application/pdf');
$this->assertStringStartsWith('%PDF-', $response->streamedContent());
For deeper assertions, pdfparser or Smalot\PdfParser can extract text and let you verify "Invoice #1234 appears." Do not try to pixel-compare — dompdf output is deterministic but version-sensitive.
Don't install this package — anywhere. 0 stars, 0 dependents, last release 2017, Symfony 2 EOL since Nov 2018. If a teammate insists on "the es bundle," clarify the requirement: they almost certainly mean "dompdf in Laravel," and barryvdh/laravel-dompdf (same underlying engine, actively maintained) is the correct answer. If it's a Symfony→Laravel migration, port the two-method interface into a service provider; the adapter is ~1 day.
Security is the real reason this matters, not the stars. dompdf has a documented history of SSRF, chroot escape, and local-file-inclusion advisories around remote resource fetching. A 2017-pinned dompdf predates years of hardening. With Laravel's maintained wrapper you still must:
// config/dompdf.php
'isRemoteEnabled' => false, // default OFF: blocks remote file/URL fetch
'chroot' => storage_path('app/pdf'), // restrict local reads to a sandbox
'isPhpEnabled' => false, // never enable in the renderer
If a feature needs remote images, download them server-side, validate the host against an allowlist, and pass base64 — never set isRemoteEnabled = true while user HTML touches the renderer. Anywhere you render user-influenced HTML (e.g. a custom notes field inside a template), treat it as an injection surface; strip tags or use a strict allowlist.
Symfony config key mapping. The bundle's es_dompdf.defaults YAML maps one-to-one to dompdf Options, and barryvdh/laravel-dompdf exposes the same keys under config/dompdf.php. Port dpi, defaultPaperSize, defaultFont, chroot, isRemoteEnabled, isHtml5ParserEnabled directly. Note defaultPaperSize (the bundle key) becomes paper-style config in the Laravel wrapper — check the published config, don't assume key parity.
Paper size and fonts are the most common "it looks broken" causes. ->setPaper('a4', 'portrait') per call beats a global default when you mix invoices (A4) and tickets (A5 or custom mm). If non-English glyphs (Cyrillic, CJK, emoji) render as boxes, it's a font issue: register DejaVu Sans (bundled with dompdf) or a font with the glyphs, not a layout issue.
Modern CSS won't work. dompdf renders a limited CSS 2.1 subset. No flexbox, no grid, no JS, no position: sticky. Use tables and floats. If you find yourself fighting the renderer on a branded template, that's your signal to route to Browsershot/Gotenberg — a renderer decision the wrapper can't help with.
Performance. A single render is hundreds of ms to a couple seconds and is memory-heavy. Budget it, cache it, or queue it. Do not render PDFs inside a DB::transaction() — a slow render holds the transaction open.
Version pinning is your job. The bundle is dead, so nothing pins dompdf for you. In your own composer.json, pin dompdf/dompdf to a maintained major (≥2.x) and run composer audit in CI — a 2017-era transitive dompdf is exactly what composer audit should block. This is the maintenance posture the abandonment forces on you.
Where to extend. If you do port the interface, keep the two-method contract (streamHtml/getPdf) on your own adapter and swap its internals — first to the maintained wrapper, then possibly to a browser renderer — as separate, revertible commits. That's the one genuinely valuable takeaway from this package: a tiny, well-defined seam.
Bottom line for a Laravel dev: adopt dompdf via barryvdh/laravel-dompdf or a self-bound service provider; skip the Symfony bundle. It buys you ~20 minutes of wiring and costs you a maintenance obligation on a package nobody is using.
How can I help you explore Laravel packages today?