More Channels

Reading stored notifications, what the broadcast and Slack channels need, and how a channel of your own is a class with send().

Syntaxpublic function via(object $notifiable): array { return ['database', VoiceChannel::class]; }

Reading database notifications

The Notifiable trait adds three relations: $user->notifications (newest first), $user->unreadNotifications and $user->readNotifications. Each row is an Illuminate\Notifications\DatabaseNotification model. Its data column is text in the database but cast to array on the model, so $notification->data['invoice_id'] works and no json_decode is needed. type holds the notification class name, which is what a view switches on to pick a line of copy. $notification->markAsRead() sets read_at.

Broadcast and Slack

The broadcast channel calls toBroadcast(), which returns a BroadcastMessage, and fires BroadcastNotificationCreated on a private channel named after the notifiable: App.Models.User.1. It therefore needs a broadcaster configured; without one the channel is the only part that is ready. Slack is a separate package, composer require laravel/slack-notification-channel, reads its token and default channel from services.slack.notifications in config/services.php, and calls toSlack() for a SlackMessage.

A channel of your own

A channel is a class with one method, send($notifiable, Notification $notification). Name the class itself in via() and it is used. The mechanism is ChannelManager: a name with no built-in create<Name>Driver method falls through to class_exists() and is then resolved out of the container, which is why a custom channel gets constructor injection for free.

Example

Example · php
<?php
// app/Notifications/Channels/VoiceChannel.php

declare(strict_types=1);

namespace App\Notifications\Channels;

use App\Services\VoiceGateway;
use Illuminate\Notifications\Notification;

class VoiceChannel
{
    /**
     * Resolved from the container, so dependencies are injected.
     */
    public function __construct(
        private readonly VoiceGateway $gateway,
    ) {}

    public function send(object $notifiable, Notification $notification): void
    {
        // routeNotificationFor() returns null unless the notifiable
        // defines routeNotificationForVoice(). Nothing to call: stop.
        $number = $notifiable->routeNotificationFor('voice', $notification);

        if (! $number || ! method_exists($notification, 'toVoice')) {
            return;
        }

        $this->gateway->call($number, $notification->toVoice($notifiable));
    }
}

When to use it

  • Render a bell menu with an unread count from the notifications table.
  • Update an open page the moment a long job finishes, over the broadcast channel.
  • Post deployment failures into Slack from the notification that already emails the owner.
  • Deliver to an internal paging gateway that has no community package.

More examples

Reading and clearing the bell menu

Count the unread with a query (->unreadNotifications()->count()) rather than loading the relation and counting in PHP. The collection form of markAsRead() exists because the relation returns a DatabaseNotificationCollection; it is fine for a handful of rows and wasteful for thousands, which is when the update() form earns its place.

Example · php
// app/Http/Controllers/NotificationController.php
public function index(Request $request): View
{
    $user = $request->user();

    return view('notifications.index', [
        'unreadCount' => $user->unreadNotifications()->count(),
        'items' => $user->notifications()
            ->latest()
            ->paginate(20),
    ]);
}

public function markAllRead(Request $request): RedirectResponse
{
    // Loads the models, then saves each one. Fires model events.
    $request->user()->unreadNotifications->markAsRead();

    // Same outcome in one query, no models, no events:
    // $request->user()->unreadNotifications()->update(['read_at' => now()]);

    return back();
}

The same notification on three channels

Reach for broadcast when the user is already looking at the page and should not have to refresh, and for Slack when the audience is your own team. Both read the same constructor data as toMail() and toArray(), so adding a channel is a new method rather than a new notification.

Example · php
use Illuminate\Notifications\Messages\BroadcastMessage;
use Illuminate\Notifications\Slack\BlockKit\Blocks\SectionBlock;
use Illuminate\Notifications\Slack\SlackMessage;

public function via(object $notifiable): array
{
    return ['database', 'broadcast', 'slack'];
}

public function toBroadcast(object $notifiable): BroadcastMessage
{
    // Delivered on the private channel App.Models.User.{id}
    return new BroadcastMessage([
        'invoice_id' => $this->invoice->id,
        'number' => $this->invoice->number,
    ]);
}

public function toSlack(object $notifiable): SlackMessage
{
    return (new SlackMessage)
        ->text('Invoice '.$this->invoice->number.' was paid.')
        ->headerBlock('Invoice paid')
        ->sectionBlock(function (SectionBlock $block): void {
            $block->field('*Invoice:*'.PHP_EOL.$this->invoice->number)->markdown();
        });
}

// On the notifiable, to override the default Slack destination:
public function routeNotificationForSlack(Notification $notification): mixed
{
    return '#billing';
}

Wiring the custom channel up

Three pieces and no registration step: the channel class, a toVoice() method on the notification, and a routeNotificationForVoice() method on the notifiable. If you prefer a short name in via() instead of a class name, register it once with Notification::extend('voice', fn ($app) => $app->make(VoiceChannel::class)) in a service provider.

Example · php
// app/Notifications/ServerDown.php
use App\Notifications\Channels\VoiceChannel;

public function via(object $notifiable): array
{
    // A class name is a valid channel: ChannelManager resolves it.
    return ['mail', VoiceChannel::class];
}

public function toVoice(object $notifiable): string
{
    return 'Server '.$this->server->hostname.' stopped responding.';
}

// app/Models/OnCallEngineer.php
use Illuminate\Notifications\Notifiable;

class OnCallEngineer extends Model
{
    use Notifiable;

    public function routeNotificationForVoice(Notification $notification): ?string
    {
        return $this->phone_e164;
    }
}

Discussion

  • Be the first to comment on this lesson.