Перейти к содержимому
getnextpdf.com

Отправка сгенерированного PDF как вложения письма

Отправка счёта, квитанции или отчёта по почте — одно из самых частых действий, которые вы делаете со сгенерированным PDF. Чистый способ сделать это — построить документ, взять его сырые байты и передать эти байты прямо в API вложений вашего почтовика. Вам не нужен временный файл на диске ни в один момент.

Это операционное руководство. Оно предполагает, что вы уже знаете, как отправлять почту в вашем фреймворке. Со стороны NextPDF это один вызов: Document::getPdfData() возвращает сырые байты Portable Document Format (PDF) как строку. Сторона вложения полностью принадлежит вашему почтовику — это руководство использует Attachment::fromData() Laravel и Email::attach() Symfony Mailer.

NextPDF не поставляет почтового помощника. Нет метода “отправить этот 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() строит документ и возвращает его байты как строку. Он не пишет ничего на диск и не отправляет заголовков 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 из контроллера для пути разрешения в каждом фреймворке. Всё ниже работает так же независимо от того, как вы получили документ.

Attachment::fromData() Laravel принимает колбэк, который возвращает сырые байты, плюс имя файла. Временного файла нет. Реализуйте attachments() на вашем Mailable и верните один 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() вызывается лениво, когда сообщение строится, поэтому PDF генерируется во время отправки, а не при конструировании. Задайте ->withMime('application/pdf'), чтобы клиент получателя трактовал часть как PDF, а не угадывал по расширению. Внутри контроллера вы можете вместо этого вызвать $message->attachData($bytes, $name, ['mime' => 'application/pdf']) на сыром сообщении, но Attachment::fromData() на Mailable — идиоматичная современная форма.

Email::attach() Symfony Mailer принимает тело как строку в памяти, с явным именем файла и типом содержимого. Опять же, без временного файла.

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 и отправка почты оба достаточно медленные, чтобы вам не стоило делать их в потоке запроса. Поставьте работу в очередь. Паттерн — диспетчеризировать задачу (или, в Laravel, поставить сам Mailable в очередь) и строить PDF на воркере.

Простейшая форма Laravel: сделать Mailable ShouldQueue. Поскольку колбэк Attachment::fromData() выполняется, когда поставленное в очередь сообщение строится, PDF генерируется на воркере, а не при диспетчеризации.

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, несущее идентификаторы (а не байты), и дайте обработчику построить 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(), прикрепите через Laravel attach($path) / Symfony Email::attachFromPath($path), затем удалите файл — обменяв круговой ход к диску на меньший пик памяти. (Для пути в памяти, используемого везде ещё на этой странице, форма на основе байтов — это Laravel Attachment::fromData() / Symfony Email::attach($bytes, 'name.pdf', 'application/pdf').)
  • Никогда не интерполируйте непроверенный ввод пользователя в имя файла вложения. Передавайте значение, которое вы контролируете (номер счёта, который вы сгенерировали), чтобы клиент получателя нельзя было направить созданным именем.
  • Отправляйте каждому клиенту только его собственный документ. Стройте PDF из записей аутентифицированного субъекта внутри задачи, а не из id, взятого на веру из запроса.
  • В пути с очередью журналируйте класс исключения и id корреляции при сбое, никогда сообщение исключения или трассировку стека. Никогда не пишите пустой блок catch вокруг сборки-и-отправки.