Muhammad Yasir Amin

Loading websiteโ€ฆ

More from Muhammad Yasir Amin

Build Along With Me

Practical walkthroughs on Laravel, React, APIs and deployment workflows.

Subscribe on YouTube

How to Generate PDF Files in Laravel 13 with DomPDF

๐Ÿ› ๏ธ Laravel Tutorials5 min read

How to Generate PDF Files in Laravel 13 with DomPDF

Muhammad Yasir Amin

Muhammad Yasir Amin

Senior Full Stack Engineerโ€ขNov 12, 2026
How to Generate PDF Files in Laravel 13 with DomPDF

To generate a PDF in Laravel 13, install barryvdh/laravel-dompdf with Composer, render a Blade view with Pdf::loadView('pdf.invoice', $data), and return ->download('invoice.pdf') to send it to the browser. You can also ->stream() it inline or ->output() the raw bytes and save them to a storage disk.

DomPDF converts HTML and CSS into PDF using pure PHP, with no external binary. That makes it easy to run on shared hosting, Docker and queue workers. The trade-off is limited CSS support, so I keep PDF templates simple and table-based.

Prerequisites

  • Laravel 13.x on PHP 8.3 or newer.
  • barryvdh/laravel-dompdf 3.x, which supports Laravel 9 through 13 and uses dompdf/dompdf 3.x.
  • The PHP dom and mbstring extensions (enabled on most installs). gd is needed for most image formats.

Step 1: Install the package

Code
composer require barryvdh/laravel-dompdf

The service provider and the Pdf facade are auto-discovered. To change defaults such as paper size or font directory, publish the config file:

Code
php artisan vendor:publish --provider="Barryvdh\DomPDF\ServiceProvider"

This creates config/dompdf.php.

Step 2: Create a Blade template for the PDF

Create resources/views/pdf/invoice.blade.php. Put the CSS in a style tag in the head, and include the UTF-8 meta tag so accented characters render correctly:

HTML / XML
<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <style>
        body { font-family: "DejaVu Sans", sans-serif; font-size: 12px; color: #222; }
        h1 { font-size: 20px; margin-bottom: 4px; }
        table { width: 100%; border-collapse: collapse; margin-top: 16px; }
        th, td { border-bottom: 1px solid #ddd; padding: 6px; text-align: left; }
        .right { text-align: right; }
    </style>
</head>
<body>
    <h1>Invoice #{{ $invoice->number }}</h1>
    <p>Billed to: {{ $invoice->customer_name }}<br>Date: {{ $invoice->issued_at->format('d M Y') }}</p>

    <table>
        <thead>
            <tr><th>Item</th><th class="right">Qty</th><th class="right">Price</th></tr>
        </thead>
        <tbody>
            @foreach ($invoice->items as $item)
                <tr>
                    <td>{{ $item->description }}</td>
                    <td class="right">{{ $item->quantity }}</td>
                    <td class="right">{{ number_format($item->price, 2) }}</td>
                </tr>
            @endforeach
        </tbody>
    </table>

    <p class="right"><strong>Total: {{ number_format($invoice->total, 2) }}</strong></p>
</body>
</html>

DejaVu Sans ships with DomPDF and covers far more characters than the core PDF fonts like Helvetica.

Step 3: Generate and download the PDF

JavaScript
<?php

namespace App\Http\Controllers;

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

class InvoicePdfController extends Controller
{
    public function download(Invoice $invoice): Response
    {
        $invoice->load('items');

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

    public function show(Invoice $invoice): Response
    {
        $invoice->load('items');

        // Opens the PDF in the browser tab instead of downloading it.
        return Pdf::loadView('pdf.invoice', ['invoice' => $invoice])
            ->stream("invoice-{$invoice->number}.pdf");
    }
}

Register the routes in routes/web.php, behind authentication and authorization since invoices are private:

CSS
Route::middleware('auth')->group(function () {
    Route::get('/invoices/{invoice}/pdf', [InvoicePdfController::class, 'download'])
        ->name('invoices.pdf')
        ->can('view', 'invoice');
});

How do I save the PDF to storage instead of downloading it?

Use output() to get the PDF as a string and store it on any filesystem disk. This also works in queued jobs, for example to attach the file to an email later:

JavaScript
use Illuminate\Support\Facades\Storage;

$pdf = Pdf::loadView('pdf.invoice', ['invoice' => $invoice]);

Storage::disk('local')->put("invoices/{$invoice->number}.pdf", $pdf->output());

How do I change paper size, orientation and options?

setPaper() accepts sizes such as a4, letter or legal and an orientation of portrait or landscape. Other DomPDF options can be set per document:

JavaScript
Pdf::setOption(['dpi' => 150, 'defaultFont' => 'DejaVu Sans'])
    ->loadView('pdf.report', $data)
    ->setPaper('a4', 'landscape');

Remote resources (images or CSS loaded over HTTP) are disabled by default for security, through the isRemoteEnabled option. Rather than enabling it, reference local files with an absolute path such as public_path('images/logo.png').

Where should PDF generation live in a larger app?

Controllers stay cleaner when the PDF logic sits in a small service class, for example an InvoicePdf class with a render(Invoice $invoice): string method that returns $pdf->output(). The controller then only decides whether to download, stream or store the result, and a queued job can reuse the same class to build monthly statements overnight. Keep the Blade templates in their own resources/views/pdf folder and do not reuse your normal web layout: the web layout loads Vite assets and JavaScript that DomPDF cannot use, and the print layout usually needs different spacing and fonts anyway. Pass fully prepared data into the view so the template only formats values and never runs queries.

Common mistakes

  • Using modern CSS. Flexbox and grid are not supported. Use tables, floats and simple block layout.
  • Linking assets with URLs. With remote access off, asset() URLs do not load. Use filesystem paths.
  • Squares instead of characters. Core fonts lack many glyphs. Switch to DejaVu Sans or embed your own font.
  • Huge reports in a web request. Large PDFs take time and memory. Generate them in a queued job and notify the user when ready.
  • N+1 queries in the template. Eager load relations, as in $invoice->load('items'), before rendering.

How to verify it works

Open the route in the browser and check the file. For an automated check, assert on the response headers in a Pest test:

PHP
<?php

use App\Models\Invoice;

it('downloads an invoice as a pdf', function () {
    $invoice = Invoice::factory()->hasItems(2)->create();

    $this->actingAs($invoice->user)
        ->get(route('invoices.pdf', $invoice))
        ->assertOk()
        ->assertHeader('content-type', 'application/pdf')
        ->assertDownload("invoice-{$invoice->number}.pdf");
});

The factory relationship and the policy are assumptions about your app; adjust them to your models.

FAQ

Is DomPDF good enough for complex layouts?

For invoices, receipts, certificates and simple reports, yes. For pixel-perfect output with modern CSS, a headless-browser based tool is a better fit.

Can I add page numbers?

Yes. Enable PHP in the DomPDF options and use its page script API, or add a fixed-position footer in CSS. Only enable PHP execution for templates you fully control.

Can I email the generated PDF?

Yes. Attach the stored file to a Mailable with Attachment::fromStorage(), or attach the raw output with Attachment::fromData().

References

Article Index & Topic Tags
LaravelBladePdfDompdf

YouTube Channel

Build Along With Me

Practical walkthroughs on Laravel, React, APIs and deployment workflows.

Muhammad Yasir Amin
Available for Contracts
Author & Solutions Architect

Senior Full Stack Engineer

Independent Senior Full Stack Engineer working on Laravel, React and Next.js applications, APIs and delivery workflows.

Laravel & PHP ApplicationsReact & Next.js InterfacesREST APIs & IntegrationsDatabase Queries & DebuggingDocker Development WorkflowsClear Scope & Handover
Discuss Your Next Step

What Does Your Application Need Next?

Share your current system, the problem you want to solve and what a useful result looks like. We can identify the right starting point and agree on a practical scope.

Technical Audit

Find the Next Useful Fix

Application Development

Scoped, Reviewable Milestones

Ongoing Support

Agreed Capacity and Priorities

Share your goals, current stack and requirements before the call.