Отправка сгенерированного PDF как вложения письма
Отправка счёта, квитанции или отчёта по почте — одно из самых частых действий, которые вы делаете со сгенерированным PDF. Чистый способ сделать это — построить документ, взять его сырые байты и передать эти байты прямо в API вложений вашего почтовика. Вам не нужен временный файл на диске ни в один момент.
Это операционное руководство. Оно предполагает, что вы уже знаете, как отправлять почту в
вашем фреймворке. Со стороны NextPDF это один вызов:
Document::getPdfData() возвращает сырые байты Portable Document Format (PDF) как
строку. Сторона вложения полностью принадлежит вашему почтовику — это руководство использует
Attachment::fromData() Laravel и Email::attach() Symfony Mailer.
NextPDF не поставляет почтового помощника. Нет метода “отправить этот 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() строит документ и возвращает его байты как строку. Он не пишет
ничего на диск и не отправляет заголовков Hypertext Transfer Protocol (HTTP), что
именно то, что вам нужно для вложения.
Если вы держите документ только через тип
NextPDF\Contracts\PdfDocumentInterface (например, значение, которое вам передала
интеграция с фреймворком), используйте эквивалент уровня контракта вместо этого:
use NextPDF\Contracts\OutputDestination;
$bytes = $document->output(dest: OutputDestination::String);output(dest: OutputDestination::String) объявлен на
PdfDocumentInterface и возвращает те же сырые байты без выдачи каких-либо
заголовков. Используйте getPdfData(), когда держите конкретный NextPDF\Core\Document, и
форму output(...), когда у вас есть только интерфейс.
Если вы используете интеграцию Laravel или Symfony, разрешайте свежий документ из контейнера, а не конструируйте его напрямую — см. Возврат сгенерированного PDF из контроллера для пути разрешения в каждом фреймворке. Всё ниже работает так же независимо от того, как вы получили документ.
Laravel: прикрепить байты из памяти к Mailable
Заголовок раздела «Laravel: прикрепить байты из памяти к Mailable»Attachment::fromData() Laravel принимает колбэк, который возвращает сырые байты,
плюс имя файла. Временного файла нет. Реализуйте 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));Колбэк fromData() вызывается лениво, когда сообщение строится, поэтому
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 принимает тело как строку в памяти, с
явным именем файла и типом содержимого. Опять же, без временного файла.
<?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 и отправка почты оба достаточно медленные, чтобы вам не стоило делать их в потоке запроса. Поставьте работу в очередь. Паттерн — диспетчеризировать задачу (или, в Laravel, поставить сам Mailable в очередь) и строить PDF на воркере.
Простейшая форма Laravel: сделать Mailable ShouldQueue. Поскольку
колбэк Attachment::fromData() выполняется, когда поставленное в очередь сообщение строится,
PDF генерируется на воркере, а не при диспетчеризации.
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, несущее идентификаторы (а не
байты), и дайте обработчику построить PDF и отправить письмо на воркере. Передайте
id счёта, найдите запись в обработчике и вызовите показанный выше InvoiceMailer.
Почтовик Symfony уже асинхронен, когда для SendEmailMessage настроен транспорт
Messenger, поэтому даже синхронно вызванный send()
может быть перенесён на воркер.
Если вы генерируете PDF в выделенной задаче генерации, а затем отправляете его по почте, см.
Генерацию PDF в задаче очереди
для поверхности интеграции GeneratePdfJob / GeneratePdfMessage и её
правил безопасности воркера. Распространённая форма такова: одна задача генерирует и сохраняет
PDF, вторая задача (или колбэк успеха) читает его обратно и отправляет по почте. Когда вы держите
байты в памяти в пределах одной задачи, вы полностью пропускаете файл.
Заметки о размере, встроенном содержимом и вложениях
Заголовок раздела «Заметки о размере, встроенном содержимом и вложениях»- Вложение, а не встроенное содержимое. Сгенерированный счёт или отчёт почти всегда —
отдельный загружаемый файл, поэтому прикрепляйте его. Резервируйте встроенное содержимое
(
Content-Disposition: inlineсо ссылкойcid:) для изображений, которые вы встраиваете в тело HTML — PDF не является содержимым тела. - Следите за размером. Вложения письма кодируются в base64 при передаче, что раздувает полезную нагрузку примерно на треть. Многие принимающие серверы ограничивают сообщение примерно 10–25 МБ после кодирования. Для большого отчёта прикрепите короткое уведомительное письмо с подписанной ссылкой на загрузку вместо самого файла и обслуживайте PDF по HTTP — см. Возврат сгенерированного PDF из контроллера.
- Стройте один раз, прикрепляйте один раз. Захватите возвращённые байты один раз в переменную и переиспользуйте эту строку для сообщения. Не вызывайте финальный метод вывода многократно для одного письма.
- Память на воркере. Держать полный PDF в памяти нормально для типичных
счетов и квитанций. Для очень больших документов на ограниченном воркере сохраните
на временный путь через
save(), прикрепите через Laravelattach($path)/ SymfonyEmail::attachFromPath($path), затем удалите файл — обменяв круговой ход к диску на меньший пик памяти. (Для пути в памяти, используемого везде ещё на этой странице, форма на основе байтов — это LaravelAttachment::fromData()/ SymfonyEmail::attach($bytes, 'name.pdf', 'application/pdf').)
Примечания по безопасности
Заголовок раздела «Примечания по безопасности»- Никогда не интерполируйте непроверенный ввод пользователя в имя файла вложения. Передавайте значение, которое вы контролируете (номер счёта, который вы сгенерировали), чтобы клиент получателя нельзя было направить созданным именем.
- Отправляйте каждому клиенту только его собственный документ. Стройте PDF из записей аутентифицированного субъекта внутри задачи, а не из id, взятого на веру из запроса.
- В пути с очередью журналируйте класс исключения и id корреляции при сбое,
никогда сообщение исключения или трассировку стека. Никогда не пишите пустой блок
catchвокруг сборки-и-отправки.
См. также
Заголовок раздела «См. также»- Встраивание файлов и создание PDF-портфолио — обратное: прикрепить файлы внутрь PDF.
- Генерация PDF в задаче очереди — перенесите генерацию вне потока запроса.
- Возврат сгенерированного PDF из контроллера — обслуживайте PDF по HTTP вместо прикрепления.
- Производственное использование Laravel — разрешение документа из контейнера и безопасность воркера.
- Производственное использование Symfony — воркеры Messenger и фабрика документов.