eve/pdf-converter
A Laravel package for converting PDF files, offering a simple API to handle PDF transformation tasks within your PHP applications. Lightweight and easy to integrate, it helps streamline document conversion workflows with minimal setup.
Install and publish the config
composer require eveio/pdf-converter
php artisan vendor:publish --tag=pdf-converter-config
Set the default driver in .env before you touch any code:
PDF_CONVERTER_DRIVER=dompdf
Install the driver you actually intend to use. The package is a thin abstraction; it does not ship a working renderer by itself in most setups:
dompdf — pure PHP, no system binary. Fastest path to a first PDF.wkhtmltopdf / Snappy-based drivers — requires the wkhtmltopdf binary on the host and a configured path.--no-sandbox in containers.Where to look first
config/pdf-converter.php — driver list, default driver, paper size/orientation, binary paths, options passed through to the underlying renderer.src/Drivers/ — each driver is a small adapter; reading one tells you more than the README about supported options and method signatures.First use case: a Blade-rendered invoice
The canonical day-one use is "I have a Blade view and I want bytes out of it." Register the facade (or inject the contract), render a view with data, and stream it back:
use Eveio\PdfConverter\Facades\PdfConverter;
Route::get('/invoices/{invoice}/pdf', function (Invoice $invoice) {
return PdfConverter::view('pdf.invoice', ['invoice' => $invoice])
->download("invoice-{$invoice->number}.pdf");
});
Get that working end-to-end against dompdf before you introduce a second driver. A large fraction of the pain with this package comes from mixing "which driver" and "which CSS" problems at the same time.
Wrap the facade — don't sprinkle it through controllers. The package has one maintainer, 11 stars, zero dependents, and no release since February 2022. Treat it as a swappable implementation detail behind your own service:
final class InvoicePdf
{
public function __construct(private Invoice $invoice) {}
public function render(): string
{
return PdfConverter::view('pdf.invoice', [
'invoice' => $this->invoice,
'lines' => $this->invoice->lines()->with('product')->get(),
])->toString(); // confirm method name against your installed version
}
}
Then app/Contracts/PdfRenderer.php with a render(Invoice): string signature. When the package stalls or a driver breaks on a PHP upgrade, you swap the binding for spatie/laravel-pdf or Browsershot and change one file.
Separate the PDF layout from the web layout. Do not reuse layouts.app. Create resources/views/pdf/layout.blade.php with:
<style> only — no Vite, no @vite, no external CSS pipeline.font-family (see Gotchas).<div style="page-break-after: always"> between sections rather than relying on CSS break-inside.Driver selection at call time, not config time. Keep dompdf as the default and reach for a heavier driver only where CSS demands it:
PdfConverter::driver('wkhtmltopdf')->view('pdf.report', $data)->save($path);
This is the main thing the abstraction buys you. Use it deliberately: pick per-document-type, document the reason in a comment, and don't let the choice be a per-developer preference.
Persistence pattern for async/heavy documents. Generating on the request thread is fine for one-page invoices and bad for 200-row statements. Standard shape:
// Job
$path = "invoices/{$invoice->id}.pdf";
Storage::disk('local')->put($path, $pdf->render());
$invoice->update(['pdf_path' => $path, 'pdf_generated_at' => now()]);
// Controller — signed, expiring URL
return Storage::disk('local')->temporaryUrl($path, now()->addMinutes(5));
Write to a temp path with save(), then move onto the disk, rather than pointing the renderer directly at an S3 stream wrapper — several drivers do local filesystem operations and will fail or silently produce partial files on remote disks.
Testing without rendering. Assert the HTTP contract, not the PDF bytes:
$response = $this->actingAs($user)->get("/invoices/{$invoice->id}/pdf");
$response->assertOk()
->assertHeader('content-type', 'application/pdf');
$this->assertStringStartsWith('%PDF-', $response->streamedContent());
For unit tests, bind a stub PdfRenderer in the container returning a fixed %PDF-1.4 string. Rendering real PDFs in CI is slow and makes failures opaque (you get a broken file, not a stack trace). Keep one or two "smoke render" tests tagged separately that actually exercise the driver, so you catch the day a base-image change removes the binary.
Config that belongs in config/pdf-converter.php, not in code: paper size and orientation, margin defaults, binary paths, chroot/remote-image flags. Keep per-document overrides (a landscape statement vs. a portrait invoice) at the call site so the config stays a sane global default.
The package is effectively unmaintained — plan for it. Last release 2022-02-14, 11 stars, no dependents. Before committing:
composer.json PHP and illuminate/* constraints against your Laravel version. Laravel 10/11 is likely untested against this package; you may need --ignore-platform-reqs or a fork, and both are red flags on a production dependency."eveio/pdf-converter": "1.x.y") so an abandoned package can't drift on you.Unicode and non-ASCII characters are the #1 DomPDF complaint. Default fonts silently drop or mangle characters outside Latin-1. Set an explicit font stack on body:
body { font-family: "DejaVu Sans", sans-serif; }
and verify with real data — customer names, currency symbols (kr, €), and any locale-specific characters in your actual dataset, not "John Smith."
CSS support is where driver choice actually matters. DomPDF supports roughly CSS 2.1: no flexbox, no grid, limited position. If your existing markup is Tailwind-flexbox-heavy, either rewrite the PDF view as a table layout or switch to a Chrome-backed driver. Discovering this after you've built three documents is the expensive version of this mistake. Make one throwaway test page containing a flex row, a grid, and a page-break and see what your chosen driver does before building on it.
Images need explicit, absolute paths. Remote images usually require a flag in the driver config (is_remote_enabled or equivalent) plus a reachable URL — asset() inside a queued job behind APP_URL=localhost will fail silently, producing a PDF with missing logos and no error. In queued contexts, prefer public_path() for local assets or sign storage URLs.
Binary drivers fail differently from PHP drivers. wkhtmltopdf-style drivers produce a successful PDF with missing content when they run into trouble; DomPDF tends to throw. When a document looks wrong rather than erroring, reach for the binary directly:
/path/to/wkhtmltopdf --enable-local-file-access input.html /tmp/out.pdf
If that works, your problem is the package's argument passing or config, not the renderer.
Config caching bites during driver changes. php artisan config:cache in deploy scripts means a PDF_CONVERTER_DRIVER change in .env appears to do nothing locally if you cached earlier. php artisan config:clear when a driver swap "isn't taking."
Watch memory and time on long tables. A 500-row DomPDF table can blow memory_limit and produce a truncated or empty file rather than a clean exception. Chunk very large documents into multiple PDFs and merge, or move to an async job with explicit set_time_limit and a raised memory ceiling. Test with your worst realistic dataset — the 5-row fixture will not tell you anything.
Extension point for anything custom: write your own driver class implementing the package's driver contract and register it in config/pdf-converter.php. That's the clean seam if you need per-document options the shipped drivers don't expose. It is also, given the maintenance status, likely the seam you'll use to migrate off the package later — keep custom drivers thin and delegating to the underlying renderer rather than absorbing logic.
How can I help you explore Laravel packages today?