Choosing a PDF Generator for Laravel

3 min readUpdated on

PDF generation is a rendering and operations decision, not a package popularity contest.

NeedDefaultTrade-off
Simple invoices and tablesDompdfLimited modern CSS
Browser-quality HTML/CSSspatie/laravel-pdf + BrowsershotNode and Chrome runtime
Shared Chrome or Office conversionGotenbergOperated HTTP service

Dompdf is the zero-extra-service option for simple documents. For product layouts, spatie/laravel-pdf is the modern Laravel default when Chrome is available. Gotenberg moves Chromium or LibreOffice conversion behind a private HTTP boundary, useful for multiple applications and office formats.

php
<?php

declare(strict_types=1);

use Spatie\LaravelPdf\Facades\Pdf;

return Pdf::view('pdf.invoice', ['invoice' => $invoice])
    ->format('a4')
    ->name("invoice-{$invoice->number}.pdf")
    ->download();

Render trusted templates with trusted data, not arbitrary HTML. Allowlist external hosts and block private ranges if a renderer fetches URLs; otherwise PDF rendering becomes an SSRF primitive. Queue expensive output, store it privately, authorise download and record duration, template version and failures. A visual fixture test catches layout regressions that a 200 response cannot.

Choose from fidelity, operations and volume

RequirementChooseWhy
Plain invoice and tablesDompdfPHP-only and smallest deployment footprint
Modern CSS, charts, browser layoutspatie/laravel-pdf + BrowsershotChromium renders the web platform
Shared renderer or office conversionGotenbergCentral private HTTP boundary

Dompdf is a valid zero-extra-service choice, but it does not become Chromium by adding CSS workarounds. Browsershot needs a maintained Chrome runtime; Gotenberg needs health checks, private networking and capacity planning.

Three complete rendering paths

php
<?php

declare(strict_types=1);

namespace App\\Http\\Controllers;

use App\\Models\\Invoice;
use Barryvdh\\DomPDF\\Facade\\Pdf;
use Illuminate\\Http\\Response;

final class DownloadInvoiceController
{
    public function __invoke(Invoice $invoice): Response
    {
        $this->authorize('view', $invoice);

        return Pdf::loadView('pdf.invoices.show', ['invoice' => $invoice])
            ->setPaper('a4')
            ->download("invoice-{$invoice->number}.pdf");
    }
}
php
<?php

declare(strict_types=1);

use Spatie\\LaravelPdf\\Facades\\Pdf;

return Pdf::view('pdf.reports.monthly', ['report' => $report])
    ->format('a4')->margins(12, 12, 16, 12)->name("report-{$report->period}.pdf")->download();
php
<?php

declare(strict_types=1);

use Illuminate\\Support\\Facades\\Http;

$response = Http::connectTimeout(2)->timeout(30)
    ->attach('files', view('pdf.reports.monthly', ['report' => $report])->render(), 'report.html')
    ->post('http://gotenberg:3000/forms/chromium/convert/html');

$response->throw();

Queue batch generation, cap concurrency to renderer capacity and store output privately after authorization. Keep a versioned visual fixture: a 200 and a non-empty byte string cannot prove that a table did not split across pages.

When not to use it

Do not run headless Chrome synchronously on checkout. Do not add Gotenberg for one plain invoice if Dompdf is enough, and do not fight Dompdf for days when browser CSS is a requirement. Choose the smallest system meeting fidelity, throughput and recovery needs.

Related articles

Existing system support

Need help with a live application?

I help companies improve live systems, clean up delivery workflows, and ship new features without adding avoidable complexity.

Comments (0)
Sign in to leave a comment

You need to be signed in to add a comment.

Login

Need someone to take responsibility for the next step?

Let’s talk about your project and define a scope that actually makes sense for your goals.