Pular para o conteúdo
getnextpdf.com

Envie por e-mail um PDF gerado como anexo de mailer

Enviar por e-mail uma fatura, recibo ou relatório é uma das coisas mais comuns que você faz com um PDF gerado. A forma limpa de fazer isso é construir o documento, pegar seus bytes brutos e entregá-los diretamente à API de anexos do seu mailer. Você não precisa de um arquivo temporário no disco em momento algum.

Este é um how-to. Ele assume que você já sabe como enviar e-mail no seu framework. O lado do NextPDF é uma única chamada: Document::getPdfData() retorna os bytes brutos do Portable Document Format (PDF) como uma string. O lado do anexo pertence inteiramente ao seu mailer — este guia usa Attachment::fromData() do Laravel e Email::attach() do Symfony Mailer.

O NextPDF não traz um helper de e-mail. Não há método “envie este PDF por e-mail” em um documento, e você deve desconfiar de qualquer exemplo que mostre um. A API de anexo é sempre a do seu framework.

Esta página é a contraparte de saída de incorporar arquivos dentro de um PDF. Aquele guia anexa arquivos dentro do PDF como streams incorporados; este guia anexa o PDF finalizado a um e-mail. São operações diferentes — não confunda as duas.

Faça o que mais fizer, o passo do NextPDF é o mesmo: produzir os 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() constrói o documento e retorna seus bytes como uma string. Ele não escreve nada no disco e não envia nenhum cabeçalho Hypertext Transfer Protocol (HTTP), que é exatamente o que você quer para um anexo.

Se você tem o documento apenas por meio do tipo NextPDF\Contracts\PdfDocumentInterface (por exemplo, um valor que a integração de framework lhe entregou), use o equivalente no nível do contrato em vez disso:

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

output(dest: OutputDestination::String) é declarado em PdfDocumentInterface e retorna os mesmos bytes brutos sem emitir nenhum cabeçalho. Use getPdfData() quando você tem um NextPDF\Core\Document concreto, e a forma output(...) quando você tem apenas a interface.

Se você usa a integração do Laravel ou do Symfony, resolva um documento novo a partir do contêiner em vez de construir um diretamente — consulte Retorne um PDF gerado a partir de um controller para o caminho de resolução em cada framework. Tudo abaixo funciona da mesma forma independentemente de como você obteve o documento.

O Attachment::fromData() do Laravel recebe um callback que retorna os bytes brutos, mais um nome de arquivo. Não há arquivo temporário. Implemente attachments() no seu Mailable e retorne um 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();
}
}

Envie-o como de costume:

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

O callback de fromData() é invocado de forma preguiçosa quando a mensagem é construída, então o PDF é gerado na hora do envio, não na construção. Defina ->withMime('application/pdf') para que o cliente do destinatário trate a parte como um PDF em vez de adivinhar pela extensão. Dentro de um controller você pode, em vez disso, chamar $message->attachData($bytes, $name, ['mime' => 'application/pdf']) em uma mensagem bruta, mas Attachment::fromData() no Mailable é a forma moderna idiomática.

O Email::attach() do Symfony Mailer aceita o corpo como uma string em memória, com um nome de arquivo e content type explícitos. De novo, nenhum arquivo temporário.

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) recebe os bytes diretamente. Passe 'application/pdf' como terceiro argumento para que a parte seja tipada corretamente. Se você prefere anexar a partir de um stream, attachFromPath() existe, mas para conteúdo gerado a forma em memória attach() evita uma viagem desnecessária ao disco.

Construir um PDF de várias páginas e enviar e-mail são ambos lentos o suficiente para que você não deva fazê-los na thread de requisição. Coloque o trabalho na fila. O padrão é despachar um job (ou, no Laravel, colocar o próprio Mailable na fila) e construir o PDF no worker.

A forma mais simples do Laravel: torne o Mailable ShouldQueue. Como o callback de Attachment::fromData() roda quando a mensagem em fila é construída, o PDF é gerado no worker, não no despacho.

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));

Para o Symfony, despache uma mensagem do Messenger carregando os identificadores (não os bytes), e deixe o handler construir o PDF e enviar o e-mail no worker. Passe um id de fatura, busque o registro no handler e chame o InvoiceMailer mostrado acima. O mailer do Symfony já é assíncrono quando um transporte do Messenger está configurado para SendEmailMessage, então mesmo um send() chamado de forma síncrona pode ser transportado para um worker.

Se você gera o PDF em um job de geração dedicado e depois o envia por e-mail, consulte Gere um PDF em um job em fila para a superfície GeneratePdfJob / GeneratePdfMessage da integração e suas regras de segurança no worker. Um formato comum é: um job gera e salva o PDF, um segundo job (ou o callback de sucesso) o relê e o envia por e-mail. Quando você mantém os bytes em memória ao longo de um único job, você pula o arquivo por completo.

  • Anexo, não inline. Uma fatura ou relatório gerado é quase sempre um arquivo baixável separado, então anexe-o. Reserve o conteúdo inline (Content-Disposition: inline com uma referência cid:) para imagens que você incorpora no corpo HTML — um PDF não é conteúdo de corpo.
  • Cuidado com o tamanho. Anexos de e-mail são codificados em base64 em trânsito, o que infla o payload em cerca de um terço. Muitos servidores receptores limitam uma mensagem em torno de 10–25 MB após a codificação. Para um relatório grande, anexe um e-mail curto de notificação com um link de download assinado em vez do arquivo em si, e sirva o PDF por HTTP — consulte Retorne um PDF gerado a partir de um controller.
  • Construa uma vez, anexe uma vez. Capture os bytes retornados uma vez em uma variável e reutilize essa string para a mensagem. Não chame o método de saída final repetidamente para um e-mail.
  • Memória no worker. Manter o PDF inteiro em memória está ok para faturas e recibos típicos. Para documentos muito grandes em um worker restrito, salve em um caminho temporário com save(), anexe via attach($path) do Laravel / Email::attachFromPath($path) do Symfony, e então apague o arquivo — trocando uma viagem ao disco por um pico de memória menor. (Para o caminho em memória usado em todo o resto desta página, a forma baseada em bytes é Attachment::fromData() do Laravel / Email::attach($bytes, 'name.pdf', 'application/pdf') do Symfony.)
  • Nunca interpole entrada de usuário não validada no nome do arquivo de anexo. Passe um valor que você controla (um número de fatura que você gerou), para que o cliente do destinatário não possa ser direcionado por um nome forjado.
  • Envie a cada cliente apenas o documento dele. Construa o PDF a partir dos registros do sujeito autenticado dentro do job, não a partir de um id tomado por confiança a partir da requisição.
  • Em um caminho em fila, registre a classe da exceção e um id de correlação em caso de falha, nunca a mensagem da exceção ou um stack trace. Nunca escreva um bloco catch vazio em torno do build-and-send.