ส่ง PDF ที่สร้างขึ้นทางอีเมลเป็นไฟล์แนบของ mailer
ภาพรวมโดยสังเขป
หัวข้อที่มีชื่อว่า “ภาพรวมโดยสังเขป”การส่งใบแจ้งหนี้ ใบเสร็จ หรือรายงานทางอีเมลเป็นหนึ่งในสิ่งที่พบบ่อยที่สุดที่คุณทำกับ PDF ที่สร้างขึ้น วิธีที่สะอาดคือสร้างเอกสาร นำไบต์ดิบของมัน แล้วส่งไบต์เหล่านั้นตรงไปยัง attachment API ของ mailer ของคุณ คุณไม่ต้องการไฟล์ชั่วคราวบนดิสก์ ณ จุดใดเลย
นี่คือคู่มือวิธีทำ โดยถือว่าคุณรู้วิธีส่งเมลในเฟรมเวิร์กของคุณอยู่แล้ว ฝั่ง NextPDF คือการเรียกเดียว: Document::getPdfData() คืนไบต์ Portable Document Format (PDF) ดิบเป็นสตริง ฝั่งไฟล์แนบเป็นของ mailer ของคุณทั้งหมด — คู่มือนี้ใช้ Attachment::fromData() ของ Laravel และ Email::attach() ของ Symfony Mailer
NextPDF ไม่ มาพร้อมตัวช่วยเมล ไม่มีเมธอด “email this PDF” บนเอกสาร และคุณควรสงสัยตัวอย่างใดที่แสดงสิ่งนั้น attachment API เป็นของเฟรมเวิร์กของคุณเสมอ
หน้านี้เป็นคู่หูฝั่งขาออกของการฝังไฟล์ภายใน PDF คู่มือนั้นแนบไฟล์ เข้าไปใน PDF เป็น embedded stream ส่วนคู่มือนี้แนบ 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() สร้างเอกสารและคืนไบต์ของมันเป็นสตริง มันไม่เขียนสิ่งใดลงดิสก์และไม่ส่ง header Hypertext Transfer Protocol (HTTP) ใด ซึ่งเป็นสิ่งที่คุณต้องการพอดีสำหรับไฟล์แนบ
หากคุณถือเอกสารเฉพาะผ่านชนิด NextPDF\Contracts\PdfDocumentInterface (ตัวอย่างเช่น ค่าที่การรวมเฟรมเวิร์กส่งให้คุณ) ให้ใช้สิ่งที่เทียบเท่าระดับ contract แทน:
use NextPDF\Contracts\OutputDestination;
$bytes = $document->output(dest: OutputDestination::String);output(dest: OutputDestination::String) ถูกประกาศบน PdfDocumentInterface และคืนไบต์ดิบเดียวกันโดยไม่ปล่อย header ใด ใช้ getPdfData() เมื่อคุณถือ NextPDF\Core\Document ที่เป็นรูปธรรม และรูปแบบ output(...) เมื่อคุณมีเพียง interface
หากคุณใช้การรวม Laravel หรือ Symfony ให้รีโซลฟ์เอกสารใหม่จากคอนเทนเนอร์แทนการสร้างมันโดยตรง — ดูส่งคืน PDF ที่สร้างจากคอนโทรลเลอร์ สำหรับเส้นทางการรีโซลฟ์ในแต่ละเฟรมเวิร์ก ทุกอย่างด้านล่างทำงานเหมือนกันไม่ว่าคุณได้เอกสารมาอย่างไร
Laravel: แนบไบต์ในหน่วยความจำกับ Mailable
หัวข้อที่มีชื่อว่า “Laravel: แนบไบต์ในหน่วยความจำกับ Mailable”Attachment::fromData() ของ Laravel รับ callback ที่คืนไบต์ดิบ บวกชื่อไฟล์ ไม่มีไฟล์ชั่วคราว implement attachments() บน Mailable ของคุณแล้วคืน 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));callback fromData() ถูกเรียกแบบ lazy เมื่อข้อความถูกสร้าง ดังนั้น PDF จึงถูกสร้างตอนส่ง ไม่ใช่ตอนสร้างอ็อบเจกต์ ตั้ง ->withMime('application/pdf') เพื่อให้ไคลเอนต์ของผู้รับปฏิบัติต่อส่วนนั้นเป็น PDF แทนการเดาจากนามสกุล ภายในคอนโทรลเลอร์ คุณเรียก $message->attachData($bytes, $name, ['mime' => 'application/pdf']) บนข้อความดิบแทนได้ แต่ Attachment::fromData() บน Mailable เป็นรูปแบบสมัยใหม่ที่เป็นสำนวน
Symfony Mailer: แนบไบต์กับ Email
หัวข้อที่มีชื่อว่า “Symfony Mailer: แนบไบต์กับ Email”Email::attach() ของ Symfony Mailer รับ body เป็นสตริงในหน่วยความจำ พร้อมชื่อไฟล์และ content type ที่ชัดเจน อีกครั้ง ไม่มีไฟล์ชั่วคราว
<?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' เป็นอาร์กิวเมนต์ที่สามเพื่อให้ส่วนนั้นถูกระบุชนิดอย่างถูกต้อง หากคุณต้องการแนบจาก stream attachFromPath() มีอยู่ แต่สำหรับเนื้อหาที่สร้างขึ้น รูปแบบ attach() ในหน่วยความจำเลี่ยงการไป-กลับดิสก์ที่ไม่จำเป็น
ทำจาก queued job
หัวข้อที่มีชื่อว่า “ทำจาก queued job”การสร้าง PDF หลายหน้าและการส่งเมลทั้งคู่ช้าพอที่คุณไม่ควรทำมันบน thread ของคำขอ ให้ queue งานนั้น รูปแบบคือ dispatch job (หรือใน Laravel queue ตัว Mailable เอง) แล้วสร้าง PDF บน worker
รูปแบบ Laravel ที่ง่ายที่สุด: ทำให้ Mailable เป็น ShouldQueue เพราะ callback Attachment::fromData() รันเมื่อข้อความที่ queue ถูกสร้าง PDF จึงถูกสร้างบน worker ไม่ใช่ตอน dispatch
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 ให้ dispatch ข้อความ Messenger ที่พกตัวระบุ (ไม่ใช่ไบต์) แล้วให้ handler สร้าง PDF และส่งอีเมลบน worker ส่ง invoice id ค้นหาเรกคอร์ดใน handler แล้วเรียก InvoiceMailer ที่แสดงด้านบน mailer ของ Symfony เป็น asynchronous อยู่แล้วเมื่อกำหนดค่า Messenger transport สำหรับ SendEmailMessage ดังนั้นแม้ send() ที่เรียกแบบ synchronous ก็ถูกขนส่งไปยัง worker ได้
หากคุณสร้าง PDF ใน generation job เฉพาะแล้วจึงส่งเมล ดูสร้าง PDF ใน queued job สำหรับพื้นผิว GeneratePdfJob / GeneratePdfMessage ของการรวมและกฎความปลอดภัยของ worker รูปร่างที่พบบ่อยคือ: job หนึ่งสร้างและบันทึก PDF, job ที่สอง (หรือ callback ที่สำเร็จ) อ่านมันกลับแล้วส่งเมล เมื่อคุณเก็บไบต์ไว้ในหน่วยความจำตลอด job เดียว คุณข้ามไฟล์ไปทั้งหมด
หมายเหตุเรื่องขนาด อินไลน์ และไฟล์แนบ
หัวข้อที่มีชื่อว่า “หมายเหตุเรื่องขนาด อินไลน์ และไฟล์แนบ”- ไฟล์แนบ ไม่ใช่อินไลน์ ใบแจ้งหนี้หรือรายงานที่สร้างขึ้นเกือบจะเป็นไฟล์ที่ดาวน์โหลดแยกเสมอ ดังนั้นให้แนบมัน สงวนเนื้อหาอินไลน์ (
Content-Disposition: inlineพร้อมการอ้างอิงcid:) ไว้สำหรับรูปภาพที่คุณฝังใน HTML body — PDF ไม่ใช่เนื้อหา body - ระวังขนาด ไฟล์แนบอีเมลถูก base64-encoded ระหว่างขนส่ง ซึ่งทำให้ payload พองขึ้นราวหนึ่งในสาม เซิร์ฟเวอร์ผู้รับจำนวนมากจำกัดข้อความไว้ที่ราว 10–25 MB หลัง การ encode สำหรับรายงานขนาดใหญ่ ให้แนบอีเมลแจ้งเตือนสั้น ๆ พร้อมลิงก์ดาวน์โหลดที่ลงนามแทนตัวไฟล์ แล้วให้บริการ PDF ผ่าน HTTP — ดูส่งคืน PDF ที่สร้างจากคอนโทรลเลอร์
- สร้างครั้งเดียว แนบครั้งเดียว เก็บไบต์ที่คืนมาครั้งเดียวลงในตัวแปรแล้วนำสตริงนั้นกลับมาใช้ซ้ำสำหรับข้อความ อย่าเรียกเมธอด output สุดท้ายซ้ำสำหรับอีเมลเดียว
- หน่วยความจำบน worker การถือ PDF ทั้งฉบับในหน่วยความจำเป็นเรื่องปกติสำหรับใบแจ้งหนี้และใบเสร็จทั่วไป สำหรับเอกสารขนาดใหญ่มากบน worker ที่มีข้อจำกัด ให้บันทึกไปยัง path ชั่วคราวด้วย
save()แนบผ่าน Laravelattach($path)/ SymfonyEmail::attachFromPath($path)แล้วลบไฟล์ — แลกการไป-กลับดิสก์กับหน่วยความจำสูงสุดที่ต่ำลง (สำหรับเส้นทางในหน่วยความจำที่ใช้ทุกที่อื่นในหน้านี้ รูปแบบที่อิงไบต์คือ LaravelAttachment::fromData()/ SymfonyEmail::attach($bytes, 'name.pdf', 'application/pdf'))
หมายเหตุด้านความปลอดภัย
หัวข้อที่มีชื่อว่า “หมายเหตุด้านความปลอดภัย”- ไม่เคยแทรกอินพุตผู้ใช้ที่ไม่ได้ตรวจสอบลงในชื่อไฟล์แนบ ส่งค่าที่คุณควบคุม (เลขใบแจ้งหนี้ที่คุณสร้าง) เพื่อให้ไคลเอนต์ของผู้รับไม่ถูกชี้นำด้วยชื่อที่ถูกประดิษฐ์ขึ้น
- ส่งเอกสารของลูกค้าแต่ละรายเฉพาะของพวกเขาเอง สร้าง PDF จากเรกคอร์ดของ subject ที่ผ่านการยืนยันตัวตนภายใน job ไม่ใช่จาก id ที่เชื่อตามคำขอ
- ในเส้นทางที่ queue ให้ log คลาส exception และ correlation id เมื่อล้มเหลว ไม่เคย log ข้อความ exception หรือ stack trace ไม่เคยเขียนบล็อก
catchว่างรอบการสร้างและส่ง
ดูเพิ่มเติม
หัวข้อที่มีชื่อว่า “ดูเพิ่มเติม”- ฝังไฟล์และสร้าง PDF portfolio — สิ่งที่ตรงกันข้าม: แนบไฟล์ เข้าไปใน PDF
- สร้าง PDF ใน queued job — ย้ายการสร้างออกจาก thread ของคำขอ
- ส่งคืน PDF ที่สร้างจากคอนโทรลเลอร์ — ให้บริการ PDF ผ่าน HTTP แทนการแนบมัน
- การใช้งานจริงของ Laravel — การรีโซลฟ์เอกสารจากคอนเทนเนอร์และความปลอดภัยของ worker
- การใช้งานจริงของ Symfony — Messenger worker และ document factory