PDF generation is a rendering and operations decision, not a package popularity contest.
| Need | Default | Trade-off |
|---|---|---|
| Simple invoices and tables | Dompdf | Limited modern CSS |
| Browser-quality HTML/CSS | spatie/laravel-pdf + Browsershot | Node and Chrome runtime |
| Shared Chrome or Office conversion | Gotenberg | Operated 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
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
| Requirement | Choose | Why |
|---|---|---|
| Plain invoice and tables | Dompdf | PHP-only and smallest deployment footprint |
| Modern CSS, charts, browser layout | spatie/laravel-pdf + Browsershot | Chromium renders the web platform |
| Shared renderer or office conversion | Gotenberg | Central 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
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
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
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.