Bỏ qua để đến nội dung
getnextpdf.com

Gửi email một PDF được tạo dưới dạng tệp đính kèm của mailer

Gửi email một hóa đơn, biên lai, hay báo cáo là một trong những việc phổ biến nhất bạn làm với một PDF được tạo. Cách gọn gàng để làm điều đó là dựng tài liệu, lấy các byte thô của nó, và trao thẳng các byte đó cho API đính kèm của mailer. Bạn không cần một tệp tạm trên đĩa ở bất kỳ thời điểm nào.

Đây là một how-to. Nó giả định bạn đã biết cách gửi mail trong framework của mình. Phía NextPDF là một lệnh gọi duy nhất: Document::getPdfData() trả về các byte Portable Document Format (PDF) thô dưới dạng một chuỗi. Phía đính kèm hoàn toàn thuộc về mailer của bạn — hướng dẫn này dùng Attachment::fromData() của Laravel và Email::attach() của Symfony Mailer.

NextPDF không đi kèm một mail helper. Không có phương thức “email this PDF” trên một tài liệu, và bạn nên nghi ngờ bất kỳ ví dụ nào cho thấy một cái. API đính kèm luôn là của framework của bạn.

Trang này là đối ứng gửi-đi của việc nhúng tệp vào bên trong một PDF. Hướng dẫn đó đính kèm tệp vào trong PDF dưới dạng các luồng được nhúng; hướng dẫn này đính kèm PDF hoàn chỉnh vào một email. Chúng là các thao tác khác nhau — đừng lẫn lộn hai cái.

Dù bạn làm gì khác, bước NextPDF vẫn như nhau: tạo ra các byte.

<?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() dựng tài liệu và trả về các byte của nó dưới dạng một chuỗi. Nó không ghi gì xuống đĩa và không gửi header Hypertext Transfer Protocol (HTTP) nào, đó chính xác là điều bạn muốn cho một tệp đính kèm.

Nếu bạn chỉ giữ tài liệu qua kiểu NextPDF\Contracts\PdfDocumentInterface (ví dụ, một giá trị mà tích hợp framework trao cho bạn), hãy dùng tương đương ở cấp hợp đồng thay vì vậy:

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

output(dest: OutputDestination::String) được khai báo trên PdfDocumentInterface và trả về cùng các byte thô mà không phát ra bất kỳ header nào. Hãy dùng getPdfData() khi bạn giữ một NextPDF\Core\Document cụ thể, và dạng output(...) khi bạn chỉ có interface.

Nếu bạn dùng tích hợp Laravel hoặc Symfony, hãy phân giải một tài liệu mới từ container thay vì khởi tạo một cái trực tiếp — xem Trả về một PDF được tạo từ một controller để biết lối phân giải trong mỗi framework. Mọi thứ bên dưới hoạt động như nhau bất kể bạn lấy tài liệu bằng cách nào.

Laravel: đính kèm các byte trong bộ nhớ vào một Mailable

Phần tiêu đề “Laravel: đính kèm các byte trong bộ nhớ vào một Mailable”

Attachment::fromData() của Laravel nhận một callback trả về các byte thô, cộng với một tên tệp. Không có tệp tạm. Hãy hiện thực attachments() trên Mailable của bạn và trả về một 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();
}
}

Gửi nó như bình thường:

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

Callback fromData() được gọi một cách lười biếng khi thông điệp được dựng, nên PDF được tạo ở thời điểm gửi, không phải lúc khởi tạo. Đặt ->withMime('application/pdf') để client của người nhận coi phần đó là một PDF thay vì đoán từ phần mở rộng. Bên trong một controller, bạn có thể thay vào đó gọi $message->attachData($bytes, $name, ['mime' => 'application/pdf']) trên một thông điệp thô, nhưng Attachment::fromData() trên Mailable là dạng hiện đại đúng lề lối.

Symfony Mailer: đính kèm các byte vào một Email

Phần tiêu đề “Symfony Mailer: đính kèm các byte vào một Email”

Email::attach() của Symfony Mailer chấp nhận body dưới dạng một chuỗi trong bộ nhớ, kèm một tên tệp tường minh và content type. Một lần nữa, không có tệp tạm.

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) nhận các byte trực tiếp. Truyền 'application/pdf' làm đối số thứ ba để phần đó được gắn kiểu đúng. Nếu bạn muốn đính kèm từ một luồng, attachFromPath() tồn tại, nhưng với nội dung được tạo, dạng attach() trong bộ nhớ tránh một lần khứ hồi không cần thiết tới đĩa.

Làm điều đó từ một job được xếp hàng

Phần tiêu đề “Làm điều đó từ một job được xếp hàng”

Dựng một PDF nhiều trang và gửi mail đều đủ chậm để bạn không nên làm chúng trên luồng request. Hãy xếp hàng công việc. Mẫu là dispatch một job (hoặc, trong Laravel, xếp hàng chính Mailable) và dựng PDF trên worker.

Dạng Laravel đơn giản nhất: làm cho Mailable là ShouldQueue. Vì callback Attachment::fromData() chạy khi thông điệp được xếp hàng được dựng, PDF được tạo trên worker, không phải lúc dispatch.

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

Với Symfony, dispatch một thông điệp Messenger mang các định danh (không phải các byte), và để handler dựng PDF và gửi email trên worker. Truyền một id hóa đơn, tra bản ghi trong handler, và gọi InvoiceMailer được trình bày ở trên. Mailer của Symfony vốn đã bất đồng bộ khi một transport Messenger được cấu hình cho SendEmailMessage, nên ngay cả một send() được gọi đồng bộ cũng có thể được vận chuyển tới một worker.

Nếu bạn tạo PDF trong một job tạo chuyên biệt rồi gửi mail nó, xem Tạo một PDF trong một job được xếp hàng để biết bề mặt GeneratePdfJob / GeneratePdfMessage của tích hợp và các quy tắc an-toàn-worker của nó. Một hình dạng phổ biến là: một job tạo và lưu PDF, một job thứ hai (hoặc callback thành công) đọc nó lại và gửi mail nó. Khi bạn giữ các byte trong bộ nhớ xuyên suốt một job duy nhất, bạn bỏ qua tệp hoàn toàn.

Ghi chú về kích thước, inline, và đính kèm

Phần tiêu đề “Ghi chú về kích thước, inline, và đính kèm”
  • Đính kèm, không phải inline. Một hóa đơn hoặc báo cáo được tạo gần như luôn là một tệp tải về riêng biệt, nên hãy đính kèm nó. Hãy dành nội dung inline (Content-Disposition: inline với một tham chiếu cid:) cho các ảnh bạn nhúng trong body HTML — một PDF không phải là nội dung body.
  • Để ý kích thước. Các tệp đính kèm email được mã hóa base64 khi truyền, điều này làm phình payload lên khoảng một phần ba. Nhiều máy chủ nhận giới hạn một thông điệp ở khoảng 10–25 MB sau khi mã hóa. Với một báo cáo lớn, hãy đính kèm một email thông báo ngắn kèm một liên kết tải về có chữ ký thay vì chính tệp đó, và phục vụ PDF qua HTTP — xem Trả về một PDF được tạo từ một controller.
  • Dựng một lần, đính kèm một lần. Bắt các byte trả về một lần vào một biến và tái sử dụng chuỗi đó cho thông điệp. Đừng gọi phương thức đầu ra cuối cùng lặp đi lặp lại cho một email.
  • Bộ nhớ trên worker. Giữ toàn bộ PDF trong bộ nhớ là ổn cho các hóa đơn và biên lai điển hình. Với các tài liệu rất lớn trên một worker bị hạn chế, hãy lưu vào một đường dẫn tạm với save(), đính kèm qua Laravel attach($path) / Symfony Email::attachFromPath($path), rồi xóa tệp — đánh đổi một lần khứ hồi đĩa lấy một đỉnh bộ nhớ thấp hơn. (Với lối trong-bộ-nhớ dùng ở mọi nơi khác trên trang này, dạng dựa-trên-byte là Laravel Attachment::fromData() / Symfony Email::attach($bytes, 'name.pdf', 'application/pdf').)
  • Đừng bao giờ nội suy đầu vào người dùng chưa xác thực vào tên tệp đính kèm. Hãy truyền một giá trị bạn kiểm soát (một số hóa đơn bạn đã tạo), để client của người nhận không thể bị lái bởi một tên được dàn dựng.
  • Chỉ gửi cho mỗi khách hàng tài liệu của riêng họ. Hãy dựng PDF từ các bản ghi của chủ thể đã xác thực bên trong job, không phải từ một id được nhận một cách tin tưởng từ request.
  • Trong một lối được xếp hàng, hãy ghi log lớp ngoại lệ và một id tương quan khi thất bại, không bao giờ là thông điệp ngoại lệ hay một stack trace. Đừng bao giờ ghi một khối catch rỗng quanh việc dựng-và-gửi.