Przejdź do głównej zawartości
getnextpdf.com

Wysyłanie wygenerowanego PDF jako załącznika mailera

Wysłanie faktury, paragonu czy raportu to jedna z najczęstszych rzeczy, które robisz z wygenerowanym PDF-em. Czystym sposobem na to jest zbudowanie dokumentu, wzięcie jego surowych bajtów i przekazanie tych bajtów wprost do API załączników twojego mailera. Nie potrzebujesz pliku tymczasowego na dysku w żadnym momencie.

To poradnik how-to. Zakłada, że potrafisz już wysyłać pocztę w swoim frameworku. Strona NextPDF to pojedyncze wywołanie: Document::getPdfData() zwraca surowe bajty Portable Document Format (PDF) jako łańcuch znaków. Strona załącznika należy w całości do twojego mailera — ten przewodnik używa Laravel Attachment::fromData() oraz Symfony Mailer Email::attach().

NextPDF nie dostarcza pomocnika pocztowego. Nie ma metody „wyślij ten PDF mailem” na dokumencie i powinieneś być podejrzliwy wobec każdego przykładu, który taką pokazuje. API załącznika należy zawsze do twojego frameworka.

Ta strona jest wychodzącym odpowiednikiem osadzania plików wewnątrz PDF. Tamten przewodnik dołącza pliki do PDF jako osadzone strumienie; ten przewodnik dołącza gotowy PDF do e-maila. To różne operacje — nie myl ich.

Cokolwiek innego robisz, krok NextPDF jest ten sam: wytwórz bajty.

<?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() buduje dokument i zwraca jego bajty jako łańcuch znaków. Nie zapisuje nic na dysk i nie wysyła żadnych nagłówków Hypertext Transfer Protocol (HTTP), co jest dokładnie tym, czego chcesz dla załącznika.

Jeśli trzymasz dokument tylko przez typ NextPDF\Contracts\PdfDocumentInterface (na przykład wartość, którą przekazała ci integracja z frameworkiem), użyj zamiast tego odpowiednika na poziomie kontraktu:

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

output(dest: OutputDestination::String) jest zadeklarowane na PdfDocumentInterface i zwraca te same surowe bajty bez emitowania żadnych nagłówków. Użyj getPdfData(), gdy trzymasz konkretny NextPDF\Core\Document, a formy output(...), gdy masz tylko interfejs.

Jeśli używasz integracji Laravel lub Symfony, rozwiąż świeży dokument z kontenera, zamiast konstruować go bezpośrednio — zobacz Zwracanie wygenerowanego PDF z kontrolera po ścieżkę rozwiązywania w każdym frameworku. Wszystko poniżej działa tak samo niezależnie od tego, jak uzyskałeś dokument.

Laravel Attachment::fromData() przyjmuje wywołanie zwrotne, które zwraca surowe bajty, plus nazwę pliku. Nie ma pliku tymczasowego. Zaimplementuj attachments() na swoim Mailable i zwróć jeden 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();
}
}

Wyślij to jak zwykle:

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

Wywołanie zwrotne fromData() jest wykonywane leniwie, gdy wiadomość jest budowana, więc PDF jest generowany w czasie wysyłki, a nie konstrukcji. Ustaw ->withMime('application/pdf'), tak by klient odbiorcy traktował część jako PDF, a nie zgadywał z rozszerzenia. Wewnątrz kontrolera możesz zamiast tego wywołać $message->attachData($bytes, $name, ['mime' => 'application/pdf']) na surowej wiadomości, ale Attachment::fromData() na Mailable to idiomatyczna współczesna forma.

Symfony Mailer Email::attach() przyjmuje treść jako łańcuch znaków w pamięci, z jawną nazwą pliku i typem zawartości. Znów żadnego pliku tymczasowego.

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) przyjmuje bajty bezpośrednio. Przekaż 'application/pdf' jako trzeci argument, tak by część była otypowana poprawnie. Jeśli wolisz dołączać ze strumienia, attachFromPath() istnieje, ale dla wygenerowanej treści forma attach() w pamięci unika zbędnego obiegu na dysk.

Budowanie wielostronicowego PDF i wysyłanie poczty są oba na tyle powolne, że nie powinieneś robić ich na wątku żądania. Skolejkuj pracę. Wzorzec polega na wysłaniu zadania (lub, w Laravel, skolejkowaniu samego Mailable) i zbudowaniu PDF na workerze.

Najprostsza forma Laravel: uczyń Mailable ShouldQueue. Ponieważ wywołanie zwrotne Attachment::fromData() uruchamia się, gdy skolejkowana wiadomość jest budowana, PDF jest generowany na workerze, a nie przy wysyłce.

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

Dla Symfony wyślij wiadomość Messenger niosącą identyfikatory (nie bajty) i pozwól, by handler zbudował PDF i wysłał e-mail na workerze. Przekaż id faktury, odszukaj rekord w handlerze i wywołaj pokazany powyżej InvoiceMailer. Mailer Symfony jest już asynchroniczny, gdy transport Messenger jest skonfigurowany dla SendEmailMessage, więc nawet synchronicznie wywołane send() może zostać przetransportowane na workera.

Jeśli generujesz PDF w dedykowanym zadaniu generowania, a potem wysyłasz go mailem, zobacz Generowanie PDF w zadaniu w kolejce po powierzchnię GeneratePdfJob / GeneratePdfMessage integracji oraz jej reguły bezpieczeństwa workera. Częsty kształt to: jedno zadanie generuje i zapisuje PDF, drugie zadanie (lub wywołanie zwrotne sukcesu) odczytuje go z powrotem i wysyła mailem. Gdy trzymasz bajty w pamięci w obrębie jednego zadania, całkowicie pomijasz plik.

  • Załącznik, nie inline. Wygenerowana faktura lub raport to niemal zawsze osobny plik do pobrania, więc dołącz go jako załącznik. Rezerwuj treść inline (Content-Disposition: inline z odwołaniem cid:) dla obrazów, które osadzasz w treści HTML — PDF nie jest treścią ciała wiadomości.
  • Pilnuj rozmiaru. Załączniki e-mail są w tranzycie kodowane w base64, co zwiększa ładunek o około jedną trzecią. Wiele serwerów odbierających ogranicza wiadomość do około 10–25 MB po zakodowaniu. Dla dużego raportu dołącz krótki e-mail powiadamiający z podpisanym linkiem do pobrania zamiast samego pliku i serwuj PDF przez HTTP — zobacz Zwracanie wygenerowanego PDF z kontrolera.
  • Buduj raz, dołączaj raz. Przechwyć zwrócone bajty raz do zmiennej i wykorzystaj ten łańcuch znaków ponownie dla wiadomości. Nie wywołuj końcowej metody wyjściowej wielokrotnie dla jednego e-maila.
  • Pamięć na workerze. Trzymanie pełnego PDF w pamięci jest w porządku dla typowych faktur i paragonów. Dla bardzo dużych dokumentów na ograniczonym workerze zapisz do ścieżki tymczasowej za pomocą save(), dołącz przez Laravel attach($path) / Symfony Email::attachFromPath($path), a następnie usuń plik — wymieniając obieg na dysk na niższy szczyt pamięci. (Dla ścieżki w pamięci używanej wszędzie indziej na tej stronie formą opartą na bajtach jest Laravel Attachment::fromData() / Symfony Email::attach($bytes, 'name.pdf', 'application/pdf').)
  • Nigdy nie interpoluj niezweryfikowanego wejścia użytkownika w nazwę pliku załącznika. Przekaż wartość, którą kontrolujesz (numer faktury, który wygenerowałeś), tak by klient odbiorcy nie mógł zostać sterowany spreparowaną nazwą.
  • Wysyłaj każdemu klientowi tylko jego własny dokument. Buduj PDF z rekordów uwierzytelnionego podmiotu wewnątrz zadania, a nie z id wziętego na wiarę z żądania.
  • Na ścieżce w kolejce loguj klasę wyjątku i id korelacji przy awarii, nigdy wiadomości wyjątku ani śladu stosu. Nigdy nie pisz pustego bloku catch wokół budowy i wysyłki.