nitmedia/wkhtml2pdf
Laravel wrapper around wkhtmltopdf for converting HTML and Blade views into PDF files. Set your binary path and options in config, then generate, stream, or download PDFs through a simple facade, with support for page size, orientation, and other wkhtmltopdf flags.
This package is a thin facade wrapper around the wkhtmltopdf command-line binary — it does not render HTML itself. Your first job is getting the binary working, not the PHP.
composer require nitmedia/wkhtml2pdf
config/app.php:
'providers' => [
Nitmedia\Wkhtml2pdf\Wkhtml2pdfServiceProvider::class,
],
'aliases' => [
'PDF' => Nitmedia\Wkhtml2pdf\Facades\Wkhtml2pdf::class,
],
which wkhtmltopdf
wkhtmltopdf --version # confirm 0.12.x
wkhtmltopdf https://example.com /tmp/test.pdf # prove it works standalone
binpath/binfile point at the absolute path of that binary (e.g. /usr/local/bin/wkhtmltopdf). Do not assume PATH is inherited by PHP-FPM.Route::get('/invoices/{invoice}', function (Invoice $invoice) {
return PDF::view('pdf.invoice', ['invoice' => $invoice])
->filename("invoice-{$invoice->id}.pdf")
->download();
});
If step 3 doesn't work in your terminal, stop — no amount of PHP config will fix the package until the binary works.
Builder chain with terminal output methods. Day-to-day use is a fluent chain that ends in one of three terminals:
->render() — returns the raw result / response object; return it directly from a controller.->download() — sets Content-Disposition: attachment, good for "Save my invoice".The pattern is always source → options → filename → terminal:
return PDF::view('pdf.report', compact('rows'))
->filename('report.pdf')
->setOptions([
'margin-top' => 10,
'margin-bottom' => 10,
'footer-center' => 'Page [page] of [topage]',
'footer-font-size' => 8,
])
->download();
Global vs per-request options. Put site-wide defaults (page size, margins, DPI, header/footer text, --print-media-type) in the published config so every PDF is consistent. Use setOption()/setOptions() only for the outliers — landscape statements, a one-off --orientation landscape, or --javascript-delay for a chart.
Render Blade views, not string-concatenated HTML. PDF::view('...', $data) keeps your PDF templates alongside your other Blade files, gives you @include, and makes the PDF testable with view()->render() assertions. Reserve PDF::html($string) for small snippets or when you already have rendered HTML (e.g. from a Markdown renderer).
Wrap generation in a small service class. Instead of sprinkling chains across controllers, create App\Pdf\InvoicePdf with a generate(Invoice $invoice): string that returns the saved file path. This keeps controllers thin, makes the PDF logic queueable, and means when you eventually swap the renderer (see gotchas) you change one class.
Persist to disk for anything important, stream for anything on-demand.
->filename(storage_path(...))->render(), store the path on the model, serve via a normal download route.Queues are your friend. PDF generation is CPU-bound and blocking. A report that takes 4s synchronous will block a web worker; dispatch it to a queue and write to storage.
Reuse a Blade layout. Because it's just a view, you can share a resources/views/pdf/layout.blade.php across all documents; only the @yield content varies. Keep CSS inline or in a single <style> block — external CSS over HTTP is slow and brittle in wkhtmltopdf's old WebKit.
The binary path is the #1 failure. binpath in config must be an absolute directory with a trailing separator, and binfile the executable name. Under PHP-FPM PATH and even HOME may differ from your shell — always test by hitting a route, not by php artisan tinker alone (tinker inherits your shell env).
wkhtmltopdf needs headless graphics libs. On slim Docker images you'll see error while loading shared libraries: libXrender.so.1 or libfontconfig. Install libxrender1 libfontconfig1 libxext6 libssl1.1 (names vary by distro). For newer binaries, running under xvfb-run solves headless crash loops.
Relative asset URLs break. wkhtmltopdf resolves src/href against the HTML source, not your app. Either (a) use absolute URLs (asset(), url()), or (b) inject <base href="https://yourapp"> in your PDF layout. On wkhtmltopdf 0.12.6+, local file references require --enable-local-file-access or images silently vanish.
Old WebKit means old CSS. No flexbox, no CSS grid, limited border-radius/box-shadow support, no modern JS. Test your invoice layout on the binary itself before blaming the package. For JavaScript-built charts use setOption('javascript-delay', 1000) (milliseconds) and --enable-javascript.
Builder state is mutable. Chained calls mutate the same object. In long-running processes (queue workers, Octane, ReactPHP) a shared/reused instance will leak options, filename, and even the HTML from the previous call. Always build a fresh chain per document — never store a PDF instance as a singleton.
config:cache freezes your options. After editing config/wkhtml2pdf.php you must re-run php artisan config:cache (or clear it) in production, or you'll debug the wrong config for hours.
Temp directory must be writable. Large documents and headers/footers are written to a temp path before final output. Ensure storage/ (or whatever the package config points to) is writable by the PHP user — a read-only container will fail with cryptic pipe errors.
Timeouts and memory. Add a per-run timeout and raise max_execution_time for big documents. --no-stop-slow-scripts prevents early termination on JS-heavy pages, but also means it will run to the timeout — pair with a queue.
Argument injection. Options get passed to a shell command. Never interpolate user input into keys or values (e.g. a user-supplied footer-html URL). Whitelist options if they're configurable by end users.
It's a 2017 package on an unmaintained path. Practical implications:
PdfGenerator interface. If you outgrow it, swapping to mikehaertl/phpwkhtmltopdf (same binary, better API) or spatie/browsershot / barryvdh/laravel-dompdf becomes a one-class change instead of a codebase-wide find/replace.composer update doesn't surprise a production deploy.Testing tip. Bind a fake implementation of your own PdfGenerator interface in tests; assert the HTML passed to it rather than shelling out to a real binary in CI. If you must assert output, assert the file exists and is non-empty — never compare PDF bytes.
How can I help you explore Laravel packages today?