pxlrbt/laravel-pdfable
pxlrbt/laravel-pdfable adds a lightweight trait to Eloquent models for generating PDFs. Point a model at a Blade view, then download or stream the finished PDF with a single method call—keeping document generation out of your controllers. Handy for invoices, reports, and exports.
1. Check the framework gate first. v1.0.0 targets Laravel 11, 12, and 13 — Laravel 9 and 10 support was dropped. If composer show laravel/framework reports anything older, that's an upgrade ticket, not a footnote.
composer require pxlrbt/laravel-pdfable
php artisan vendor:publish --tag="pdfable-config"
2. Read the config file before the README. It is the shape of the package: a default driver plus a drivers map, with paper size, orientation, driver options, and any engine-specific settings. Most "why does my PDF look like that" questions are answered there rather than in your code. After the config, read the driver classes (they are small) and the v1.0.0 Pest suite — the tests are the real contract for method signatures and fake behaviour. Treat the README as a starting point and the tests as truth.
3. Pick your first driver deliberately.
noSandbox in Docker, and a timeout. Prove Chromium works in a throwaway script before you wire it into a queue worker; debugging Chromium-through-PHP-inside-a-job is not where you want to begin.4. First use case: one model renders itself. The package's opinion is that a document is model-bound — an Invoice owns its presentation. Add the model-side trait (check the tests for the exact trait name and required methods: view, view data, filename, optional driver override), point it at a Blade view, and your controller collapses to:
return $invoice->pdf()->download();
Learn this pattern first. It scales to receipts, statements, packing slips, and certificates without new plumbing.
5. Render one real file, then write your first test. Before any test, render to disk and open the PDF in a viewer. Then add a feature test using the package fake and assert the right view and data were requested with the right driver and filename. Don't assert pixels; do assert wiring.
Pdf::fake();
$this->get("/invoices/{$invoice->id}/pdf")->assertOk();
Pdf::assertRendered('pdf.invoice', fn ($data) => $data['invoice']->is($invoice));
Model-bound rendering as the default. Put the trait on the document model and let the model answer "how do I look?" — view name, view data, filename, driver override, paper options. Controllers stay one-liners. This is also the only tractable route to multi-tenant branding: if rendering is a method on the model, per-tenant theming becomes template resolution (pick a view from a tenant/brand key) rather than a fork in every render call. Build that resolution in one place on day one.
Fluent builder for ad-hoc documents. Reports, exports, and one-off statements that aren't models use the builder directly:
Pdf::view('pdf.monthly-statement', ['account' => $account, 'rows' => $rows])
->driver('browsershot')
->paper('a4', 'landscape')
->render();
Keep exactly one method per document type that owns this call — e.g. MonthlyStatementPdf::render($account). Never scatter Pdf::view(...) across controllers; the day you swap drivers, add a footer, or fix a filename, you'll be grepping.
Wrap it in your own adapter immediately. This package has one maintainer and effectively zero public dependents. Define a one-method interface (DocumentRenderer::render(Document $doc): RenderedPdf) and a thin implementation that calls this package. Everything else in your app depends on your interface. That seam costs an hour and buys you a drop-in replacement if the package is abandoned (direct Browsershot call, Gotenberg, a commercial API) plus one natural home for cross-cutting concerns: storage paths, expiry, render-time metrics, tenant theming.
Async generation, storage, signed URL. Don't stream PDFs inline on the request in production. Render in a queued job, write to a private disk path, record the artifact (path, content hash, template version, expiry), notify the user, serve via a short-lived signed route. This buys retries, immunity to PHP request timeouts on large documents, a natural cache (same inputs + same template version → same artifact), and the ability to regenerate when a template changes. Decide this before the first ticket — retrofitting it means rewriting every call site.
Driver per document type, chosen and documented. The abstraction's value is letting you defer the cheap-vs-accurate call until you have cost data — not avoiding it forever. Sensible split: Dompdf for internal exports and transactional receipts where "good enough" is fine; Browsershot for customer-facing documents where brand fidelity matters. Write the reason in a comment next to the driver override. The failure mode this package invites is shipping a mediocre default renderer because it happened to be the one configured.
Blade conventions that pay off. One shared layout with slots for header, footer, and page numbers; a dedicated resources/views/pdf/ namespace; all CSS in a single <style> block (no Vite/Tailwind in the render path — the renderer does not run your asset pipeline); print rules (@page, break-inside: avoid, repeating thead). Pass computed values in from PHP — the template should be dumb. Pre-aggregate in SQL rather than looping in Blade.
Two-layer testing. Use Pdf::fake() in feature tests to prove the wiring (view, data, driver, filename). Separately render your Blade view as HTML and assert on content — that's where regressions actually live (a missing @foreach key, a null logo, a broken total). Keep one or two tests that render for real, marked slow or in a separate group, so a driver or template break can't hide behind the fake. Also assert authorization (can user A download user B's invoice?) and that artifact cleanup actually deletes files on expiry.
Local dev loop. Give yourself a route or Artisan command that renders a fixture document to storage/app/pdf-preview/ so you can iterate on the template and open the file without going through the full feature. That loop is faster than any test and catches layout problems the fake never will.
The two drivers do not produce the same document — and Dompdf's gap is bigger than you assume. Weak or no flexbox/grid, limited cascade support, spotty background-image printing, unreliable web fonts. Develop against the weakest driver you might ship on, or accept that the driver is a per-document commitment rather than a config toggle. Switching "just to try it" will collapse your layout.
Fonts and images are the #1 "works on my machine." Remote images and webfonts need explicit enablement and can fail silently (blank box, no exception). Prefer local assets via absolute filesystem paths, register custom fonts in config, and make sure those files exist on the queue worker's filesystem — which may be a different container from the web one. If you must use URLs, set APP_URL correctly in every environment; workers build asset URLs from it, and a wrong value is a missing logo nobody notices for a quarter.
config:cache bites driver swaps. Changing the default driver then seeing no effect in staging is almost always an un-rebuilt config cache. .env values are read at cache time, not request time.
Browsershot needs babysitting in Docker and in workers. Expect to configure the node binary, Chrome path, noSandbox, and an explicit timeout. Chromium is CPU- and memory-hungry: run PDF jobs on a dedicated queue with bounded workers, an explicit tries, and a job timeout slightly above the render timeout. Keep Puppeteer concurrency low — parallel Chromium instances are how you OOM a small box. Long-running workers can leak; recycle them.
Retries plus documents plus email equals duplicates. Set tries deliberately. A render job that re-sends a notification on every attempt emails the same invoice twice. Make the notification idempotent, keyed on the artifact.
Dompdf degrades hard on large documents. Thousands of rows or embedded high-res images will hit memory_limit or time out. Mitigate with SQL aggregation, chunked multi-page reports, and downscaled images. If you're reaching for ini_set('memory_limit', ...), that's a signal to move that document to Browsershot or out to a rendering service.
N+1 queries in Blade are invisible until production. The template looks like a template but is application code. Eager-load everything the view touches. A queued render taking 40 seconds is usually lazy loading, not the renderer.
The fake does not render. It records intent and will happily pass while the real PDF is a blank page. Never let a green suite substitute for opening a document — especially not after a driver upgrade.
Escape everything; use {!! !!} deliberately. View data is customer names, addresses, and line-item descriptions — all user-controlled. In PDF templates the urge to raw-print HTML is high, and the XSS surface feels less obvious than on the web, but you are still rendering untrusted input into a file you may email or archive.
Storage is not automatically private. Confirm the disk you save to isn't publicly exposed, and confirm the signed route's expiry matches your documented retention. Test that cleanup actually deletes artifacts.
Pin the version and read release notes before upgrading. v1.0.0 was a stability and compatibility release (framework support, a real test suite) — no new drivers, no new document features. Capability gaps will persist; verify your target renderer is covered by the shipped driver set before promising a design.
Know where the abstraction ends. Multi-table financial reports, charts, precise repeating headers, PDF/A accessibility tagging, digital signatures, and interactive forms are all beyond a general-purpose renderer wrapper. When you hit one, drop down and write against the engine directly — but keep your adapter interface in place so the boundary stays visible instead of dissolving into the codebase.
How can I help you explore Laravel packages today?