Closure yang kita pakai di chapter sebelumnya menyelesaikan email sekali kirim, tetapi email nyata dalam aplikasi selalu punya lebih banyak kepentingan: subject dinamis, data pesanan, template, sampai lampiran faktur. Menumpuk semua itu di closure controller sama dengan menaruh HTML di dalam PHP — cepat berantakan. Laravel menawarkan Mailable: satu class per jenis email yang merangkum amplop (envelope()), isi (content()), dan lampiran (attachments()) dalam tiga method terpisah. Class ini bisa diuji tanpa mengirim, dikirim dari mana saja dengan satu baris, dan di-queue otomatis. Kita bangun OrderShipped dari nol sampai terkirim, lalu membuktikan isinya lewat feature test.

Membuat Class dengan make:mail #

Perintah artisan menghasilkan kerangka siap isi:

bash
php artisan make:mail OrderShipped

Output yang Dihasilkan:

text

   INFO  Mailable [app/Mail/OrderShipped.php] created successfully.  

Struktur yang dihasilkan terdiri dari class dengan tiga method kosong plus trait Queueable dan SerializesModels — dua trait inilah yang membuat mailable siap di-queue dan aman membawa model. Kita isi ketiganya dengan contoh nyata email pengiriman pesanan:

php
namespace App\Mail;

use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;

class OrderShipped extends Mailable
{
    use Queueable, SerializesModels;

    public function __construct(
        public string $orderId,
        public string $namaPembeli,
    ) {}

    public function envelope(): Envelope
    {
        return new Envelope(
            subject: "Pesanan #{$this->orderId} Telah Dikirim",
            from: new \Illuminate\Mail\Mailables\Address(
                '[email protected]', 'Toko Latihan'),
            replyTo: [new \Illuminate\Mail\Mailables\Address('[email protected]')],
        );
    }

    public function content(): Content
    {
        return new Content(
            view: 'emails.order-shipped',
        );
    }

    public function attachments(): array
    {
        return [];
    }
}

Mari kita bedah peran tiap bagian:

  1. Constructor dengan promoted properties — $orderId dan $namaPembeli menjadi public property. Mailable otomatis mem-publish semua public property ke view, jadi template bisa langsung memakai $orderId tanpa with() eksplisit.
  2. envelope() — method ini mengembalikan objek Envelope yang membaca semua metadata pesan: subject, pengirim from, dan balasan replyTo. Kita menimpa alamat from global dengan identitas toko.
  3. content() — method ini mengembalikan objek Content yang menunjuk view emails.order-shipped sebagai badan email.
  4. attachments() — array kosong untuk sekarang; chapter Email Attachments mengisinya.

Template resources/views/emails/order-shipped.blade.php tinggal membaca property publik:

blade
Halo {{ $namaPembeli }},

Pesanan #{{ $orderId }} sudah kami kirim hari ini.
Estimasi tiba: 2-3 hari kerja.

Salam,
Toko Latihan

Mengirim dan Membuktikan Isinya #

Pengiriman mailable memakai rantai Mail::to()->send() yang sudah kita kenal:

php
use App\Mail\OrderShipped;
use Illuminate\Support\Facades\Mail;

Mail::to('[email protected]')->send(new OrderShipped('123', 'Budi'));
echo "mailable_terkirim\n";

Output yang Dihasilkan:

text
mailable_terkirim
text
[2026-10-13 19:25:41] local.DEBUG: From: Toko Latihan <[email protected]>
To: [email protected]
Reply-To: [email protected]
Subject: Pesanan #123 Telah Dikirim
MIME-Version: 1.0
Date: Tue, 13 Oct 2026 19:25:41 +0000
Message-ID: <[email protected]>
Content-Type: text/html; charset=utf-8
Content-Transfer-Encoding: quoted-printable

Halo Budi,

Pesanan #123 sudah kami kirim hari ini.
Estimasi tiba: 2-3 hari kerja.

Salam,
Toko Latihan

Log membuktikan tiga klaim sekaligus: From kini identitas toko (bukan global), Reply-To terpasang, dan subject memuat nomor pesanan dari constructor. Selain mengirim, mailable juga bisa dirender sebagai string — berguna untuk pratinjau di browser tanpa mengirim apa pun:

php
$render = new OrderShipped('456', 'Siti');
$html = $render->render();

echo 'subject: ' . $render->envelope()->subject . "\n";
echo 'view: ' . $render->content()->view . "\n";
echo 'body_baris_pertama: ' . trim(explode("\n", trim(strip_tags($html)))[0]) . "\n";

Output yang Dihasilkan:

text
subject: Pesanan #456 Telah Dikirim
view: emails.order-shipped
body_baris_pertama: Halo Siti,

Method render() menjalankan seluruh pipeline view — data constructor, envelope, template — dan mengembalikan HTML jadi. Inilah dasar dari fitur “pratinjau email” yang sering kita lihat di panel admin.

Envelope: Metadata Lanjutan #

Selain subject dan alamat, Envelope menerima tags dan metadata — data pelacak yang dikirim ke penyedia mail seperti Postmark untuk analitik per pesan:

php
$env = new Envelope(
    subject: 'Tes Envelope',
    tags: ['order', 'pengiriman'],
    metadata: ['order_id' => '123', 'kampanye' => 'retensi-maret'],
);

Runner pembuktian mencetak seluruh nilai:

text
subject: Tes Envelope
tags: ["order","pengiriman"]
metadata: {"order_id":"123","kampanye":"retensi-maret"}
from_default: NULL

Jika from tidak ditetapkan, nilainya NULL — dan pada saat pengiriman Laravel mengisinya otomatis dari mail.from global. Tags dan metadata tidak muncul di email penerima sama sekali; keduanya dilekatkan sebagai header khusus (misalnya X-PM-Tags di Postmark) yang dibaca penyedia untuk pengelompokan analitik. Karena bergantung pada penyedia, fitur ini aman diabaikan saat memakai mailer log — nilainya tetap tersimpan di objek envelope untuk inspeksi.

Satu kekuatan lain mailable yang jarang dipakai: karena class ini adalah objek biasa, kita bisa menyimpannya, meng-cloning-nya untuk penerima berbeda, atau menyuntikkan dependensi lewat constructor (misalnya service yang menghitung pajak) — selama semua data bisa diserialisasi bila mailable di-queue.

Pengujian dengan Mail::fake #

Feature test mengganti transport sungguhan dengan fake lalu memeriksa mailable yang tertangkap. Ini contoh paling awal dari pola yang akan kita pakai penuh di chapter Pengujian Email :

php
use App\Mail\OrderShipped;
use Illuminate\Support\Facades\Mail;

public function test_mailable_terkirim_dengan_subject_dan_data_yang_benar(): void
{
    Mail::fake();

    Mail::to('[email protected]')->send(new OrderShipped('123', 'Budi'));

    Mail::assertSent(OrderShipped::class, 1);
    Mail::assertSent(OrderShipped::class, function (OrderShipped $mail) {
        return $mail->envelope()->subject === 'Pesanan #123 Telah Dikirim'
            && $mail->orderId === '123'
            && $mail->namaPembeli === 'Budi'
            && $mail->hasTo('[email protected]');
    });

    fwrite(STDERR, "mailable_test: lulus (subject + data + penerima terverifikasi)\n");
}

Output yang Dihasilkan:

text
mailable_test: lulus (subject + data + penerima terverifikasi)

  PASS  Tests\Feature\MailableTest
  ✓  mailable terkirim dengan subject dan data yang benar                0.08s

  Tests:    1 passed (2 assertions)

Closure di dalam assertSent menerima instance mailable sehingga property, envelope, dan penerima bisa diperiksa satu per satu. Method hasTo() bawaan mailable memeriksa daftar penerima tanpa perlu membaca header mentah.

Mailable menuntaskan sisi logika email; yang tersisa adalah sisi visual. Chapter Email Templates membahas cara menyusun template HTML/plain-text yang rapi plus komponen markdown bawaan Laravel. Rujukan lengkap API-nya ada di dokumentasi Mail .