tfox/mpdf-port-bundle
A Symfony bundle that integrates the mPDF library, providing service configuration and a straightforward way to generate PDFs from HTML in Symfony applications.
Reality check before install: this is a Symfony2 bundle, not a Laravel package. Its composer.json targets Symfony 2.x / PHP 5.x and depends on Symfony\Component\HttpKernel\Bundle\Bundle, the Symfony DI extension model, and Resources/config/services.yml conventions. It will not resolve alongside Laravel 10/11's symfony/* transitive requirements, and even if it did, Laravel's container has no notion of bundles. There is no "install and go" path here.
What you actually start with: clone the Bitbucket repo read-only and read three things, in this order:
Resources/config/services.yml — the service wiring. This is the whole bundle's value: how it registers mPDF as a container service and what it injects.DependencyInjection/Configuration.php — the config tree. This is your spec document for which mPDF defaults (tempDir, fontDir, fontdata, page format, margins, orientation) the author considered worth externalising.new mPDF(...) happens, so you know what a "configured instance" looks like.Your first real use case: take that pattern and stand it up on mpdf/mpdf (the maintained upstream) in a fresh Laravel app — roughly 1–3 engineering days:
// config/pdf.php
return [
'temp_dir' => storage_path('app/mpdf/tmp'),
'font_dir' => storage_path('app/mpdf/fonts'),
'font_data' => [],
'defaults' => ['format' => 'A4', 'margin_top' => 20],
];
// app/Services/Pdf/MpdfRenderer.php — implements PdfRenderer
// app/Providers/PdfServiceProvider.php — binds PdfRenderer as singleton
Then render your first document: a Twig/Blade-rendered invoice template fed to $mpdf->WriteHTML($html) and returned via $mpdf->Output('', 'S'). That single round-trip tells you more about the engine than any doc.
Where to look when it breaks: the upstream mPDF manual's HTML/CSS support table and tempDir/fontdata sections — not this bundle, which will not have answers.
Preserve the one good idea: configuration out of call sites. The bundle's entire architectural contribution is "engine config lives in DI, call sites just render." Keep that in Laravel as config/pdf.php + a bound singleton. Everything else — the MpdfPortBundle class, the DI extension lifecycle, the Symfony version constraints — is dead weight and zero of it is reusable.
Bind to an interface, not the concrete class. This is the highest-leverage decision in the whole exercise:
interface PdfRenderer {
public function render(string $html, array $options = []): string; // binary
}
With this seam, mPDF → Dompdf → Gotenberg/Browsershot becomes a config swap rather than a refactor of every controller that generates a statement.
Inject a factory, not a shared mPDF instance, for multi-document work. mPDF is stateful — one instance per document. The bundle's factory service exists precisely for this. Pattern:
class StatementService {
public function __construct(private MpdfFactory $factory) {}
public function generate(Account $a): string {
$mpdf = $this->factory->make(['format' => 'A4-L']);
$mpdf->WriteHTML(view('pdf.statement', compact('a'))->render());
return $mpdf->Output('', 'S');
}
}
Render through Blade to a string, then hand it to mPDF. Treat the template as the unit of work — version it in git, snapshot-test it. Do not scatter WriteHTML calls across a controller; assemble one HTML string from one template.
Move rendering behind a queue job from day one if documents exceed ~10 pages or embed images. mPDF is synchronous and memory-hungry; inline in a request it caps your p95 latency and can OOM your PHP-FPM pool. Starting async is cheap; retrofitting it is not. Set an explicit memory_limit and timeout on the job.
Integration tip: disable remote resource loading. mPDF will fetch remote images/styles referenced in HTML. Set $mpdf->showImageErrors = false and allow-list or disable remote fetching — you are rendering potentially attacker-influenceable HTML.
You cannot install this into Laravel — stop trying after ten minutes. Any time spent fighting Composer resolution of Symfony 2.x constraints is time you could have spent writing the ~150-line equivalent. The recommendation from every angle is: adopt the engine (mpdf/mpdf), re-implement the pattern, do not adopt the package. It has 0 stars, 0 dependents, and no maintenance signal — treat it as read-only reference material.
Licensing is a hard gate, not a footnote. mPDF is GPLv2 (commercial licence available from the maintainer). If you are shipping redistributable on-prem software, GPLv2 obligations attach before you write a line. Clear this with legal in Phase 0 — it can invalidate the whole approach. The bundle's own licence must be checked separately.
CSS support stops at roughly CSS 2.1. No flexbox, no grid, no modern layout. This is an engine constraint and it shapes your template contract: get print-oriented HTML from design (tables, floats, absolute units), not a web layout. Discover this on a spike, not in production.
Font handling is the classic compatibility trap. mPDF needs a writable tempDir and a font cache; CJK/RTL fonts add tens of megabytes per generated file. Validate against your actual production fonts before estimating output size or memory. The tempDir must exist and be writable or you get opaque failures — create it in the service provider, not the template.
Pin the mPDF minor version. A patch release can silently shift pagination on archived/regulated documents. Pin it in the lockfile, and golden-file diff PDFs (or their text extracts) in CI so pagination regressions surface as test failures.
Security surface: old mPDF releases carried known CVEs. Only the current upstream release is defensible. Run the security advisory check before it enters the lockfile, and keep remote-image fetching off.
Debugging tip: when output looks wrong, $mpdf->Output($path, 'F') to disk and inspect the file rather than streaming — half of "it's blank" issues are an exception swallowed during WriteHTML or a missing tempDir.
Extension points that translate cleanly: a PdfRenderer interface (swap engines), a custom header/footer renderer using mPDF's {PAGENO}/{nbpg} tokens for correct "page X of Y", and a queue job boundary (scale throughput). The bundle's Symfony-specific extension hooks — DI compiler passes, bundle boot — do not translate and should be discarded.
Performance ceiling: mPDF degrades on very large documents and memory-constrained pools. Benchmark one real statement at production size (p95 pages, images, scripts) before promising "download now" versus "we'll email it to you."
How can I help you explore Laravel packages today?