跳到內容
getnextpdf.com

把已產生的 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 附加 到一封郵件。它們是不同的操作——不要把這兩者搞混。

無論你還做什麼,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

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

照常寄出:

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 作為一個記憶體中的字串,搭配一個明確的檔名與內容型別。同樣地,沒有暫存檔。

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) 直接接受位元組。把 'application/pdf' 當成第三個引數傳入,讓那個部分被正確地標型。如果你寧可從一個串流附加,attachFromPath() 存在,但對於已產生的內容,記憶體中的 attach() 形式避免一次對磁碟的多餘往返。

建構一份多頁 PDF 與寄信都夠慢,慢到你不該在請求執行緒上做。把工作排進 queue。模式是分派一個工作(或在 Laravel 中,把 Mailable 本身排進 queue),並在 worker 上建構 PDF。

最簡單的 Laravel 形式:讓 Mailable ShouldQueue。因為 Attachment::fromData() callback 在 queue 訊息被建構時執行,PDF 是在 worker 上產生,而非在分派時。

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

對於 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 內容 (Content-Disposition: inline 搭配一個 cid: 參照)保留給你內嵌進 HTML body 的影像——PDF 不是 body 內容。
  • 留意大小。 電子郵件附件在傳輸中以 base64 編碼,這會把酬載膨脹約三分之一。許多收信伺服器把訊息上限訂在編碼 約 10–25 MB。對於大型報表,改附一封帶有有簽章下載連結的簡短通知郵件,而不是檔案本身,並透過 HTTP 服務該 PDF——見 從控制器回傳已產生的 PDF
  • 建構一次,附加一次。 把回傳的位元組一次捕捉進一個變數, 並把那個字串重用於訊息。不要為一封郵件反覆呼叫最終的輸出方法。
  • worker 上的記憶體。 把整份 PDF 保留在記憶體中,對典型的發票與收據沒問題。對於受限 worker 上非常大的文件,用 save() 存到一個暫存路徑、透過 Laravel attach($path) / Symfony Email::attachFromPath($path) 附加,再刪掉檔案——以一次磁碟往返換取較低的記憶體尖峰。(對於本頁其他地方到處用的記憶體中路徑,以位元組為基礎的形式是 Laravel Attachment::fromData() / Symfony Email::attach($bytes, 'name.pdf', 'application/pdf')。)
  • 絕不把未經驗證的使用者輸入插進附件檔名。傳入一個你掌控的值(一個你產生的發票編號),讓收件者的用戶端無法被一個精心構造的名稱操縱。
  • 只寄給每位客戶他們自己的文件。在工作內,從已驗證主體的記錄建構 PDF,而非從一個從請求中照單全收的 id。
  • 在 queue 路徑中,失敗時記錄例外類別與一個關聯 id, 絕不記錄例外訊息或堆疊追蹤。絕不在 build-and-send 周圍寫一個空的 catch 區塊。