mfabdelaziz/weasyprint-bundle
Symfony bundle that integrates WeasyPrint so your PHP app can convert HTML and CSS into high-quality PDFs—reports, invoices, tickets, and more. Register it as a service, render Twig templates to PDF, stream or save the output, and skip fragile headless-browser setups.
mfabdelaziz/WeasyPrintBundle is a thin PHP wrapper around the WeasyPrint CLI (a Python HTML→PDF engine). It does not render PDFs in PHP — it shells out to the weasyprint binary — so setup is as much about the host environment as it is about Composer.
composer require mfabdelaziz/weasyprintbundle
python3 -m pip install weasyprint
weasyprint --version # must succeed as the same user PHP-FPM runs as
WeasyPrint needs system libraries (Pango, cairo/pangoft2, gdk-pixbuf, libffi). On Debian/Ubuntu: apt install libpango-1.0-0 libpangoft2-1.0-0 libcairo2 libgdk-pixbuf-2.0-0.PATH:
// config/weasyprint.php
'binary' => env('WEASYPRINT_BINARY', '/usr/local/bin/weasyprint'),
$html = view('invoices.pdf', ['invoice' => $invoice])->render();
$pdf = WeasyPrint::fromHtml($html);
return response($pdf, 200, [
'Content-Type' => 'application/pdf',
'Content-Disposition' => 'inline; filename="invoice-'.$invoice->number.'.pdf"',
]);
Where to look first: the package's service class (usually src/ root) and its config file. Confirm three things before writing feature code: (a) whether it accepts raw HTML and a URL, (b) whether it returns a string, a stream, or writes to a path, and (c) whether it wraps Symfony Process (good — safe escaping) or builds a shell string (bad — you must escape).
Render from a Blade view, not string concatenation. Build a dedicated resources/views/pdf/ tree with its own layout that inlines CSS. WeasyPrint's CSS support is strong for paged media but partial for modern web CSS — no flexbox quirks you'd expect from Chrome, no JS execution at all.
Use @page rules for pagination. This is WeasyPrint's superpower and the reason to choose it over headless Chrome:
@page { size: A4; margin: 20mm 15mm;
@bottom-center { content: "Page " counter(page) " of " counter(pages); } }
thead { display: table-header-group; } /* repeat table headers across pages */
tr { page-break-inside: avoid; }
Resolve asset URLs absolutely. WeasyPrint fetches CSS/images relative to the document's base URL. When you pass an HTML string there is no base, so relative href="css/app.css" silently fails. Fix with either an explicit base tag or a file path:
$html = view('pdf.invoice', $data)->render();
$html = str_replace('<head>', '<head><base href="'.public_path().'/">', $html);
For CSS use asset()/public_path() so URLs are absolute in every environment. file:// URLs work but must be absolute and readable by the FPM user.
Push generation to a queue for anything nontrivial. WeasyPrint is CPU- and memory-bound; a 60-page report can take seconds and hundreds of MB. Generate in a job, store to disk, and let the controller redirect to a signed URL:
class GenerateInvoicePdf implements ShouldQueue {
public $timeout = 120;
public function handle(WeasyPrint $wp) {
$path = "invoices/{$this->invoice->id}.pdf";
Storage::put($path, $wp->fromHtml(view('pdf.invoice', [...])->render()));
}
}
Set the job timeout below your worker's retry_after, and raise memory_limit for the worker specifically — not globally.
Wrap it in your own service. Bind a small PdfRenderer interface in a service provider and have it delegate to the package. When WeasyPrint's API shifts (or you swap to Chromium for a page that needs JS), you change one class instead of forty call sites.
Test against the real binary in CI. Assert %PDF- is the response prefix and page count matches expectations; a unit test with a mocked renderer tells you nothing about the environment.
weasyprint: command not found in production but fine in your terminal. This is the #1 issue. PHP-FPM runs with a minimal PATH and a different user. Always configure an absolute binary path, and verify with sudo -u www-data /usr/local/bin/weasyprint --version.1.2.0 tag merge into develop and a composer.json edit — no new public API, no deprecations, no breaking changes. The composer.json change is the only thing worth a second look: run composer update mfabdelaziz/weasyprintbundle and diff the constraint, since metadata edits sometimes tighten the PHP version or add/move dependencies. If your lockfile resolves cleanly and php artisan about is happy, no code changes are needed.fonts-dejavu, fonts-liberation, or copy TTFs into ~/.fonts and run fc-cache -f). Non-Latin scripts (Arabic, CJK) almost always need an explicit font-family plus an installed font.<img>.stdin (Process::setInput()) over a temp file; if you must use a temp file, create it with tempnam() and delete it in a finally block.ARG_MAX limit if it's passed as a CLI argument. Another reason to use stdin.base_url versus file:// matters for security. If user-controlled HTML is ever rendered, disable remote resource loading — otherwise you've built an SSRF vector (WeasyPrint will happily fetch http://169.254.169.254/).--optimize-images, --dpi, --presentational-hints) are the main knobs. If the package's config exposes a "custom arguments" array, prefer it over sub-classing; if it doesn't, that's the one place a fork or a thin adapter is justified.weasyprint>=60 changed several rendering defaults; a server rebuild that silently upgrades Python deps can shift pagination in production PDFs. Put the version in your provisioning Dockerfile/Ansible role, not just in a README.How can I help you explore Laravel packages today?