In a Laravel mailable, how would you attach an order's PDF receipt, and where should the PDF bytes come from when the mail is queued?
answer
- attachments() returns an array
- fromPath, fromStorage, fromStorageDisk
- fromData takes a closure and a name
- ->as('receipt.pdf')->withMime(...)
- attachments() runs when the mail is sent
basics
~20 sReturn Attachment objects from attachments(): fromStorage() or fromPath() for a saved file, fromData(fn () => $bytes, 'receipt.pdf') for generated bytes. For queued mail, pass the order, not the PDF bytes, and build the attachment in attachments().
solid answer
~40 s`attachments()` returns an array of `Illuminate\Mail\Mailables\Attachment` objects. For a receipt you already saved, `Attachment::fromStorage('receipts/1042.pdf')` reads it from the default disk and `Attachment::fromStorageDisk('s3', 'receipts/1042.pdf')` from a named disk; `fromPath()` takes a local file path. For bytes you generate, `Attachment::fromData(fn () => $pdf, 'receipt.pdf')` takes a **closure** plus a file name. Chain `->as('receipt.pdf')` and `->withMime('application/pdf')` to control the name and type. For a queued mailable, the object is serialized when queued and `attachments()` is only called in the worker, at send time. So give the constructor the `Order` (stored as an identifier by `SerializesModels`) and load or render the PDF inside `attachments()`; a public property holding raw PDF bytes is copied into the queue payload. A model can also implement `Attachable` with `toMailAttachment()`.
code
php · 28 lines<?php
namespace App\Mail;
use App\Models\Order;
use App\Support\ReceiptRenderer;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Attachment;
use Illuminate\Queue\SerializesModels;
class OrderConfirmed extends Mailable implements ShouldQueue
{
use Queueable, SerializesModels;
public function __construct(public Order $order) {}
// envelope() and content() omitted
public function attachments(): array
{
return [
Attachment::fromData(fn () => app(ReceiptRenderer::class)->render($this->order), 'receipt.pdf')
->withMime('application/pdf'),
];
}
}go deeper
Recall that attachments() returns an array of Attachment objects, and name fromPath, fromStorage and fromData along with as() for the file name.
Explain when attachments() is called, why fromData takes a closure, and how fromStorageDisk reads from a named disk such as s3.
Show you keep queue payloads small: identifiers in properties, PDFs stored or rendered in the worker, no closures serialized, and a link instead of an attachment for large files.
Weigh attachments against signed download links for receipts: deliverability and size limits, retention and access control of financial documents, and one Attachable conversion shared across mail.
## The attachments() contract A Laravel mailable declares its files in one method, `attachments()`, which returns an array. Each element is an `Illuminate\Mail\Mailables\Attachment` — or any object implementing `Illuminate\Contracts\Mail\Attachable`, which converts itself into one. Laravel calls `attachments()` while it prepares the message for delivery, just before handing it to the transport. That timing is the part candidates miss. For a mailable sent with `send()`, "prepare for delivery" happens in the request. For a queued mailable, the object is serialized first and prepared later, inside the worker. So whatever `attachments()` does — reading a file, rendering a PDF — happens **in the worker**. ## Ways to build an Attachment | Constructor | Source of the bytes | Typical use | |---|---|---| | `Attachment::fromPath($path)` | a file on the local filesystem | a generated file in `storage/app` | | `Attachment::fromStorage($path)` | the default Storage disk | receipts saved with the Storage facade | | `Attachment::fromStorageDisk($disk, $path)` | a named disk such as `s3` | receipts kept in object storage | | `Attachment::fromData(Closure $data, $name)` | whatever the closure returns | a PDF rendered on the fly | | `Attachment::fromUrl($url)` | an `http`/`https` URL | a file hosted elsewhere | Every constructor returns an `Attachment` you can refine: - `->as('receipt-1042.pdf')` sets the file name the customer sees. - `->withMime('application/pdf')` sets the MIME type explicitly instead of relying on detection. Note the signature of `fromData()`: the first argument is a **closure** that returns the bytes, not the bytes themselves. The closure runs only when the attachment is actually attached. ## Where the PDF bytes should come from when queued Suppose checkout renders a receipt PDF and queues `OrderConfirmed`. There are three shapes, and only two are good: 1. **Store, then reference.** Render the PDF once, save it with `Storage::put("receipts/{$order->id}.pdf", $pdf)`, and in `attachments()` return `Attachment::fromStorage(...)`. The payload holds only the order identifier; the receipt is also kept for later downloads. 2. **Render in the worker.** Pass only the `Order` and, in `attachments()`, return `Attachment::fromData(fn () => $renderer->render($this->order), 'receipt.pdf')`. Rendering moves off the request entirely. 3. **Avoid: bytes in a public property.** `public string $pdf` is serialized into the job payload, inflating every queued job by the size of the file and exposing it in the queue store. It is also passed to the Blade view as `$pdf`, since public properties become view data. A related mistake is building `Attachment` objects in the constructor and keeping them in a property. An `Attachment` wraps closures, and PHP refuses to serialize closures, so queueing that mailable throws. ## Attachable models If receipts are first-class records, let the model convert itself: ```php class Receipt extends Model implements Attachable { public function toMailAttachment(): Attachment { return Attachment::fromStorage($this->path)->as($this->filename); } } ``` Then `attachments()` can simply return `[$this->order->receipt]`, and any other mailable that needs the receipt reuses the same conversion. ## Inline images are a different mechanism A shop logo or product thumbnail shown *inside* the email body is not an entry in `attachments()`. In the HTML template, Laravel provides a `$message` variable, and `<img src="{{ $message->embed($pathToImage) }}">` embeds the file as an inline part referenced by a content ID; `$message->embedData($bytes, 'logo.png')` does the same for bytes in memory. The distinction matters in interviews because it changes how the recipient sees the file: - `attachments()` produces downloadable files listed by the mail client — the right place for the PDF receipt. - `embed()` produces images rendered in the body and usually not offered as downloads. - `$message` is not available in plain-text templates, since plain text cannot show inline images. ## Size and delivery considerations - Mail providers cap total message size, and base64 encoding makes attachments roughly a third larger on the wire. A multi-page PDF with embedded images can exceed a limit that plain HTML never would. - For very large or sensitive documents, a signed download link in the email body is often better than an attachment. - Assert on attachments with the framework's mail-testing helpers rather than by sending real mail. ## Summary - Return `Attachment` objects (or `Attachable` models) from `attachments()`. - `fromStorage`/`fromStorageDisk`/`fromPath` read saved files; `fromData` takes a closure and a name. - For queued mail, pass identifiers, not bytes: `attachments()` runs in the worker.
- Why does queueing a mailable fail when its constructor stores Attachment::fromPath(...) in a property?An `Attachment` wraps closures that resolve the file later, and PHP cannot serialize closures, so pushing the mailable onto the queue throws. Build attachments inside `attachments()`, which runs after the worker unserializes the mailable, and keep only serializable data such as the order in properties.
- How do you attach the same receipt from several different mailables without repeating the lookup?Make the model implement `Illuminate\Contracts\Mail\Attachable` and return an `Attachment` from `toMailAttachment()`. Each mailable's `attachments()` then returns the model itself, for example `[$this->order->receipt]`, and Laravel converts it.
saying these in an interview costs you the question
- attachments() runs when the mailable is queued, so the PDF is built in the request.
- Keeping the PDF bytes in a public property is the clean way to queue an attachment.
- Attachment::fromData() takes the raw byte string as its first argument.
- Attachment::fromStorage() can only read from the local disk.
- Only files on the local filesystem can ever be attached to a Laravel mailable.