Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Dynamicpdf Plugin Laravel Package

renatio/dynamicpdf-plugin

Generate dynamic PDF documents in October CMS with Twig-based templates, backend management, and flexible PDF generation for orders, invoices, reports, and other tailored outputs.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and migrate. The plugin is a standard October CMS plugin, so install it with Composer from the project root and run the migrations:

composer require renatio/dynamicpdf-plugin
php artisan october:migrate   # October v3; use october:up on v2
php artisan dynamicpdf:check  # lists drifted/customized templates and layouts

If you install through the Marketplace/backend instead, still verify the migrations ran and that the renatio_dynamicpdf_templates / layout tables exist before debugging rendering issues. From 9.0.0 there are no new migrations — 9.0.1 is a fixes-only release on top of the 9.0.0 upgrade, so read UPGRADE.md in the plugin root before you touch a working install.

Where to look first.

  • The backend PDF menu (PDF Templates and PDF Layouts) is the primary UI — this is where non-developers will edit HTML/Twig.
  • UPGRADE.md — mandatory reading for 9.x. It documents the sandbox behaviour introduced in 9.0.0 and how byCode() behaves when the database has no row (#352), which is exactly the kind of thing that silently returns null in a queue worker.
  • classes/PDF.php is the facade/entry point. Read this file before the README; it tells you exactly which methods your installed version exposes (loadTemplate, loadView, loadHTML, setData, setPaper, setOption(s), download, stream, save, output, the encryption/password methods, and fake()).
  • models/PDFTemplate.php shows how template code is resolved, how the Twig context is built, and what the 9.x sandbox permits.
  • Check composer.lock for the bundled dompdf/dompdf version — almost every rendering quirk traces back to it.

First use case: an order/invoice PDF. Create a layout with your letterhead and footer, then a template with code invoice and a body like:

<h1>Invoice #{{ order.number }}</h1>
<p>{{ order.customer.name }} — {{ order.created_at|date('Y-m-d') }}</p>
<table>{% for item in order.items %}...{% endfor %}</table>

Then render it from a controller, CMS component, or job:

use Renatio\DynamicPDF\Classes\PDF;

return PDF::loadTemplate('invoice')
    ->setData(['order' => $order])
    ->setPaper('a4')
    ->download("invoice-{$order->number}.pdf");

That single call — template code plus a data array — is the whole mental model of the package. Everything else is options around it. One caveat up front: in 9.x, any model method, relation, or accessor the sandbox blocks becomes an empty value, not an error. Load relations and build the data array before you call loadTemplate().

Implementation Patterns

Wrap the facade in your own service. Don't scatter PDF::loadTemplate(...) calls across controllers. Create something like App\Services\InvoicePdf (or a plugin class) with methods stream(Order $order), save(Order $order): string, download(Order $order). This gives you one place to change paper size, filenames, data shaping, and one place to swap the underlying library if the plugin stalls. With ~30 stars and zero dependents, that abstraction is cheap insurance.

Treat template codes as constants. Template codes are string keys looked up in the database. Define them (const TEMPLATE_INVOICE = 'invoice';) so a rename in the backend fails loudly in code review rather than silently at runtime. Since 9.x, byCode() on a missing row is documented behaviour rather than an implicit one — read UPGRADE.md and decide explicitly in your wrapper whether a missing template should throw or return null, then enforce that in one place.

Use layouts for anything repeated. Letterhead, address blocks, footer with company details, and page-number scaffolding belong in a layout. Keep templates focused on the variable content. This also means a branding change is one backend edit, not N template edits — and 9.0.1 stopped flagging layouts that differ from their view only by indentation or line breaks as "customized", so dynamicpdf:check output now means something.

Choose the output mode deliberately.

  • stream() — inline preview in the browser; useful for authenticated "view invoice" routes.
  • download() — user-initiated save; sanitize filenames and avoid user-controlled paths.
  • save($path) / output() — the workhorse for automation: save to Storage::disk('local'), then attach to mail, expose via signed URL, or push to S3.
  • For anything slow or bulk, generate in a queued job and store the file; never generate a 200-page PDF synchronously in a web request.

Shape the data before it reaches Twig. Eager-load relations ($order->load('items.product', 'customer')) and pass plain arrays or a small DTO where practical. Under the 9.x sandbox this is no longer just a performance concern: an unloaded relation or an accessor outside $appends renders as an empty value and only emits a warning to the log. Passing a pre-built array sidesteps the whole class of problem.

Keep developer-owned templates in code. Use PDF::loadView('vendor.plugin::pdf.invoice', $data) for templates that change with deploys, and DB templates only where business users genuinely need to edit copy/layout. A hybrid works well: DB layout for branding, code view for structure. Note that loadView content is subject to the same sandbox restrictions in 9.x.

Preview route with real auth. Add a signed, permission-checked route that streams the PDF so support staff can verify output in production-like data without downloading files.

Backend controllers and AJAX. Returning PDF::...->stream() from a backend controller works fine for a "Preview" button; for lists, prefer a redirect to a signed URL or a queued generation to avoid long AJAX requests.

Bulk generation via console command. A command that iterates records in chunks, calls your service, and writes files is far more debuggable than a web loop, and lets you resume after failures. Run dynamicpdf:check as part of your deploy checklist alongside it.

Testing with PDF::fake(). 9.0.1 adds throwOnRender($exception) (#353), so you can now assert failure paths, not just happy paths:

PDF::fake()->throwOnRender(new \RuntimeException('render failed'));

$this->expectException(\RuntimeException::class);
$this->get("/orders/{$order->id}/invoice");

Pair that with a real (unfaked) smoke test that renders each template code against fixture data — that's what catches broken Twig and sandbox-emptied variables before users do.

Localization. Set the app locale (or pass a locale variable into the data) before rendering; if documents differ substantially by language, use per-locale template codes rather than conditional Twig spaghetti.

Gotchas and Tips

The 9.x sandbox turns mistakes into blank fields, not exceptions. If a template calls a model method, touches a relation that wasn't eager-loaded, or reads an accessor outside $appends, the sandbox resolves it to an empty value. Since 9.0.1 you get a log warning with the template name and line number (#349) — after upgrading to 9.x, generate your PDFs once and read the log. If invoices suddenly have an empty customer name, that warning is the first place to look, and eager-loaded arrays are the fix. Budget time for this when upgrading from 8.x.

Self-signed certificates are ignored in production — again. In 9.0.0, allowSelfSignedCertificates() and the DYNAMICPDF_ALLOW_SELF_SIGNED env var accidentally disabled TLS certificate verification in production too (#350). 9.0.1 restores the guard, so those switches only apply outside production. If you relied on them to reach an internal HTTPS asset server, fix the certificate chain instead of reaching for the flag — and never set it in a production .env expecting it to work.

dompdf CSS is not browser CSS. No flexbox, no grid, weak support for modern selectors. Use tables, floats, and explicit widths. If a layout looks right in Chrome and wrong in the PDF, it's almost always CSS support, not the plugin. Build a one-page "kitchen sink" template to test the CSS features you rely on before designing a full invoice.

Remote assets and chroot. Images referenced by URL require remote loading to be enabled (isRemoteEnabled), and local file paths must sit inside dompdf's allowed root. The reliable pattern is absolute filesystem paths for local images, or base64-embedded images, especially for logos that must appear on every document. Relative media URLs frequently break in queued/CLI contexts where there's no request host.

Fonts and the font cache. Custom fonts need registration and a writable cache directory; after adding a font, clear the dompdf font cache or you'll spend an hour wondering why the old font persists. Verify the cache path is writable by both the web user and the queue worker — a classic "works in browser, fails in queue" bug.

Page numbers and footers are the sharpest edge. They're layout-level concerns and depend on the bundled dompdf version's support for page-text scripting versus CSS page rules. Test with a multi-page document, not a one-pager, and decide early whether you'll allow inline PHP in templates (isPhpEnabled) — it's powerful but a genuine security concern if non-developers can edit templates.

DB-stored templates have no version control. There's no diff, no code review, no rollback. Export template HTML to files in your repo via a command or seeder so you have history, and restrict the PDF backend section with October's permission system. If template editors aren't fully trusted, remember Twig is a programming language — the 9.x sandbox narrows the attack surface but does not make template injection a non-issue.

Password protection isn't confidentiality magic. Encryption adds processing time and file size, and some viewers/automation pipelines handle restricted PDFs poorly. Test with the recipients' actual tooling (mail gateways, CRMs) before promising protected statements.

Memory and timeouts. dompdf builds the whole document in memory. Large tables, hundreds of images, or 500-record batches will hit memory_limit or max_execution_time. Chunk bulk work, raise limits only inside the job, and log per-record failures so a single bad record doesn't kill the batch.

Debug by inspecting HTML first. When a PDF is blank or malformed, render the Twig template to HTML (or log the compiled markup) and open it in a browser. If the HTML is wrong, it's a data/Twig/sandbox problem; if the HTML is right, it's a dompdf CSS/asset problem. Enable dompdf's log output temporarily for the latter.

Path and filename hygiene. save() takes a filesystem path, not a URL — use storage_path(...) or a Storage disk, and generate filenames from IDs rather than user input to avoid traversal and encoding issues.

Pin the version and watch compatibility. Low adoption and a single maintainer mean upgrades can lag October or dompdf releases — 9.0.0 shipped with two regressions that 9.0.1 had to fix days later. Constrain the version in composer.json (^9.0.1, not ^9.0), read UPGRADE.md on every minor, keep your wrapper class thin, and have a fallback plan: the plugin is essentially a template CRUD layer over dompdf, so replacing it with barryvdh/laravel-dompdf plus your own templates is a contained migration if you've kept rendering behind a service.

Testing. Don't snapshot binary PDFs. Assert on the compiled HTML from your service and on basic output properties (non-empty response, expected page count if you parse it, correct content type and filename). Use PDF::fake()->throwOnRender() for failure paths and a real render of every template code as a smoke test — together they catch both broken Twig and unexpected sandbox empties.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment