将生成的 PDF 作为邮件附件发送
把一份发票、收据或报表用邮件发出去,是你对一份生成的 PDF 所做的最常见的事情之一。干净的做法是构建文档、取它的原始字节,并把那些字节直接交给你的 mailer 的附件 API。你在任何环节都不需要一个磁盘上的临时文件。
这是一篇 how-to。它假设你已经知道如何在你的框架中发送邮件。NextPDF 这一侧只是一次调用:Document::getPdfData() 把原始的可移植文档格式(PDF)字节作为一个字符串返回。附件这一侧完全属于你的 mailer——本指南使用 Laravel 的 Attachment::fromData() 与 Symfony Mailer 的 Email::attach()。
NextPDF 不出货任何邮件辅助工具。文档上没有“email this PDF”方法,而且你应当对任何展示这种方法的示例保持怀疑。附件 API 始终是你框架的。
本页是 把文件嵌入 PDF 内部 的外发对照面。那份指南把文件作为嵌入流附加进 PDF;本指南把完成的 PDF 附加到一封邮件上。它们是不同的操作——不要把两者混为一谈。
取得原始 PDF 字节
标题为“取得原始 PDF 字节”的章节无论你还做什么,NextPDF 这一步都相同:产出字节。
<?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() 构建文档并把它的字节作为一个字符串返回。它不向磁盘写入任何东西,也不发送任何超文本传输协议(HTTP)头,这正是你对一个附件所想要的。
如果你只通过 NextPDF\Contracts\PdfDocumentInterface 类型持有文档(例如,框架集成交给你的一个值),请改用契约级的等价方式:
use NextPDF\Contracts\OutputDestination;
$bytes = $document->output(dest: OutputDestination::String);output(dest: OutputDestination::String) 声明在 PdfDocumentInterface 上,并返回相同的原始字节而不发出任何头。当你持有一个具体的 NextPDF\Core\Document 时使用 getPdfData(),当你只有接口时使用 output(...) 形式。
如果你使用 Laravel 或 Symfony 集成,请从容器解析一份全新文档,而不要直接构造一份——各框架中的解析路径参见 从控制器返回生成的 PDF。无论你以何种方式取得文档,下文一切都一样有效。
Laravel:把内存中的字节附加到一个 Mailable
标题为“Laravel:把内存中的字节附加到一个 Mailable”的章节Laravel 的 Attachment::fromData() 接受一个返回原始字节的回调,外加一个文件名。没有临时文件。在你的 Mailable 上实现 attachments() 并返回一个 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(); }}照常发送它:
use App\Mail\InvoiceMail;use Illuminate\Support\Facades\Mail;
Mail::to($invoice->customerEmail)->send(new InvoiceMail($invoice));fromData() 回调会在消息被构建时惰性调用,因此 PDF 是在发送时生成的,而非在构造时。请设置 ->withMime('application/pdf'),让收件人的客户端把这一部分当作 PDF,而不是从扩展名去猜测。在控制器内部,你也可以在一个原始消息上调用 $message->attachData($bytes, $name, ['mime' => 'application/pdf']),但在 Mailable 上使用 Attachment::fromData() 才是惯用的现代形式。
Symfony Mailer:把字节附加到一个 Email
标题为“Symfony Mailer:把字节附加到一个 Email”的章节Symfony Mailer 的 Email::attach() 接受内存中字符串形式的主体,外加一个显式的文件名与内容类型。同样地,没有临时文件。
<?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) 直接接受字节。请把 'application/pdf' 作为第三个参数传入,使该部分被正确地类型化。如果你更愿意从一个流附加,存在 attachFromPath(),但对于生成的内容,内存中的 attach() 形式避免了一次无谓的磁盘往返。
从一个队列工作中执行
标题为“从一个队列工作中执行”的章节构建一份多页 PDF 与发送邮件都足够慢,以致你不应在请求线程上做它们。请把这项工作排入队列。其模式是派发一个 job(在 Laravel 中,也可以把 Mailable 本身排入队列),并在 worker 上构建 PDF。
最简单的 Laravel 形式:让 Mailable 实现 ShouldQueue。由于 Attachment::fromData() 回调会在队列消息被构建时运行,PDF 是在 worker 上生成的,而非在派发时。
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));对于 Symfony,请派发一个携带标识符(而非字节)的 Messenger 消息,并让 handler 在 worker 上构建 PDF 并发送邮件。传入一个 invoice id,在 handler 中查出该记录,并调用上文所示的 InvoiceMailer。当为 SendEmailMessage 配置了一个 Messenger transport 时,Symfony 的 mailer 已经是异步的,因此即便是一个同步调用的 send() 也能被传输到一个 worker 上。
如果你在一个专门的生成 job 中生成 PDF 然后再用邮件发送它,关于该集成的 GeneratePdfJob / GeneratePdfMessage 接口及其 worker 安全规则,参见 在队列工作中生成 PDF。一种常见形态是:一个 job 生成并保存 PDF,第二个 job(或成功回调)把它读回并用邮件发送。当你在单个 job 内把字节保留在内存中时,你完全跳过了文件。
体积、内联与附件须知
标题为“体积、内联与附件须知”的章节- 附件,而非内联。 一份生成的发票或报表几乎总是一个独立的可下载文件,因此请把它作为附件。请把内联内容(带
cid:引用的Content-Disposition: inline)保留给你嵌入 HTML 主体中的图像——一份 PDF 不是主体内容。 - 留意体积。 邮件附件在传输中以 base64 编码,这会让载荷膨胀约三分之一。许多接收服务器把一封消息的上限设在编码之后约 10–25 MB。对于一份大型报表,请附上一封带签名下载链接的简短通知邮件,而不是文件本身,并通过 HTTP 提供该 PDF——参见 从控制器返回生成的 PDF。
- 构建一次,附加一次。 把返回的字节一次性捕获进一个变量,并把那个字符串复用于该消息。不要为一封邮件反复调用最终的输出方法。
- worker 上的内存。 对于典型的发票与收据,把整份 PDF 保留在内存中没有问题。对于受限 worker 上非常大的文档,请用
save()保存到一个临时路径、通过 Laravelattach($path)/ SymfonyEmail::attachFromPath($path)附加,然后删除该文件——以一次磁盘往返换取一个更低的内存峰值。(对于本页其余各处使用的内存内路径,基于字节的形式是 LaravelAttachment::fromData()/ SymfonyEmail::attach($bytes, 'name.pdf', 'application/pdf')。)
安全注意事项
标题为“安全注意事项”的章节- 绝不要把未经验证的用户输入插入附件文件名。请传入一个你可控的值(一个由你生成的发票号),使收件人的客户端无法被一个精心构造的名称所操纵。
- 只向每个客户发送其本人的文档。请在 job 内部从已认证主体的记录构建 PDF,而不是从一个轻信请求所取的 id 构建。
- 在队列路径中,失败时记录异常类与一个关联 id,绝不记录异常消息或堆栈跟踪。绝不要在构建并发送的外围写一个空的
catch块。
另请参阅
标题为“另请参阅”的章节- 嵌入文件并创建 PDF 作品集——其逆操作:把文件附加进 PDF。
- 在队列工作中生成 PDF——把生成移出请求线程。
- 从控制器返回生成的 PDF——通过 HTTP 提供 PDF,而不是把它作为附件。
- Laravel 生产环境用法——从容器解析文档以及 worker 安全。
- Symfony 生产环境用法——Messenger worker 与文档工厂。