Notifications
A notification is one message about one event that can leave over several channels at once, mail and the database included.
php artisan make:notification InvoicePaid --markdown=mail.invoices.paidA 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
<?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.
<?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.
<?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.
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