Laravel 13 Generate a PDF and Email It as an Attachment (DomPDF and Mailable)
To generate a PDF and email it in Laravel 13, render a Blade view to PDF with the DomPDF package, and return the PDF bytes from the attachments() method of a Mailable with Attachment::fromData(). The PDF is created in memory, so no temporary file is needed. This guide builds an invoice example from the view to the queued email.
Targets: Laravel 13 (PHP 8.3 to 8.5) and the barryvdh/laravel-dompdf package. Check the package's Packagist page for the version that supports your Laravel version.
Step 1: Install DomPDF
composer require barryvdh/laravel-dompdf The package registers itself automatically and gives you the Barryvdh\DomPDF\Facade\Pdf facade.
Step 2: Create the PDF view
Create resources/views/pdf/invoice.blade.php. DomPDF supports a limited set of CSS, so use simple tables and inline styles:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <style> body { font-family: DejaVu Sans, sans-serif; font-size: 13px; } table { width: 100%; border-collapse: collapse; } th, td { border: 1px solid #999; padding: 6px; text-align: left; } </style> </head> <body> <h1>Invoice #{{ $invoice->number }}</h1> <p>Customer: {{ $invoice->customer_name }}</p> <table> <thead><tr><th>Item</th><th>Amount</th></tr></thead> <tbody> @foreach ($invoice->items as $item) <tr><td>{{ $item['name'] }}</td><td>{{ number_format($item['amount'], 2) }}</td></tr> @endforeach </tbody> </table> </body> </html> The font DejaVu Sans ships with DomPDF and supports many characters. For other languages, such as Urdu or Arabic, you need a font that contains those glyphs.
Step 3: Create the Mailable
php artisan make:mail InvoiceMail <?php namespace App\Mail; use App\Models\Invoice; use Barryvdh\DomPDF\Facade\Pdf; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Mail\Mailable; use Illuminate\Mail\Mailables\Attachment; use Illuminate\Mail\Mailables\Content; use Illuminate\Mail\Mailables\Envelope; use Illuminate\Queue\SerializesModels; class InvoiceMail extends Mailable implements ShouldQueue { use Queueable, SerializesModels; public function __construct(public Invoice $invoice) {} public function envelope(): Envelope { return new Envelope(subject: 'Your invoice #'.$this->invoice->number); } public function content(): Content { return new Content(view: 'emails.invoice'); } /** * @return array<int, Attachment> */ public function attachments(): array { return [ Attachment::fromData( fn () => Pdf::loadView('pdf.invoice', ['invoice' => $this->invoice])->output(), 'invoice-'.$this->invoice->number.'.pdf' )->withMime('application/pdf'), ]; } } The PDF is built inside the closure when the message is actually sent. Because only the Invoice model (its ID) is serialized, the queue payload stays small. If you generated the PDF first and passed its bytes to the Mailable, the whole PDF would be stored in the queue job, which can be large.
Create the short email view resources/views/emails/invoice.blade.php:
<p>Hello {{ $invoice->customer_name }},</p> <p>Your invoice is attached to this email.</p> Public properties of the Mailable, like $invoice, are available in its view automatically.
Step 4: Send the email
use App\Mail\InvoiceMail; use Illuminate\Support\Facades\Mail; Mail::to($invoice->customer_email)->send(new InvoiceMail($invoice)); Because the Mailable implements ShouldQueue, Laravel queues it. Make sure a worker is running (php artisan queue:work) and your mail settings in .env are correct. To see the result without sending real mail, set MAIL_MAILER=log.
How do I also let the user download the PDF?
use Barryvdh\DomPDF\Facade\Pdf; public function download(Invoice $invoice) { abort_unless(auth()->id() === $invoice->user_id, 403); return Pdf::loadView('pdf.invoice', ['invoice' => $invoice]) ->download('invoice-'.$invoice->number.'.pdf'); } The same view is used for the email attachment and the download, so both always match. The authorization check makes sure users can only download their own invoices.
How do I test it?
use App\Mail\InvoiceMail; use Illuminate\Support\Facades\Mail; Mail::fake(); // ... trigger the code that sends the invoice ... Mail::assertQueued(InvoiceMail::class, fn ($mail) => $mail->hasTo('customer@example.com')); To check the attachment itself, send with MAIL_MAILER=log and open the log, or use a tool such as Mailpit that catches mail locally and shows attachments.
Common mistakes
- Complex CSS in the PDF view: DomPDF does not support all modern CSS such as flexbox and grid. Use tables.
- Passing PDF bytes into a queued Mailable: this creates huge queue payloads. Build the PDF in
attachments(). - Missing mime type: add
withMime('application/pdf')so mail clients show the file correctly. - Special characters showing as question marks: use a font with the needed glyphs, such as DejaVu Sans.
- No queue worker: queued emails never leave until a worker runs.
FAQ
Can I use a different PDF library?
Yes. Any library that returns PDF bytes works with Attachment::fromData().
How do I attach a PDF that already exists on disk?
Use Attachment::fromStorageDisk('local', 'path/invoice.pdf') or Attachment::fromPath($path).
Why is my PDF blank?
The view probably has an error or an unsupported CSS rule. Render the view in a browser first with a test route to find the problem.
References
Originally published on yasiramin.dev.