Salta ai contenuti
getnextpdf.com

Inviare via email un PDF generato come allegato del mailer

Inviare via email una fattura, una ricevuta o un report è una delle cose più comuni che si fanno con un PDF generato. Il modo pulito di farlo è costruire il documento, prenderne i byte grezzi e consegnare quei byte direttamente all’API di allegato del proprio mailer. Non serve un file temporaneo su disco in alcun momento.

Questa è una guida pratica. Presuppone che si sappia già inviare email nel proprio framework. Il lato NextPDF è una sola chiamata: Document::getPdfData() restituisce i byte grezzi Portable Document Format (PDF) come stringa. Il lato allegato appartiene interamente al proprio mailer — questa guida usa Attachment::fromData() di Laravel ed Email::attach() di Symfony Mailer.

NextPDF non include un helper di posta. Non c’è alcun metodo «invia questo PDF via email» su un documento, e si dovrebbe diffidare di qualsiasi esempio che ne mostri uno. L’API di allegato è sempre quella del proprio framework.

Questa pagina è la controparte in uscita di incorporare file all’interno di un PDF. Quella guida allega file dentro il PDF come stream incorporati; questa guida allega il PDF finito a un’email. Sono operazioni diverse — non confondere le due.

Qualunque altra cosa si faccia, il passaggio NextPDF è lo stesso: produrre i byte.

<?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() costruisce il documento e ne restituisce i byte come stringa. Non scrive nulla su disco e non invia alcun header Hypertext Transfer Protocol (HTTP), che è esattamente ciò che si vuole per un allegato.

Se si detiene il documento solo tramite il tipo NextPDF\Contracts\PdfDocumentInterface (per esempio, un valore che l’integrazione del framework vi ha consegnato), usare invece l’equivalente a livello di contratto:

use NextPDF\Contracts\OutputDestination;
$bytes = $document->output(dest: OutputDestination::String);

output(dest: OutputDestination::String) è dichiarato su PdfDocumentInterface e restituisce gli stessi byte grezzi senza emettere alcun header. Usare getPdfData() quando si detiene un NextPDF\Core\Document concreto, e la forma output(...) quando si ha solo l’interfaccia.

Se si usa l’integrazione Laravel o Symfony, risolvere un documento nuovo dal container anziché costruirne uno direttamente — vedere Restituire un PDF generato da un controller per il percorso di risoluzione in ciascun framework. Tutto ciò che segue funziona allo stesso modo indipendentemente da come si è ottenuto il documento.

Attachment::fromData() di Laravel accetta una callback che restituisce i byte grezzi, più un nome file. Non c’è alcun file temporaneo. Implementare attachments() sul proprio Mailable e restituire un Attachment.

app/Mail/InvoiceMail.php
<?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();
}
}

Inviarla come al solito:

use App\Mail\InvoiceMail;
use Illuminate\Support\Facades\Mail;
Mail::to($invoice->customerEmail)->send(new InvoiceMail($invoice));

La callback fromData() è invocata in modo lazy quando il messaggio viene costruito, quindi il PDF è generato al momento dell’invio, non alla costruzione. Impostare ->withMime('application/pdf') così che il client del destinatario tratti la parte come un PDF anziché indovinare dall’estensione. All’interno di un controller si può invece chiamare $message->attachData($bytes, $name, ['mime' => 'application/pdf']) su un messaggio grezzo, ma Attachment::fromData() sul Mailable è la forma moderna idiomatica.

Email::attach() di Symfony Mailer accetta il corpo come stringa in memoria, con un nome file e un content type espliciti. Di nuovo, nessun file temporaneo.

src/Mailer/InvoiceMailer.php
<?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) accetta i byte direttamente. Passare 'application/pdf' come terzo argomento così che la parte sia tipizzata correttamente. Se si preferisce allegare da uno stream esiste attachFromPath(), ma per il contenuto generato la forma in memoria attach() evita un inutile passaggio dal disco.

Costruire un PDF di più pagine e inviare posta sono entrambi abbastanza lenti da non doverli fare sul thread della richiesta. Mettere il lavoro in coda. Il pattern consiste nel dispatchare un job (o, in Laravel, mettere in coda il Mailable stesso) e costruire il PDF sul worker.

La forma Laravel più semplice: rendere il Mailable ShouldQueue. Poiché la callback Attachment::fromData() viene eseguita quando il messaggio in coda viene costruito, il PDF è generato sul worker, non al dispatch.

app/Mail/InvoiceMail.php (queued)
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));

Per Symfony, dispatchare un messaggio Messenger che porta gli identificatori (non i byte), e lasciare che l’handler costruisca il PDF e invii l’email sul worker. Passare un id di fattura, cercare il record nell’handler e chiamare l’InvoiceMailer mostrato sopra. Il mailer di Symfony è già asincrono quando un transport Messenger è configurato per SendEmailMessage, quindi anche un send() chiamato in modo sincrono può essere trasportato a un worker.

Se si genera il PDF in un job di generazione dedicato e poi lo si invia per email, vedere Generare un PDF in un job in coda per la superficie GeneratePdfJob / GeneratePdfMessage dell’integrazione e le sue regole di sicurezza del worker. Una forma comune è: un job genera e salva il PDF, un secondo job (o la callback di successo) lo rilegge e lo invia per email. Quando si tengono i byte in memoria attraverso un singolo job, si salta del tutto il file.

  • Allegato, non inline. Una fattura o un report generato è quasi sempre un file scaricabile separato, quindi allegarlo. Riservare il contenuto inline (Content-Disposition: inline con un riferimento cid:) per le immagini che si incorporano nel corpo HTML — un PDF non è contenuto del corpo.
  • Attenzione alla dimensione. Gli allegati email sono codificati in base64 durante il transito, il che gonfia il payload di circa un terzo. Molti server riceventi limitano un messaggio a circa 10–25 MB dopo la codifica. Per un report di grandi dimensioni, allegare una breve email di notifica con un link di download firmato anziché il file stesso, e servire il PDF su HTTP — vedere Restituire un PDF generato da un controller.
  • Costruire una volta, allegare una volta. Catturare i byte restituiti una volta in una variabile e riutilizzare quella stringa per il messaggio. Non chiamare ripetutamente il metodo di output finale per una sola email.
  • Memoria sul worker. Tenere l’intero PDF in memoria va bene per fatture e ricevute tipiche. Per documenti molto grandi su un worker con risorse limitate, salvare su un percorso temporaneo con save(), allegare tramite attach($path) di Laravel / Email::attachFromPath($path) di Symfony, poi eliminare il file — scambiando un passaggio dal disco per un picco di memoria più basso. (Per il percorso in memoria usato ovunque nel resto di questa pagina, la forma basata sui byte è Attachment::fromData() di Laravel / Email::attach($bytes, 'name.pdf', 'application/pdf') di Symfony.)
  • Non interpolare mai input utente non validato nel nome file dell’allegato. Passare un valore che si controlla (un numero di fattura che si è generato), così che il client del destinatario non possa essere pilotato da un nome appositamente creato.
  • Inviare a ogni cliente solo il proprio documento. Costruire il PDF dai record del soggetto autenticato all’interno del job, non da un id preso per buono dalla richiesta.
  • In un percorso in coda, registrare la classe dell’eccezione e un id di correlazione in caso di fallimento, mai il messaggio dell’eccezione o uno stack trace. Non scrivere mai un blocco catch vuoto attorno al build-and-send.