Laravel NotificationsとBroadcasting入門:リアルタイム通知をアクセシブルに設計する

php elephant sticker
Photo by RealToughCandy.com on Pexels.com

Laravelの通知機能は、メールを送るためだけの仕組みではありません。

Database Notificationsで後から読める状態を残し、Broadcastingで画面へ即時反映し、ユーザーインターフェースで適切に読み上げれば、接続障害や見え方の違いに左右されにくい通知体験を作れます。

このガイドでは、コメントが届いた場面を例に、通知の保存、リアルタイム配信、既読管理、アクセシビリティ、キュー運用を一つの流れとして説明します。

  • Laravel NotificationsのDatabase、Broadcast、Mail各チャネルの役割
  • Laravel EchoでBroadcast通知を受信する方法
  • role="status"とrole="alert"の使い分け
  • 接続が切れた場合のポーリングと通知一覧による代替
  • 認可、機微情報、再試行、テストで確認する項目

通知機能を三つの層に分ける

Laravel Notificationsは、出来事をメール、データベース、Broadcastなどの配信チャネルへ変換する仕組みです。

Broadcastingは、サーバー側で起きた出来事を接続中のブラウザーへ届ける仕組みです。

通知UIは、届いた情報を一覧、未読バッジ、トースト、エラー表示として利用者へ伝える部分です。

層 主な役割 障害時の扱い
Database Notifications 通知の保存、未読と既読の管理 通知一覧から後で確認できる
Broadcasting 接続中の画面へ即時反映する 恒久的な保存先にはしない
Mail アプリの外へ通知する 受信箱で確認できる
通知UI 優先度に合った表示と読み上げを行う 通知一覧から再確認できるようにする

Broadcastだけに依存すると、ブラウザーが閉じていた時間や接続が切れた時間の通知を確認できません。

Database Notificationsを記録の基準にし、Broadcastを即時反映の手段として重ねる構成なら、リアルタイム接続がなくても通知機能の目的を保てます。

通知一覧の設計を先に確認したい場合は、Laravelで作る通知センターの実務ガイドも参照できます。

Database Notificationsを準備する

Laravel 13系では、通知テーブルのマイグレーションを生成してからデータベースへ反映します。

php artisan make:notifications-table
php artisan migrate

古いLaravelでは異なるコマンド名が使われる場合があるため、プロジェクトのメジャーバージョンに対応するLaravel公式のNotificationsドキュメントを確認してください。

通知クラスには配信チャネルと、各チャネルへ渡すデータを定義します。

php artisan make:notification CommentPosted
<?php

namespace App\Notifications;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\BroadcastMessage;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;

class CommentPosted extends Notification implements ShouldQueue
{
    use Queueable;

    public function __construct(public readonly array $payload)
    {
        $this->afterCommit();
    }

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

    public function toArray(object $notifiable): array
    {
        return [
            'kind' => 'comment.posted',
            'title' => '新しいコメント',
            'message' => $this->payload['message'] ?? '新着コメントがあります。',
            'url' => $this->payload['url'] ?? url('/'),
        ];
    }

    public function toBroadcast(object $notifiable): BroadcastMessage
    {
        return new BroadcastMessage($this->toArray($notifiable));
    }

    public function toMail(object $notifiable): MailMessage
    {
        $data = $this->toArray($notifiable);

        return (new MailMessage)
            ->subject($data['title'])
            ->line($data['message'])
            ->action('コメントを見る', $data['url']);
    }
}

toArray()の返り値はDatabaseチャネルで保存され、toBroadcast()の返り値はブラウザーへ送られます。

両方へ同じ項目を渡す場合でも、公開してよい情報だけに絞り、メールアドレス、内部メモ、権限のない利用者に見せられない本文を含めないでください。

キュー化が効く条件

ShouldQueueを実装すると、Laravelは通知の配信をキューへ送ります。

ただし、キュードライバーが同期実行のままなら、HTTP応答を軽くする効果は得られません。

非同期のキュー接続とワーカーを用意し、失敗ジョブ、再試行、処理待ち時間を監視する必要があります。

データベーストランザクション内で通知を作る場合は、保存が確定する前にワーカーが通知を処理しないよう、例のようにafterCommit()を指定します。

キューの再試行や失敗隔離まで設計する場合は、Laravelのキュー設計と非同期処理で運用項目を確認できます。

Broadcast通知をLaravel Echoで受信する

NotificationsのBroadcastチャネルを使う場合は、独自イベントを重ねなくても、Laravel Echoのnotification()で受信できます。

Echo.private(`App.Models.User.${userId}`)
    .notification((notification) => {
        renderNotification(notification);
        announceNotification(notification.message);
    });

LaravelのBroadcast通知は、通知対象モデルとIDから組み立てたプライベートチャネルへ送られます。

標準のチャネル名を変更する場合はreceivesBroadcastNotificationsOn()を使い、認可コールバックも同じ名前に合わせます。

業務イベントを複数の処理へ分配したい場合は独自のEventとListenerが適しますが、同じ通知をNotificationsと独自Eventの両方から配信すると、画面に二重表示される可能性があります。

二つの方式を使い分ける判断は、Laravelのイベント駆動設計でも整理しています。

接続が切れたときの代替手段

リアルタイム接続は即時性を高めますが、通知を受け取れること自体の前提にするわけにはいきません。

画面を再読み込みしたときにDatabase Notificationsから最新状態を復元し、必要であれば未読数を軽量なAPIで定期確認します。

const badge = document.getElementById('notification-count');
const statusRegion = document.getElementById('notification-status');
let previousCount = null;

async function pollUnreadCount() {
    try {
        const response = await fetch('/api/me/notifications/unread-count', {
            headers: { 'Accept': 'application/json' },
        });

        if (!response.ok) return;

        const { count } = await response.json();
        badge.textContent = count > 0 ? String(count) : '';

        if (previousCount !== null && count !== previousCount) {
            statusRegion.textContent = count > 0
                ? `未読の通知が${count}件あります。`
                : '未読の通知はありません。';
        }

        previousCount = count;
    } finally {
        window.setTimeout(pollUnreadCount, 30000);
    }
}

pollUnreadCount();

元の例のように毎回読み上げるのではなく、未読数が変わったときだけライブリージョンを更新します。

同じ件数を繰り返し通知しないため、スクリーンリーダー利用者の作業を周期的に遮らずに済みます。

トースト、ステータス、アラートを使い分ける

ライブリージョンは、画面の一部が更新されたことを、フォーカスを移さずに支援技術へ伝える領域です。

通知の見た目ではなく、利用者が直ちに対応する必要があるかどうかで役割を選びます。

役割 適する内容 フォーカス 自動消去
role="status" 保存完了、送信完了、新着件数など、作業を中断させない情報 移動しない 別の場所で再確認できる場合に限って検討する
role="alert" 直ちに知らせる必要があるエラー 通常は移動しない 確認できる前に消さない
role="alertdialog" 確認や選択を直ちに求める重大な状態 ダイアログ内の操作要素へ移す 利用者が判断するまで閉じない

role="status"にはaria-live="polite"が、role="alert"にはaria-live="assertive"が暗黙に設定されます。

通常の更新へassertiveを使うと読み上げ中の内容へ割り込むため、緊急性がある場合だけに限定します。

W3Cのステータスメッセージの解説も、フォーカスを受け取らずに状態変化を伝えることを求めています。

aria-liveとrole="status"の実装例は、ウェブアクセシビリティのステータスメッセージでも確認できます。

ライブリージョンは先に配置する

状態が変わるたびにライブリージョン自体を作るのではなく、空の領域をページへ配置しておき、その中のテキストを更新します。

<div id="notification-status"
     class="sr-only"
     role="status"
     aria-atomic="true"></div>

aria-atomic="true"を明示すると、更新部分だけでなくメッセージ全体を伝えたい意図が明確になります。

トーストへ自動でフォーカスを移さない

情報を知らせるだけのトーストへ自動でフォーカスを移すと、入力や読書の位置を奪います。

通常は現在のフォーカスを保ち、ライブリージョンで短い結果を伝え、見えるトーストにはキーボードで操作できる閉じるボタンを用意します。

<aside class="toast" aria-labelledby="toast-title">
    <p id="toast-title">コメントを保存しました。</p>
    <button type="button">通知を閉じる</button>
</aside>

リンクや取り消し操作を含む通知は、短時間で消えるトーストだけに置かず、通知一覧などの持続する場所にも残します。

W3Cの時間制限に関する解説では、同じ情報を別の場所から確認できる一時表示と、利用者の反応時間を失わせる表示を区別しています。

アニメーションを使う場合は、CSSのprefers-reduced-motion: reduceに応じて移動やフェードを止め、状態は色だけでなく文言と形でも示します。

通知一覧と既読処理を安全に作る

通知IDを受け取って直接更新すると、別の利用者の通知を変更できる欠陥につながります。

ログイン中の利用者が所有する通知リレーションから検索すれば、IDと所有者を同時に確認できます。

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

Route::middleware('auth')->post(
    '/notifications/{id}/read',
    [NotificationController::class, 'markAsRead']
);

public function markAsRead(Request $request, string $id)
{
    $notification = $request->user()
        ->notifications()
        ->findOrFail($id);

    $notification->markAsRead();

    return response()->noContent();
}

Webルートでは認証とCSRF保護を有効にし、通知から遷移した先でも改めて閲覧権限を確認します。

一覧には、色付きの点だけでなく「未読」「既読」という見える文言を置き、通知時刻はtime要素のdatetime属性へ機械可読な値を入れます。

<li>
    <span>{{ is_null($notification->read_at) ? '未読' : '既読' }}</span>
    <h3>{{ $notification->data['title'] ?? 'お知らせ' }}</h3>
    <p>{{ $notification->data['message'] ?? '' }}</p>
    <time datetime="{{ $notification->created_at->toISOString() }}">
        {{ $notification->created_at->diffForHumans() }}
    </time>
</li>

Bladeの{{ }}は文字列をエスケープしますが、生HTMLを許可する構文へ置き換えるとXSSの入口になります。

国際化と時刻表示を通知データから分ける

通知本文を完成済みの一文として保存すると、言語の追加や表現の修正が難しくなります。

通知の種類と表示に必要な値を保存し、画面側で翻訳辞書を使って文を組み立てる方法を検討してください。

  • 件数で表現が変わる文はtrans_choice()を使う
  • Carbonのロケールをアプリの言語設定へ合わせる
  • 相対時刻だけでなく、必要に応じて正確な日時も確認できるようにする
  • メールと画面で同じ翻訳キーを使う場合も、各チャネルに適した長さへ整える

保存時の言語で固定するか、閲覧時の言語で再構成するかは、通知を監査記録として扱うかどうかで判断が変わります。

セキュリティと運用の確認項目

対象 確認すること 理由
チャネル認可 ユーザーIDと購読者が一致するか 他人向け通知の購読を防ぐため
遷移先URL 遷移先でも閲覧権限を検査するか URLを知ることと閲覧権限は別だから
ペイロード 個人情報、内部メモ、生HTMLを含めていないか ブラウザー、ログ、外部配信基盤へ露出する範囲を抑えるため
キュー 処理待ち時間、失敗数、再試行回数を監視するか 通知の遅延と欠落を検知するため
頻度 同種通知を集約し、利用者が設定できるか 通知疲れと大量配信を抑えるため
保存期間 削除とアーカイブの基準があるか テーブル肥大化と不要な個人情報保持を避けるため

通知本文をアプリケーションログへそのまま記録せず、必要な識別子、チャネル、結果、失敗理由に絞ります。

再試行で同じ通知が複数回表示される可能性も考え、通知IDを使った重複防止を画面側に用意すると確認しやすくなります。

テストは配信と伝達を分ける

サーバーが通知を送ったことと、利用者へ理解できる形で伝わったことは別々に確認します。

テスト 主な確認内容
Feature 通知が保存され、本人だけが既読にできる
Queue トランザクション確定後に処理され、失敗時の再試行方針に従う
Broadcast 認可済みチャネルだけで受信し、ペイロードに機微情報がない
ブラウザー 新着表示、閉じる操作、通知一覧への移動をキーボードだけで実行できる
支援技術 通常通知は作業へ割り込まず、同じ未読数を繰り返し読み上げない
動き 動きを減らす設定でアニメーションが抑制される

自動テストだけでは読み上げの自然さやフォーカス移動の違和感を判定しにくいため、主要な画面はキーボードと実際のスクリーンリーダーでも確認します。

実装する順序

  1. 通知の種類、受信者、優先度、再確認できる場所を決める
  2. Database Notificationsで保存と既読管理を作る
  3. 通知一覧をキーボード操作と読み上げに対応させる
  4. キュー、再試行、監視を整える
  5. Broadcastを追加し、同じ通知一覧へ即時反映する
  6. 接続断を再現し、再読み込みとポーリングで状態が戻るか確認する
  7. 頻度制御、保存期間、利用者向け設定を運用へ組み込む

この順序なら、リアルタイム配信の準備に時間がかかっても、保存済み通知と通知一覧を先に提供できます。

公開前のチェックリスト

  • 本文内に機微情報や生HTMLを含めていない
  • プライベートチャネルの認可を他人のIDでも試した
  • 通知から移動した先でも閲覧権限を確認している
  • 通常通知へrole="alert"を使っていない
  • トースト表示だけを理由にフォーカスを移していない
  • 閉じるボタンへキーボードで到達できる
  • 色以外の文言や形で未読状態を示している
  • 一時表示の内容を通知一覧で再確認できる
  • 未読数が変わらないときは再度読み上げない
  • リアルタイム接続がなくても最新状態を復元できる
  • キューの処理待ちと失敗を監視している
  • 動きを減らす設定を反映している

通知は保存、即時性、伝わり方で設計する

Laravel NotificationsとBroadcastingを組み合わせると、通知を保存しながら、接続中の画面へ素早く反映できます。

ただし、Broadcastの成否だけでは通知体験の品質を判断できません。

接続が切れても一覧から確認できること、本人だけが購読して既読にできること、優先度に合った方法で読み上げられることまで確認して、通知機能を完成させます。

この記事に関連する株式会社greedenの取り組み

Laravelの通知機能は、配信できるだけでなく、読み上げや操作確認まで含めて品質が決まります。株式会社greedenは、Webアクセシビリティサービス「UUU」とチェックサービスを通じ、使いやすいWebサイトづくりを支援しています。

投稿者 greeden

コメントを残す

メールアドレスが公開されることはありません。 ※ が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)