Notifications

A notification is one message about one event that can leave over several channels at once, mail and the database included.

Syntaxphp artisan make:notification InvoicePaid --markdown=mail.invoices.paid

A notification is a short message about something that happened, written once and delivered over one or more channels: mail, the database, broadcast, Slack, SMS. A mailable is an email. A notification is the event, and mail is one way out of it.

php artisan make:notification InvoicePaid

The class lands in app/Notifications and extends Illuminate\Notifications\Notification. via(object $notifiable): array returns the channel names. It is a method, not a property, so it can decide per recipient. Each channel then reads one method: the mail channel calls toMail(), which returns a MailMessage built from subject(), greeting(), line(), lineIf(), action(); the database channel calls toDatabase(), or toArray() if there is no toDatabase().

What makes $user->notify() work

The Illuminate\Notifications\Notifiable trait, on the User model. It supplies notify(), which resolves the notification dispatcher from the container, and the notifications() relation the database channel writes through. $user->notify($n) and Notification::send($user, $n) are the same operation; the facade form also accepts an array or a collection of notifiables. Which address the mail channel uses is routeNotificationFor('mail'), which returns $notifiable->email unless the model defines routeNotificationForMail().

The database channel

It writes one row per recipient, so the table has to exist:

php artisan make:notifications-table
php artisan migrate

One event, two deliveries: return ['mail', 'database'] from via() and both channels run against the same notification instance.

Example

Example · php
<?php
// app/Notifications/InvoicePaid.php

declare(strict_types=1);

namespace App\Notifications;

use App\Models\Invoice;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;

class InvoicePaid extends Notification implements ShouldQueue
{
    use Queueable;

    public function __construct(
        protected Invoice $invoice,
    ) {}

    /**
     * @return array<int, string>
     */
    public function via(object $notifiable): array
    {
        // Per-recipient: an in-app record always, email only if they want it.
        return $notifiable->wants_email
            ? ['mail', 'database']
            : ['database'];
    }

    public function toMail(object $notifiable): MailMessage
    {
        return (new MailMessage)
            ->subject('Invoice '.$this->invoice->number.' is paid')
            ->greeting('Hello '.$notifiable->name.',')
            ->line('We received payment for invoice '.$this->invoice->number.'.')
            ->lineIf($this->invoice->credit_cents > 0, 'A credit was applied.')
            ->action('View invoice', route('invoices.show', $this->invoice))
            ->line('No action is needed.');
    }

    /**
     * The payload the database channel stores in the data column.
     *
     * @return array<string, mixed>
     */
    public function toArray(object $notifiable): array
    {
        return [
            'invoice_id' => $this->invoice->id,
            'number' => $this->invoice->number,
            'amount_cents' => $this->invoice->amount_cents,
        ];
    }
}

When to use it

  • Tell a customer an invoice was paid by email and leave an in-app record at the same time.
  • Let each user choose email, in-app, or both, without duplicating the message.
  • Send an alert to a plain address that has no model behind it.
  • Store a feed of account activity that a bell menu can read.

More examples

The notifiable side

Any model can be notifiable, not only User: a Team, a Server, an Account. The trait is the whole requirement for notify(), and a routeNotificationForMail-style method, one per channel, is the hook for every address decision. Returning an array maps address to display name.

Example · php
<?php
// app/Models/User.php

declare(strict_types=1);

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Notifications\Notification;

class User extends Authenticatable
{
    use Notifiable;

    /**
     * Without this method the mail channel uses $user->email.
     * With it, billing mail goes to the billing contact instead.
     */
    public function routeNotificationForMail(Notification $notification): array|string
    {
        if ($notification instanceof \App\Notifications\InvoicePaid) {
            return [$this->billing_email => $this->name];
        }

        return $this->email;
    }
}

The table the database channel needs

One table holds every notification for every notifiable type: morphs() writes notifiable_type and notifiable_id plus an index on the pair. type is the notification class name, data is the JSON from toArray(), and read_at is null until the recipient reads it. Run the generator once per application, not once per notification.

Example · php
<?php
// database/migrations/xxxx_xx_xx_xxxxxx_create_notifications_table.php
// Written by: php artisan make:notifications-table

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('notifications', function (Blueprint $table) {
            $table->uuid('id')->primary();
            $table->string('type');
            $table->morphs('notifiable');
            $table->text('data');
            $table->timestamp('read_at')->nullable();
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('notifications');
    }
};

Three ways to send the same notification

Notification::send() loops the notifiables for you and, because the class implements ShouldQueue, pushes one job per notifiable per channel. notifyNow() (or Notification::sendNow()) bypasses the queue, which is what you want in a command whose whole job is to send and then exit.

Example · php
use App\Notifications\InvoicePaid;
use Illuminate\Support\Facades\Notification;

// One notifiable, via the trait.
$user->notify(new InvoicePaid($invoice));

// Many notifiables, via the facade. Same dispatcher underneath.
Notification::send($team->members, new InvoicePaid($invoice));

// Nobody in the database: route the channels by hand.
Notification::route('mail', '[email protected]')
    ->notify(new InvoicePaid($invoice));

// Ignore the queue for this one and send in-process.
$user->notifyNow(new InvoicePaid($invoice));

Discussion

  • Be the first to comment on this lesson.