Email a generated PDF as a mailer attachment
At a glance
Section titled “At a glance”Emailing an invoice, receipt, or report is one of the most common things you do with a generated PDF. The clean way to do it is to build the document, take its raw bytes, and hand those bytes straight to your mailer’s attachment API. You do not need a temporary file on disk at any point.
This is a how-to. It assumes you already know how to send mail in your
framework. The NextPDF side is a single call:
Document::getPdfData() returns the raw Portable Document Format (PDF) bytes as
a string. The attachment side belongs entirely to your mailer — this guide uses
Laravel’s Attachment::fromData() and Symfony Mailer’s Email::attach().
NextPDF does not ship a mail helper. There is no “email this PDF” method on a document, and you should be suspicious of any example that shows one. The attachment API is always your framework’s.
This page is the outbound counterpart to embedding files inside a PDF. That guide attaches files into the PDF as embedded streams; this guide attaches the finished PDF to an email. They are different operations — do not confuse the two.
Get the raw PDF bytes
Section titled “Get the raw PDF bytes”Whatever else you do, the NextPDF step is the same: produce the bytes.
<?php
declare(strict_types=1);
use NextPDF\Core\Document;
// Standalone entrypoint: the static factory wires the default dependencies.// (The bare `new Document(...)` constructor requires injected collaborators.)$document = Document::createStandalone();$document->addPage();$document->cell(0, 10, 'Invoice #1042', newLine: true);
// Raw PDF bytes, built in memory. No file is written.$bytes = $document->getPdfData();getPdfData() builds the document and returns its bytes as a string. It writes
nothing to disk and sends no Hypertext Transfer Protocol (HTTP) headers, which is
exactly what you want for an attachment.
If you hold the document only through the NextPDF\Contracts\PdfDocumentInterface
type (for example, a value the framework integration handed you), use the
contract-level equivalent instead:
use NextPDF\Contracts\OutputDestination;
$bytes = $document->output(dest: OutputDestination::String);output(dest: OutputDestination::String) is declared on
PdfDocumentInterface and returns the same raw bytes without emitting any
headers. Use getPdfData() when you hold a concrete NextPDF\Core\Document, and
the output(...) form when you only have the interface.
If you use the Laravel or Symfony integration, resolve a fresh document from the container rather than constructing one directly — see Return a generated PDF from a controller for the resolution path in each framework. Everything below works the same regardless of how you obtained the document.
Laravel: attach in-memory bytes to a Mailable
Section titled “Laravel: attach in-memory bytes to a Mailable”Laravel’s Attachment::fromData() takes a callback that returns the raw bytes,
plus a filename. There is no temporary file. Implement attachments() on your
Mailable and return one Attachment.
<?php
declare(strict_types=1);
namespace App\Mail;
use App\Models\Invoice;use Illuminate\Mail\Mailable;use Illuminate\Mail\Mailables\Attachment;use Illuminate\Mail\Mailables\Content;use Illuminate\Mail\Mailables\Envelope;use NextPDF\Core\Document;
final class InvoiceMail extends Mailable{ public function __construct(private readonly Invoice $invoice) {}
public function envelope(): Envelope { return new Envelope(subject: "Invoice #{$this->invoice->number}"); }
public function content(): Content { return new Content(markdown: 'mail.invoice'); }
/** @return array<int, Attachment> */ public function attachments(): array { return [ Attachment::fromData(fn (): string => $this->buildPdf(), "invoice-{$this->invoice->number}.pdf") ->withMime('application/pdf'), ]; }
private function buildPdf(): string { // Standalone document: the static factory wires the default // dependencies, so this example is self-contained. If you use the // nextpdf/laravel integration, resolve a document via its documented // binding instead — see the integration page linked below. $document = Document::createStandalone(); $document->addPage(); $document->cell(0, 10, "Invoice #{$this->invoice->number}", newLine: true);
return $document->getPdfData(); }}Send it as usual:
use App\Mail\InvoiceMail;use Illuminate\Support\Facades\Mail;
Mail::to($invoice->customerEmail)->send(new InvoiceMail($invoice));The fromData() callback is invoked lazily when the message is built, so the
PDF is generated at send time, not at construction. Set ->withMime('application/pdf')
so the recipient’s client treats the part as a PDF rather than guessing from the
extension. Inside a controller you can instead call $message->attachData($bytes, $name, ['mime' => 'application/pdf'])
on a raw message, but Attachment::fromData() on the Mailable is the idiomatic
modern form.
Symfony Mailer: attach bytes to an Email
Section titled “Symfony Mailer: attach bytes to an Email”Symfony Mailer’s Email::attach() accepts the body as an in-memory string, with
an explicit filename and content type. Again, no temporary file.
<?php
declare(strict_types=1);
namespace App\Mailer;
use NextPDF\Core\DocumentFactory;use Symfony\Component\Mailer\MailerInterface;use Symfony\Component\Mime\Email;
final class InvoiceMailer{ public function __construct( private readonly MailerInterface $mailer, private readonly DocumentFactory $documents, ) {}
public function sendInvoice(string $to, int $invoiceId): void { // Build a fresh document from the factory, which wires the default // dependencies for you. (Use Document::createStandalone() if you do // not have the factory injected.) $document = $this->documents->create(); $document->addPage(); $document->cell(0, 10, "Invoice #{$invoiceId}", newLine: true);
$email = (new Email()) ->from('billing@example.com') ->to($to) ->subject("Invoice #{$invoiceId}") ->text('Your invoice is attached.') ->attach( $document->getPdfData(), "invoice-{$invoiceId}.pdf", 'application/pdf', );
$this->mailer->send($email); }}attach(string $body, ?string $name, ?string $contentType) takes the bytes
directly. Pass 'application/pdf' as the third argument so the part is typed
correctly. If you prefer to attach from a stream, attachFromPath() exists, but
for generated content the in-memory attach() form avoids a needless round trip
to disk.
Do it from a queued job
Section titled “Do it from a queued job”Building a multi-page PDF and sending mail are both slow enough that you should not do them on the request thread. Queue the work. The pattern is to dispatch a job (or, in Laravel, queue the Mailable itself) and build the PDF on the worker.
The simplest Laravel form: make the Mailable ShouldQueue. Because the
Attachment::fromData() callback runs when the queued message is built, the PDF
is generated on the worker, not at dispatch.
use Illuminate\Contracts\Queue\ShouldQueue;use Illuminate\Mail\Mailable;
final class InvoiceMail extends Mailable implements ShouldQueue{ // ... same envelope(), content(), attachments() as above ...}// Dispatched to the queue; the worker builds the PDF and sends the mail.Mail::to($invoice->customerEmail)->queue(new InvoiceMail($invoice));For Symfony, dispatch a Messenger message carrying the identifiers (not the
bytes), and let the handler build the PDF and send the email on the worker. Pass
an invoice id, look the record up in the handler, and call the InvoiceMailer
shown above. Symfony’s mailer is already asynchronous when a Messenger transport
is configured for SendEmailMessage, so even a synchronously-called send()
can be transported to a worker.
If you generate the PDF in a dedicated generation job and then mail it, see
Generate a PDF in a queued job
for the integration’s GeneratePdfJob / GeneratePdfMessage surface and its
worker-safety rules. A common shape is: one job generates and saves the PDF, a
second job (or the success callback) reads it back and mails it. When you keep
the bytes in memory across a single job, you skip the file entirely.
Size, inline, and attachment notes
Section titled “Size, inline, and attachment notes”- Attachment, not inline. A generated invoice or report is almost always a
separate downloadable file, so attach it. Reserve inline content
(
Content-Disposition: inlinewith acid:reference) for images you embed in the HTML body — a PDF is not body content. - Watch the size. Email attachments are base64-encoded in transit, which inflates the payload by roughly a third. Many receiving servers cap a message at around 10–25 MB after encoding. For a large report, attach a short notification email with a signed download link instead of the file itself, and serve the PDF over HTTP — see Return a generated PDF from a controller.
- Build once, attach once. Capture the returned bytes once into a variable and reuse that string for the message. Do not call the final output method repeatedly for one email.
- Memory on the worker. Holding the full PDF in memory is fine for typical
invoices and receipts. For very large documents on a constrained worker, save
to a temporary path with
save(), attach via Laravelattach($path)/ SymfonyEmail::attachFromPath($path), then delete the file — trading a disk round trip for a lower memory peak. (For the in-memory path used everywhere else on this page, the byte-based form is LaravelAttachment::fromData()/ SymfonyEmail::attach($bytes, 'name.pdf', 'application/pdf').)
Security notes
Section titled “Security notes”- Never interpolate unvalidated user input into the attachment filename. Pass a value you control (an invoice number you generated), so the recipient’s client cannot be steered by a crafted name.
- Send each customer only their own document. Build the PDF from the authenticated subject’s records inside the job, not from an id taken on faith from the request.
- In a queued path, log the exception class and a correlation id on failure,
never the exception message or a stack trace. Never write an empty
catchblock around the build-and-send.
See also
Section titled “See also”- Embed files and create PDF portfolios — the inverse: attach files into the PDF.
- Generate a PDF in a queued job — move generation off the request thread.
- Return a generated PDF from a controller — serve the PDF over HTTP instead of attaching it.
- Laravel production usage — resolving a document from the container and worker safety.
- Symfony production usage — Messenger workers and the document factory.