Your client just pinged you. They want downloadable invoices by end of the week. You open a new tab, search “Laravel PDF generation,” and suddenly you’re reading about wkhtmltopdf needing a binary installed on the server, or some paid SaaS, or a 400-line custom solution someone posted in 2017. Frustrating, right?
Here’s the good news — there’s a package that makes Laravel PDF generation genuinely simple. It’s called barryvdh/laravel-dompdf, and it lets you turn any Blade view into a downloadable PDF in about five lines of code. No headless browsers, no system dependencies, no nonsense.
By the end of this article, you’ll know how to install it, convert a Blade template to PDF, handle download vs. browser streaming, and build a real invoice PDF from scratch. Let’s get into it.
Why Laravel PDF Generation Is Something Every Real App Needs
I’ve worked on a lot of Laravel projects — SaaS apps, e-commerce platforms, internal dashboards — and almost every single one eventually needed PDFs. It starts with one feature request. “Can we add an invoice download?” Then comes the report export. Then the user data export. Then the confirmation letter. It snowballs.
The problem is that generating PDFs isn’t something PHP does natively, and building your own HTML-to-PDF pipeline is a rabbit hole. You either mess around with server-level tools like wkhtmltopdf (which requires the binary to be installed and often breaks in cloud deployments), or you end up doing some hacky print-to-PDF thing that never looks right.
What you actually want is something that takes your existing Blade template — the HTML and CSS you already know how to write — and just produces a PDF. That’s exactly what barryvdh/laravel-dompdf does.
What Is barryvdh/laravel-dompdf?
It’s a Laravel wrapper around the dompdf/dompdf library. dompdf itself is a PHP library that converts HTML and CSS into PDF documents. The barryvdh package wraps it up nicely for Laravel — giving you a clean Facade, auto-discovery, config publishing, and a simple API that fits right into how you already write controllers.
The package supports PHP 8.1 and above, works with Laravel 9, 10, and 11, and is licensed under MIT. As of the latest v3.x release, it uses dompdf v3.x under the hood, which brought some important security improvements (more on that later).
The GitHub repo is here: https://github.com/barryvdh/laravel-dompdf — check it out for the changelog and latest release notes.
Step-by-Step: Installing and Setting Up Laravel PDF Generation with barryvdh/laravel-dompdf
Step 1 — Install the Package
Open your terminal inside your Laravel project and run:
composer require barryvdh/laravel-dompdfThat’s it. Composer pulls in the package along with the dompdf library and the font library. Laravel’s package auto-discovery takes care of registering the service provider and facade — you don’t need to touch config/app.php manually if you’re on Laravel 9 or later.
Step 2 — Publish the Config (Optional but Recommended)
You don’t have to do this, but I’d suggest it. Publishing the config gives you control over things like paper size, DPI, and font defaults:
php artisan vendor:publish --provider="Barryvdh\DomPDF\ServiceProvider"This creates a config/dompdf.php file in your project. Open it up and you’ll see settings like:
'options' => [
'font_dir' => storage_path('fonts/'),
'font_cache' => storage_path('fonts/'),
'temp_dir' => sys_get_temp_dir(),
'chroot' => realpath(base_path()),
'allowed_protocols' => [
'data://' => ['rules' => []],
'file://' => ['rules' => []],
'http://' => ['rules' => []],
'https://' => ['rules' => []],
],
'enable_remote' => false,
'default_media_type' => 'screen',
'default_paper_size' => 'a4',
'default_font' => 'serif',
'dpi' => 96,
'enable_php' => false,
'enable_css_float' => false,
'enable_html5_parser' => true,
],Fair warning: Notice enable_remote is set to false by default in v3.x. This is intentional — it’s a security decision. If your PDF needs to load external images or CSS files from a URL, you’ll need to enable it, but think twice before doing that in production. For most use cases, inline styles or base64-encoded images are the safer route.
Converting a Blade View to PDF
Here’s the core concept: you write a normal Blade template — standard HTML with inline CSS — and then use the Pdf facade to load it and return it as a PDF. That’s it. Your Blade skills transfer directly.
Here’s the simplest working example:
use Barryvdh\DomPDF\Facade\Pdf;
public function downloadReport()
{
$data = [
'name' => 'John Doe',
'date' => now()->format('d M Y'),
];
$pdf = Pdf::loadView('pdf.report', $data);
return $pdf->download('report.pdf');
}And the Blade view at resources/views/pdf/report.blade.php:
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: sans-serif;
padding: 20px;
color: #333;
}
h1 {
color: #2563eb;
margin-bottom: 10px;
}
p {
font-size: 14px;
line-height: 1.6;
}
</style>
</head>
<body>
<h1>User Report</h1>
<p><strong>Name:</strong> {{ $name }}</p>
<p><strong>Date:</strong> {{ $date }}</p>
</body>
</html>One thing to keep in mind: dompdf supports CSS 2.1 with some CSS3 properties. Flexbox and CSS Grid won’t work here. If you need complex layouts inside your PDF, use HTML tables for structure — the same way email templates are built. Styles should go inside a <style> block in the Blade file, not in a separate stylesheet linked via URL (unless you’ve enabled remote loading).
Download vs. Stream — What’s the Difference?
Two methods, two different behaviors.
->download('file.pdf') triggers a file download in the browser — the user sees a “Save As” dialog or the file goes straight to their downloads folder. Use this for invoices, receipts, reports — anything the user is supposed to save.
->stream('file.pdf') opens the PDF directly in the browser tab. The user sees it rendered in their PDF viewer right there in the browser without downloading it first. Useful for previews.
// Forces a file download
return $pdf->download('invoice.pdf');
// Opens directly in the browser
return $pdf->stream('invoice.pdf');In practice: for invoices and formal documents, go with download(). For preview features where the user wants to see the file before saving, use stream(). Both take the filename as the argument.
Real-World Example — Generate an Invoice PDF in Laravel
Alright, this is the part you actually came for. Let’s build a proper invoice PDF — the kind clients actually expect to receive.
The Controller
Create a new controller or add this method to an existing one:
namespace App\Http\Controllers;
use Barryvdh\DomPDF\Facade\Pdf;
use Illuminate\Http\Request;
class InvoiceController extends Controller
{
public function download($id)
{
// In a real app, you'd fetch this from the database
// For now, we're simulating the data
$invoice = [
'number' => 'INV-' . str_pad($id, 5, '0', STR_PAD_LEFT),
'date' => now()->format('d M Y'),
'due_date' => now()->addDays(15)->format('d M Y'),
'client' => [
'name' => 'Acme Corp',
'email' => 'billing@acme.com',
'address' => '123 Business Street, New York, USA',
],
'items' => [
[
'description' => 'Laravel Development',
'qty' => 10,
'rate' => 150,
'total' => 1500,
],
[
'description' => 'API Integration',
'qty' => 5,
'rate' => 200,
'total' => 1000,
],
[
'description' => 'Bug Fixes & Support',
'qty' => 3,
'rate' => 100,
'total' => 300,
],
],
'subtotal' => 2800,
'tax' => 280,
'total' => 3080,
];
$pdf = Pdf::loadView('pdf.invoice', compact('invoice'))
->setPaper('a4', 'portrait');
return $pdf->download("invoice-{$invoice['number']}.pdf");
}
}The Blade View
Create the file at resources/views/pdf/invoice.blade.php:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: sans-serif;
font-size: 13px;
color: #333;
padding: 40px;
}
.header {
width: 100%;
margin-bottom: 30px;
}
.header-left {
float: left;
}
.header-right {
float: right;
}
.clearfix::after {
content: "";
display: table;
clear: both;
}
.company-name {
font-size: 22px;
font-weight: bold;
color: #2563eb;
}
.company-details {
font-size: 12px;
color: #666;
margin-top: 4px;
line-height: 1.6;
}
.invoice-title {
font-size: 32px;
color: #ccc;
font-weight: bold;
letter-spacing: 2px;
}
.meta-table {
width: 100%;
margin-bottom: 30px;
border-collapse: collapse;
}
.meta-table td {
padding: 5px 0;
font-size: 13px;
vertical-align: top;
}
.meta-table td:first-child {
width: 50%;
}
.meta-table strong {
color: #555;
}
.section-label {
font-size: 11px;
text-transform: uppercase;
color: #999;
letter-spacing: 1px;
margin-bottom: 6px;
margin-top: 20px;
}
table.items {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
}
table.items thead tr {
background-color: #2563eb;
color: #ffffff;
}
table.items th {
padding: 10px 12px;
text-align: left;
font-size: 12px;
font-weight: bold;
}
table.items td {
padding: 10px 12px;
border-bottom: 1px solid #eee;
font-size: 13px;
}
table.items tbody tr:nth-child(even) {
background-color: #f9f9f9;
}
.totals-wrapper {
margin-top: 20px;
text-align: right;
}
table.totals {
display: inline-table;
border-collapse: collapse;
}
table.totals td {
padding: 5px 14px;
font-size: 13px;
}
table.totals .total-row td {
border-top: 2px solid #333;
font-weight: bold;
font-size: 15px;
padding-top: 8px;
}
.footer {
margin-top: 50px;
font-size: 11px;
color: #aaa;
text-align: center;
border-top: 1px solid #eee;
padding-top: 16px;
}
</style>
</head>
<body>
{{-- Header --}}
<div class="header clearfix">
<div class="header-left">
<div class="company-name">YourCompany Inc.</div>
<div class="company-details">
123 Dev Lane, San Francisco, CA 94107<br>
hello@yourcompany.com | +1 (415) 000-0000
</div>
</div>
<div class="header-right">
<div class="invoice-title">INVOICE</div>
</div>
</div>
{{-- Invoice Meta --}}
<table class="meta-table">
<tr>
<td>
<strong>Invoice #:</strong> {{ $invoice['number'] }}<br>
<strong>Date:</strong> {{ $invoice['date'] }}<br>
<strong>Due Date:</strong> {{ $invoice['due_date'] }}
</td>
<td>
<div class="section-label">Bill To</div>
<strong>{{ $invoice['client']['name'] }}</strong><br>
{{ $invoice['client']['email'] }}<br>
{{ $invoice['client']['address'] }}
</td>
</tr>
</table>
{{-- Line Items --}}
<table class="items">
<thead>
<tr>
<th>Description</th>
<th>Qty</th>
<th>Rate ($)</th>
<th>Total ($)</th>
</tr>
</thead>
<tbody>
@foreach($invoice['items'] as $item)
<tr>
<td>{{ $item['description'] }}</td>
<td>{{ $item['qty'] }}</td>
<td>{{ number_format($item['rate'], 2) }}</td>
<td>{{ number_format($item['total'], 2) }}</td>
</tr>
@endforeach
</tbody>
</table>
{{-- Totals --}}
<div class="totals-wrapper">
<table class="totals">
<tr>
<td>Subtotal:</td>
<td>${{ number_format($invoice['subtotal'], 2) }}</td>
</tr>
<tr>
<td>Tax (10%):</td>
<td>${{ number_format($invoice['tax'], 2) }}</td>
</tr>
<tr class="total-row">
<td>Total Due:</td>
<td>${{ number_format($invoice['total'], 2) }}</td>
</tr>
</table>
</div>
{{-- Footer --}}
<div class="footer">
Thank you for your business! Payment is due within 15 days of the invoice date.<br>
Please include the invoice number in your payment reference.
</div>
</body>
</html>The Route
Add this to your routes/web.php:
use App\Http\Controllers\InvoiceController;
Route::get('/invoice/{id}/download', [InvoiceController::class, 'download'])
->middleware('auth'); // protect it if neededNow hit /invoice/1/download in your browser and you’ll get a clean, properly structured invoice PDF downloaded immediately.
What just happened: the controller builds the invoice data array (in a real app, this would come from your Invoice and InvoiceItem models), passes it to the Blade view with compact(), sets the paper to A4 portrait using ->setPaper(), and then calls ->download() with a dynamic filename. The Blade view does the rest — it’s just HTML and CSS rendered by dompdf into a PDF.
A Few Tips and Gotchas Before You Ship
The biggest thing to understand is that dompdf isn’t a real browser engine. It implements CSS 2.1 with partial CSS3 support. That means flexbox doesn’t work, CSS Grid doesn’t work, and some of the modern layout properties you’re used to will just be silently ignored. The fix is straightforward: use HTML tables for your PDF layout structure, the same way developers used to build HTML emails. It feels old-fashioned but it gets the job done reliably.
Keep your styles inside a <style> block directly in the Blade file, not in a linked external stylesheet. If you absolutely need an external CSS file, you can link it using the asset() helper — but only if you’ve enabled remote loading in the config, which comes with security tradeoffs. For most invoices and reports, inline or embedded styles are the right call.
For images, your cleanest option is to base64-encode them and embed them directly in the HTML. This avoids the remote loading issue entirely. You can also reference images from the public folder using an absolute file path, which works without enabling remote:
<img src="{{ public_path('images/logo.png') }}">If you need better print quality, bump the DPI before generating:
$pdf = Pdf::setOption(['dpi' => 150, 'defaultFont' => 'sans-serif'])
->loadView('pdf.invoice', compact('invoice'));And for paper orientation — landscape works well for wide data tables, portrait for standard documents and invoices:
->setPaper('a4', 'landscape') // wide tables, reports
->setPaper('a4', 'portrait') // invoices, lettersOne last thing: enable_php in the config is false by default. Keep it that way. Enabling it lets PHP execute inside Blade views during PDF rendering, which is a real security risk if any user data ever ends up in those views.
That’s All You Need
You now have everything it takes to add PDF generation to any Laravel app — install the package, write your Blade view like you normally would, load it through the Pdf facade, and choose between download and stream. That’s the whole flow.
For a real project, you’d replace the hardcoded array in the controller with actual Eloquent model data. The Blade template structure stays exactly the same. Once you’ve got one PDF working, the rest are just variations on the same pattern — reports, receipts, confirmation letters, contracts.
The package is well-maintained, actively updated, and used in production by thousands of Laravel apps. Go check out the repo, read the release notes, and star it if it saves you time: https://github.com/barryvdh/laravel-dompdf.
If this walkthrough helped you, share it with someone on your team who’s about to tackle PDF generation for the first time. Save them the two hours of googling you just avoided


Leave a Reply