Enviar por correo un PDF generado como adjunto del mailer
De un vistazo
Sección titulada «De un vistazo»Enviar por correo una factura, un recibo o un informe es una de las cosas más comunes que se hacen con un PDF generado. La forma limpia de hacerlo es construir el documento, tomar sus bytes en bruto y entregar esos bytes directamente a la API de adjuntos de tu mailer. No necesitas un archivo temporal en disco en ningún momento.
Este es un tutorial práctico. Asume que ya sabes cómo enviar correo en tu
framework. El lado de NextPDF es una sola llamada:
Document::getPdfData() devuelve los bytes en bruto del formato de documento
portátil (PDF) como una cadena. El lado del adjunto pertenece por entero a tu
mailer: esta guía usa Attachment::fromData() de Laravel y Email::attach() de
Symfony Mailer.
NextPDF no incluye un ayudante de correo. No hay ningún método «envía este PDF» en un documento, y deberías sospechar de cualquier ejemplo que muestre uno. La API de adjuntos es siempre la de tu framework.
Esta página es la contraparte saliente de incrustar archivos dentro de un PDF. Aquella guía adjunta archivos dentro del PDF como flujos incrustados; esta guía adjunta el PDF terminado a un correo. Son operaciones distintas: no las confundas.
Obtén los bytes en bruto del PDF
Sección titulada «Obtén los bytes en bruto del PDF»Hagas lo que hagas, el paso de NextPDF es el mismo: producir los 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() construye el documento y devuelve sus bytes como una cadena. No
escribe nada en disco y no envía cabeceras del Protocolo de Transferencia de
Hipertexto (HTTP), que es exactamente lo que quieres para un adjunto.
Si tienes el documento solo a través del tipo
NextPDF\Contracts\PdfDocumentInterface (por ejemplo, un valor que te entregó la
integración de framework), usa en su lugar el equivalente a nivel de contrato:
use NextPDF\Contracts\OutputDestination;
$bytes = $document->output(dest: OutputDestination::String);output(dest: OutputDestination::String) se declara en
PdfDocumentInterface y devuelve los mismos bytes en bruto sin emitir ninguna
cabecera. Usa getPdfData() cuando tengas un NextPDF\Core\Document concreto, y
la forma output(...) cuando solo tengas la interfaz.
Si usas la integración de Laravel o Symfony, resuelve un documento nuevo desde el contenedor en lugar de construir uno directamente: consulta Devuelve un PDF generado desde un controlador para la vía de resolución en cada framework. Todo lo de abajo funciona igual independientemente de cómo obtuviste el documento.
Laravel: adjunta bytes en memoria a un Mailable
Sección titulada «Laravel: adjunta bytes en memoria a un Mailable»Attachment::fromData() de Laravel recibe un callback que devuelve los bytes en
bruto, más un nombre de archivo. No hay archivo temporal. Implementa
attachments() en tu Mailable y devuelve un 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(); }}Envíalo como de costumbre:
use App\Mail\InvoiceMail;use Illuminate\Support\Facades\Mail;
Mail::to($invoice->customerEmail)->send(new InvoiceMail($invoice));El callback de fromData() se invoca de forma diferida cuando se construye el
mensaje, así que el PDF se genera en el momento del envío, no en la construcción.
Establece ->withMime('application/pdf') para que el cliente del destinatario
trate la parte como un PDF en lugar de adivinar a partir de la extensión. Dentro de
un controlador puedes en cambio llamar a $message->attachData($bytes, $name, ['mime' => 'application/pdf'])
sobre un mensaje en bruto, pero Attachment::fromData() en el Mailable es la forma
moderna idiomática.
Symfony Mailer: adjunta bytes a un Email
Sección titulada «Symfony Mailer: adjunta bytes a un Email»Email::attach() de Symfony Mailer acepta el cuerpo como una cadena en memoria,
con un nombre de archivo y un tipo de contenido explícitos. De nuevo, sin archivo
temporal.
<?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) recibe los bytes
directamente. Pasa 'application/pdf' como tercer argumento para que la parte
quede tipada correctamente. Si prefieres adjuntar desde un flujo, attachFromPath()
existe, pero para contenido generado la forma en memoria attach() evita una ida y
vuelta innecesaria al disco.
Hazlo desde un job en cola
Sección titulada «Hazlo desde un job en cola»Construir un PDF de varias páginas y enviar correo son ambas tareas lo bastante lentas como para que no debas hacerlas en el hilo de la petición. Encola el trabajo. El patrón es despachar un job (o, en Laravel, encolar el propio Mailable) y construir el PDF en el worker.
La forma más sencilla de Laravel: haz que el Mailable sea ShouldQueue. Como el
callback de Attachment::fromData() se ejecuta cuando se construye el mensaje en
cola, el PDF se genera en el worker, no en el despacho.
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));Para Symfony, despacha un mensaje de Messenger que transporte los identificadores
(no los bytes), y deja que el handler construya el PDF y envíe el correo en el
worker. Pasa un id de factura, busca el registro en el handler y llama al
InvoiceMailer mostrado arriba. El mailer de Symfony ya es asíncrono cuando hay un
transporte de Messenger configurado para SendEmailMessage, así que incluso un
send() llamado de forma síncrona puede transportarse a un worker.
Si generas el PDF en un job de generación dedicado y luego lo envías por correo,
consulta
Genera un PDF en un job en cola
para la superficie GeneratePdfJob / GeneratePdfMessage de la integración y sus
reglas de seguridad del worker. Una forma común es: un job genera y guarda el PDF,
y un segundo job (o el callback de éxito) lo lee de vuelta y lo envía por correo.
Cuando mantienes los bytes en memoria a lo largo de un solo job, te saltas el
archivo por completo.
Notas sobre tamaño, contenido en línea y adjuntos
Sección titulada «Notas sobre tamaño, contenido en línea y adjuntos»- Adjunto, no en línea. Una factura o un informe generados son casi siempre un
archivo descargable aparte, así que adjúntalos. Reserva el contenido en línea
(
Content-Disposition: inlinecon una referenciacid:) para imágenes que incrustas en el cuerpo HTML: un PDF no es contenido del cuerpo. - Vigila el tamaño. Los adjuntos de correo se codifican en base64 en tránsito, lo que infla la carga útil en aproximadamente un tercio. Muchos servidores receptores limitan un mensaje a alrededor de 10–25 MB después de la codificación. Para un informe grande, adjunta un breve correo de notificación con un enlace de descarga firmado en lugar del archivo en sí, y sirve el PDF sobre HTTP: consulta Devuelve un PDF generado desde un controlador.
- Construye una vez, adjunta una vez. Captura los bytes devueltos una vez en una variable y reutiliza esa cadena para el mensaje. No llames al método final de salida repetidamente para un solo correo.
- Memoria en el worker. Mantener el PDF completo en memoria está bien para
facturas y recibos típicos. Para documentos muy grandes en un worker restringido,
guarda en una ruta temporal con
save(), adjunta mediante Laravelattach($path)/ SymfonyEmail::attachFromPath($path), y luego borra el archivo: cambias una ida y vuelta al disco por un pico de memoria más bajo. (Para la vía en memoria usada en todo el resto de esta página, la forma basada en bytes es LaravelAttachment::fromData()/ SymfonyEmail::attach($bytes, 'name.pdf', 'application/pdf').)
Notas de seguridad
Sección titulada «Notas de seguridad»- Nunca interpoles entrada de usuario sin validar en el nombre de archivo del adjunto. Pasa un valor que controles (un número de factura que tú generaste), para que el cliente del destinatario no pueda ser dirigido por un nombre manipulado.
- Envía a cada cliente solo su propio documento. Construye el PDF a partir de los registros del sujeto autenticado dentro del job, no de un id tomado por fe de la petición.
- En una vía en cola, registra la clase de la excepción y un id de correlación en
caso de fallo, nunca el mensaje de la excepción ni una traza de pila. Nunca
escribas un bloque
catchvacío alrededor de la construcción y el envío.
Véase también
Sección titulada «Véase también»- Incrusta archivos y crea portafolios de PDF: la inversa: adjunta archivos dentro del PDF.
- Genera un PDF en un job en cola: saca la generación del hilo de la petición.
- Devuelve un PDF generado desde un controlador: sirve el PDF sobre HTTP en lugar de adjuntarlo.
- Uso en producción con Laravel: resolver un documento desde el contenedor y la seguridad del worker.
- Uso en producción con Symfony: los workers de Messenger y la factoría de documentos.