Channel database menyimpan riwayat, tetapi user jarang membuka aplikasi hanya untuk memeriksa notifikasi — email tetap jalur dorong yang paling andal. Di dalam sistem Notification, versi email dibangun oleh method toMail() yang mengembalikan MailMessage: builder pesan berbasis baris teks dan tombol aksi, jauh lebih ringkas daripada menulis view Blade dari nol. MailMessage otomatis memakai template markdown bawaan Laravel sehingga hasilnya rapi tanpa satu baris CSS pun. Pada chapter ini kita bedah builder ini — subject, greeting, line, action, sampai mode error() — membuktikan render HTML-nya nyata, lalu menguji semuanya lewat Notification::fake.

Builder MailMessage #

Kita mulai dari notifikasi OrderPaid yang sudah dipakai sepanjang section ini:

php
public function toMail(object $notifiable): MailMessage
{
    $url = url('/pesanan/'.$this->orderId);

    return (new MailMessage)
        ->subject('Pembayaran Pesanan #'.$this->orderId.' Diterima')
        ->greeting('Halo '.($notifiable->name ?? 'Pelanggan').'!')
        ->line('Pembayaran sebesar Rp'.number_format($this->total).' sudah kami terima.')
        ->action('Lihat Pesanan', $url)
        ->line('Pesanan akan segera kami proses dan dikirim.');
}

Method builder menyusun pesan bertingkat:

  1. subject($judul) — menyetel judul email; tanpa ini, Laravel memakai nama class sebagai dasar judul.
  2. greeting($sapaan) — baris pembuka besar di atas isi. Parameter $notifiable memberi akses ke model penerima sehingga sapaan bisa personal; operator ?? menjaga notifikasi on-demand yang tidak punya properti name.
  3. line($teks) — paragraf isi; bisa dipanggil berulang, tiap panggilan menjadi satu paragraf.
  4. action($label, $url) — tombol aksi menonjol di tengah pesan. Argumen pertama jadi label, kedua jadi URL tujuan.

Bagaimana tampilan jadinya? MailMessage bisa dirender sebagai HTML penuh tanpa mengirim:

php
use Illuminate\Notifications\Messages\MailMessage;

$msg = (new MailMessage)
    ->subject('Pembayaran Pesanan #123 Diterima')
    ->greeting('Halo Budi!')
    ->line('Pembayaran sebesar Rp775.000 sudah kami terima.')
    ->action('Lihat Pesanan', url('/pesanan/123'))
    ->line('Terima kasih sudah berbelanja.');

$html = $msg->render();
echo 'greeting_ada: ' . var_export(str_contains($html, 'Halo Budi!'), true) . "\n";
echo 'line_1_ada: ' . var_export(str_contains($html, 'Rp775.000 sudah kami terima.'), true) . "\n";
echo 'button_label: ' . var_export(str_contains($html, 'Lihat Pesanan'), true) . "\n";
echo 'default_footer: ' . var_export(str_contains($html, 'Regards,'), true) . "\n";
echo 'level_primary: ' . var_export(str_contains($html, 'button-primary'), true) . "\n";

Output yang Dihasilkan:

text
greeting_ada: true
line_1_ada: true
button_label: true
default_footer: true
level_primary: true

Footer Regards, otomatis muncul sebagai penutup standar — berasal dari template email bawaan notifikasi (Illuminate\Notifications\Resources\views\email.blade.php) yang membungkus isi MailMessage dengan komponen x-mail::message.

Mode error untuk Kabar Buruk #

Pesan penolakan pembayaran perlu nada berbeda dari konfirmasi. Method error() mengganti tone visual — heading berubah “Whoops!” dan tombol jadi merah:

php
$msg = (new MailMessage)
    ->subject('Pembayaran Gagal')
    ->greeting('Halo Budi!')
    ->line('Kartu Anda ditolak saat memproses pesanan #123.')
    ->error()
    ->action('Perbarui Metode Pembayaran', url('/kartu'))
    ->line('Jika ini keliru, hubungi [email protected].');

echo 'level: ' . $msg->level . "\n";
echo 'action: ' . $msg->actionText . ' -> ' . $msg->actionUrl . "\n";
echo 'outro_lines: ' . count($msg->outroLines) . "\n";
$html = $msg->render();
echo 'ada_button_error: ' . var_export(str_contains($html, 'button-error'), true) . "\n";

Output yang Dihasilkan:

text
level: error
action: Perbarui Metode Pembayaran -> http://localhost:8000/kartu
outro_lines: 1
ada_button_error: true

Property level berubah dari primary default menjadi error, dan template memberi tombol class button-error berwarna merah. Line setelah action() masuk ke outroLines — paragraf penutup setelah tombol, terpisah dari introLines sebelum tombol. Property publik seperti subject, greeting, actionText, actionUrl, introLines, dan outroLines inilah yang kita periksa di pengujian.

Menimpa Alamat dengan routeNotificationForMail #

Secara default channel mail mengirim ke $notifiable->email. Model bisa mengubah aturan itu dengan method routeNotificationForMail() — misalnya aplikasi yang mengarahkan semua notifikasi ke alamat utama akun meskipun user punya beberapa email. Kita uji kedua jalurnya dengan model yang menimpa:

php
class UserCustom extends User
{
    protected $table = 'users';

    public function routeNotificationForMail($notification)
    {
        return $this->email;
    }
}

$user = UserCustom::where('email', '[email protected]')->firstOrFail();
echo 'custom_route: ' . $user->routeNotificationFor('mail') . "\n";
echo 'default_route_siti: ' . User::where('email', '[email protected]')
    ->firstOrFail()->routeNotificationFor('mail') . "\n";

Output yang Dihasilkan:

text
custom_route: [email protected]
default_route_siti: [email protected]

Bentuk kembalian method ini fleksibel: string alamat tunggal, atau array [$alamat => $nama] untuk memasang nama tampilan.

Pengujian dengan Notification::fake #

Feature test tidak perlu mengirim email sungguhan. Setelah Notification::fake(), closure assertSentTo menerima tiga argument: notifikasi, daftar channel, dan penerima — semuanya bisa diperiksa:

php
use App\Models\User;
use App\Notifications\OrderPaid;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Notification;

public function test_notifikasi_mail_dikirim_ke_penerima_yang_tepat(): void
{
    Notification::fake();

    $user = User::factory()->create([
        'name' => 'Budi',
        'email' => '[email protected]',
    ]);

    $user->notify(new OrderPaid('123', 775000));

    Notification::assertSentTo(
        $user,
        OrderPaid::class,
        function (OrderPaid $notification, array $channels, object $notifiable) {
            fwrite(STDERR, 'channels: ' . implode(', ', $channels) . "\n");

            $mail = $notification->toMail($notifiable);

            fwrite(STDERR, 'subject: ' . $mail->subject . "\n");
            fwrite(STDERR, 'greeting: ' . $mail->greeting . "\n");
            fwrite(STDERR, 'action: ' . $mail->actionText . ' -> ' . $mail->actionUrl . "\n");
            fwrite(STDERR, 'intro_lines: ' . count($mail->introLines) . "\n");

            return true;
        }
    );

    fwrite(STDERR, "mail_notification_test: lulus\n");
}

Output yang Dihasilkan:

text
channels: database, mail
subject: Pembayaran Pesanan #123 Diterima
greeting: Halo Budi!
action: Lihat Pesanan -> http://localhost:8000/pesanan/123
intro_lines: 1
mail_notification_test: lulus

  PASS  Tests\Feature\MailNotificationTest
  ✓  notifikasi mail dikirim ke penerima yang tepat                      0.15s

  Tests:    1 passed (1 assertions)

Test ini membuktikan lima hal sekaligus: channel yang aktif, subject yang terbentuk, greeting personal dari data user, tombol aksi dengan URL, dan jumlah paragraf pembuka. Karena toMail() dipanggil manual di dalam closure, kita memeriksa MailMessage yang sama persis dengan yang akan dikirim — tanpa menunggu email sampai.

Template Kustom untuk Notifikasi #

Jika builder MailMessage terasa sempit, notifikasi tetap bisa memakai view Blade sendiri — cukup kembalikan mailable dari toMail():

php
public function toMail(object $notifiable): MailMessage|Mailable
{
    return (new OrderReceipt($this->orderId, $this->items, $this->total))
        ->to($notifiable->email);
}

Kembalian Mailable langsung dikirim apa adanya, membawa seluruh kemampuan template dari chapter Email Templates — markdown komponen, tema, sampai lampiran. Pendekatan ini dipakai saat notifikasi butuh tampilan sekompleks email transaksional biasa.

Email notifikasi yang panjang bisa memakan waktu render di tengah request user. Chapter Queued Notifications menunjukkan antrean yang menjaga response tetap cepat. Rujukan API MailMessage lengkap ada di dokumentasi Notifications .