把已產生的 PDF 當成 mailer 附件寄出
快速概覽
標題為「快速概覽」的區段寄一份發票、收據或報表,是你拿一份已產生 PDF 最常做的事情之一。乾淨的做法是建構文件、取它的原始位元組,並把那些位元組直接交給你 mailer 的附件 API。你在任何時點都不需要磁碟上的一個暫存檔。
這是一篇 how-to。它假設你已經知道如何在你的框架中寄信。NextPDF 端就一個呼叫:
Document::getPdfData() 回傳原始可攜式文件格式(PDF)位元組作為一個字串。附件端完全屬於你的 mailer——本指南使用
Laravel 的 Attachment::fromData() 與 Symfony Mailer 的 Email::attach()。
NextPDF 不 隨附一個郵件 helper。文件上沒有「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
型別握有文件(例如,框架整合交給你的一個值),請改用
contract 層級的對等寫法:
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() 接受一個回傳原始位元組的 callback,
加上一個檔名。沒有暫存檔。在你的 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() callback 會在訊息被建構時延遲叫用,所以
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() 接受 body 作為一個記憶體中的字串,搭配一個明確的檔名與內容型別。同樣地,沒有暫存檔。
<?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() 形式避免一次對磁碟的多餘往返。
從一個 queue 工作中做
標題為「從一個 queue 工作中做」的區段建構一份多頁 PDF 與寄信都夠慢,慢到你不該在請求執行緒上做。把工作排進 queue。模式是分派一個工作(或在 Laravel 中,把 Mailable 本身排進 queue),並在 worker 上建構 PDF。
最簡單的 Laravel 形式:讓 Mailable ShouldQueue。因為
Attachment::fromData() callback 在 queue 訊息被建構時執行,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。當有一個 Messenger transport 為 SendEmailMessage 設定時,
Symfony 的 mailer 已經是非同步的,所以即使是一個同步呼叫的 send()
也能被傳送到一個 worker。
如果你在一個專責的產生工作中產生 PDF 再寄出它,見
在 queue 工作中產生 PDF
以了解該整合的 GeneratePdfJob / GeneratePdfMessage 介面及其
worker 安全規則。一個常見的形狀是:一個工作產生並儲存 PDF,一個第二個工作(或成功的 callback)把它讀回來並寄出。當你在單一工作中把位元組保留在記憶體裡時,你就完全跳過了檔案。
大小、inline 與附件須知
標題為「大小、inline 與附件須知」的區段- 附件,而非 inline。 一份已產生的發票或報表幾乎總是一個獨立、可下載的檔案,所以把它附加。把 inline 內容
(
Content-Disposition: inline搭配一個cid:參照)保留給你內嵌進 HTML body 的影像——PDF 不是 body 內容。 - 留意大小。 電子郵件附件在傳輸中以 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')。)
安全性說明
標題為「安全性說明」的區段- 絕不把未經驗證的使用者輸入插進附件檔名。傳入一個你掌控的值(一個你產生的發票編號),讓收件者的用戶端無法被一個精心構造的名稱操縱。
- 只寄給每位客戶他們自己的文件。在工作內,從已驗證主體的記錄建構 PDF,而非從一個從請求中照單全收的 id。
- 在 queue 路徑中,失敗時記錄例外類別與一個關聯 id,
絕不記錄例外訊息或堆疊追蹤。絕不在 build-and-send 周圍寫一個空的
catch區塊。
另請參閱
標題為「另請參閱」的區段- 內嵌檔案並建立 PDF portfolio——相反操作:把檔案附加 進 PDF。
- 在 queue 工作中產生 PDF——把產生移出請求執行緒。
- 從控制器回傳已產生的 PDF——透過 HTTP 服務 PDF,而非附加它。
- Laravel 正式環境用法——從容器解析一份文件與 worker 安全。
- Symfony 正式環境用法——Messenger worker 與文件 factory。