サイトアイコン IT & ライフハックブログ|学びと実践のためのアイデア集

Laravelのリアルタイム通信実装:Broadcasting、Reverb、SSEとアクセシブルな更新設計

php elephant sticker

Photo by RealToughCandy.com on Pexels.com

LaravelのリアルタイムUIは、サーバーで起きた変化を画面へ届けるだけでは完成しません。
誰がどの更新を受け取れるか、接続が切れたときに何を表示するか、突然の変化を支援技術へどう伝えるかまで設計する必要があります。

この記事では、Laravel BroadcastingとLaravel Echoを軸に、WebSocket、Server-Sent Events(SSE)、ポーリングの使い分けを整理します。
通知、チャット、処理進捗などの画面に応用できるよう、実装、認可、再接続、アクセシビリティ、テストを一つの流れで説明します。

最初に押さえる結論

  • 双方向のやり取りやPresenceが必要ならWebSocketを検討します。
  • サーバーからブラウザーへの一方向配信ならSSEが候補になります。
  • 通信方式にかかわらず、通常のHTTP APIや更新ボタンを代替手段として残します。
  • Privateチャンネルの認可、配信データの最小化、再接続後の再同期を別々に設計します。
  • 画面更新は見える表示とライブリージョンの両方で伝え、原則としてフォーカスを移しません。

リアルタイムUIを支える四つの層

Laravelのリアルタイム通信は、次の四層に分けると責任範囲が明確になります。
通信障害と表示上の問題を同じ原因として扱わず、どの層で失敗したかを切り分けられます。

  1. アプリケーションイベント:注文作成やエクスポート完了など、サーバー側で起きた事実です。
  2. チャンネル:イベントの配信先です。公開範囲に応じてpublic、private、presenceを選びます。
  3. 通信方式:WebSocket、SSE、ポーリングなど、ブラウザーへ情報を運ぶ方法です。
  4. UI更新:件数、一覧、進捗、接続状態などを画面と支援技術へ伝える処理です。

たとえば注文通知では、注文作成をイベントにし、対象ユーザー専用のprivateチャンネルへ配信し、Laravel Echoが受信した内容を通知一覧へ反映します。
表示更新に失敗しても注文自体が失われないよう、データベースや通常のAPIを情報の正本にします。

WebSocket、SSE、ポーリングの選び方

通信方式を選ぶときの比較
方式 通信の向き 向いている例 確認事項
WebSocket 双方向 チャット、共同操作、Presence 接続認証、チャンネル認可、再接続、接続数、プロセス監視
SSE サーバーからクライアントへの一方向 通知、ログ、進捗の配信 長時間接続、プロキシーのバッファリング、認証、イベントID
ポーリング クライアントから定期取得 更新頻度が低い画面、代替経路 取得間隔、重複取得、サーバー負荷、画面非表示時の停止

SSEは、ブラウザーのEventSourceが受信する一方向のイベントストリームです。
切断後の再接続をブラウザーに任せられますが、HTTP/1.1では同一ブラウザーとドメインに対する接続数の制約があるため、タブを多く開く業務画面では事前検証が必要です。

「WebSocketが失敗したら必ずSSE、その次にポーリング」という固定順はありません。
送信も必要なチャット、受信だけでよい進捗、数分ごとの更新で足りる集計画面では、要件が異なるからです。

Laravel BroadcastingとReverbの基本構成

現行のLaravel公式ドキュメントでは、Broadcastingの選択肢としてLaravel Reverb、Pusher Channels、Ablyが案内されています。
自社でWebSocketサーバーを運用する場合は、Laravelと統合されたReverbを候補にし、運用を外部へ委ねたい場合はマネージドサービスの料金、接続上限、データの取り扱いを比較します。

旧来の記事でよく使われたBeyond CodeのLaravel WebSocketsは、リポジトリがアーカイブされ、保守終了が明記されています。
既存環境は即時停止するのではなく、対応中のLaravelとPHPの版、セキュリティー更新、移行試験を確認し、Reverbなど保守中の選択肢へ移る計画を立てます。

配信イベントは必要なデータだけを持たせる

<?php

namespace App\Events;

use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;

final class OrderCreated implements ShouldBroadcast
{
    public function __construct(
        public readonly int $userId,
        public readonly int $orderId,
    ) {}

    public function broadcastOn(): array
    {
        return [new PrivateChannel('users.'.$this->userId)];
    }

    public function broadcastAs(): string
    {
        return 'order.created';
    }

    public function broadcastWith(): array
    {
        return ['id' => $this->orderId];
    }
}

ShouldBroadcastを実装したイベントはキューで配信されるため、キューワーカーの稼働確認が必要です。
データベーストランザクション内でイベントを発火する場合は、コミット前に配信処理が進まない構成も確認します。

privateチャンネルは所有関係まで認可する

use Illuminate\Support\Facades\Broadcast;

Broadcast::channel('users.{id}', function ($user, int $id): bool {
    return (int) $user->id === $id;
});

ログイン済みであることと、対象チャンネルを購読できることは別の条件です。
ユーザー単位だけでなく、注文、案件、チャットルームなどの所有関係や参加権限をサーバー側で判定します。

Laravel EchoからReverbを購読する

import Echo from 'laravel-echo';
import Pusher from 'pusher-js';

window.Pusher = Pusher;

window.Echo = new Echo({
  broadcaster: 'reverb',
  key: import.meta.env.VITE_REVERB_APP_KEY,
  wsHost: import.meta.env.VITE_REVERB_HOST,
  wsPort: import.meta.env.VITE_REVERB_PORT ?? 80,
  wssPort: import.meta.env.VITE_REVERB_PORT ?? 443,
  forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https',
  enabledTransports: ['ws', 'wss'],
});

window.Echo.private(`users.${userId}`)
  .listen('.order.created', ({ id }) => refreshOrder(id));

環境変数名と初期化方法はLaravelとLaravel Echoの版によって変わる可能性があります。
導入時は、利用中の版に対応するLaravel Broadcasting公式ドキュメントとLaravel Reverb公式ドキュメントを確認してください。

SSEを選ぶときの実装条件

SSEは通知や処理進捗のように、サーバーからブラウザーへ送るだけの画面に適合します。
クライアントは次のようにEventSourceで受信できます。

const source = new EventSource('/stream/orders');

source.addEventListener('order', (event) => {
  const order = JSON.parse(event.data);
  renderOrder(order);
});

source.onerror = () => {
  setConnectionState('reconnecting');
};

本番では、ストリームの認証と認可、イベントIDによる重複対策、ハートビート、プロキシーのバッファリングとタイムアウトを確認します。
EventSourceの再接続だけでは、切断中に取り逃した更新や、すでに反映した更新の重複までは解決しません。

再接続後は、最後に処理したイベントIDから再開するか、通常のAPIで最新状態を取得して画面を合わせます。
イベント形式と再接続の仕様はMDNのSSE解説で確認できます。

認可、データ最小化、濫用対策

リアルタイム配信では、接続できることを許可しても、すべてのイベントを閲覧できるとは限りません。
チャンネル単位の認可に加え、イベントから受け取ったIDで詳細APIを呼ぶ場合も、API側で同じ権限を検証します。

  • publicチャンネルへ個人情報や内部識別子を流しません。
  • broadcastWith()では画面更新に必要な項目だけを返します。
  • Reverbのallowed_originsを実際の接続元に限定します。
  • メッセージ送信や通知作成のHTTPエンドポイントにレート制限を設けます。
  • 認可失敗、切断、キュー遅延、配信エラーを追跡できるログを残します。

短時間の集中を抑えるレート制限と、一定期間の利用上限であるクォータは役割が異なります。
設計の分け方はLaravelのレート制限とクォータの実務ガイドも参照してください。

切断、再接続、オフライン時の状態設計

接続状態は「オンライン」と「オフライン」の二値では足りません。
ブラウザーがネットワークへ接続していても、認証期限切れ、配信サーバー停止、プロキシー遮断などで購読できない場合があるためです。

画面に持たせる接続状態の例
状態 表示例 画面の動作
接続中 「接続しています」 初回データを通常のAPIから取得する
接続済み 常時表示は省略可能 イベントを画面へ反映する
再接続中 「接続が切れました。再接続しています」 操作を止めず、手動更新も残す
認証エラー 「再ログインが必要です」 再試行の連打を止め、ログイン導線を示す
代替表示 「ライブ更新を利用できません」 更新ボタンまたはポーリングへ切り替える

「オフラインです。変更は一時保存されます」と表示できるのは、IndexedDBなどへの保存と再送を実装し、競合時の扱いまで決めた場合だけです。
保存機能がない画面では、「送信できません。入力内容を確認して再試行してください」のように、実際の挙動だけを案内します。

再接続直後は、ライブ配信だけを再開せず、通常のAPIから最新スナップショットを取得します。
この手順により、切断中の取りこぼしを埋めたうえで、以後のイベントを処理できます。

ライブ更新をアクセシブルに伝える

画面の一部が変わっても、フォーカスを別の要素へ自動移動すると、キーボードや支援技術を使う人の操作が中断されます。
通常の更新は現在位置を保ち、別の場所で起きた変化をライブリージョンで短く伝えます。

statusとalertを使い分ける

role='status'にはaria-live='polite'とaria-atomic='true'が暗黙に設定されます。
注文の追加、保存完了、再接続中など、現在の操作を割り込ませる必要がない案内に使います。

<p id='connection-status' role='status'>接続しています</p>

role='alert'は、入力内容の消失や処理続行に関わるエラーなど、直ちに知らせる必要がある場面へ限定します。
通常の新着通知までalertにすると読み上げが現在の操作へ割り込むため、通知の優先度設計が必要です。

ライブリージョンは、更新が起きる前からDOMに置き、同じ文言を連続して設定した場合も確認します。
詳しい挙動はMDNのstatusロール解説とWCAG 2.2のステータスメッセージ解説を参照してください。

件数、履歴、進捗を見た目以外でも伝える

  • 新着バッジは色だけでなく数値と「新着3件」のような名前で伝えます。
  • チャット履歴は追記部分を短く通知し、全履歴を毎回読み上げません。
  • 進捗は色や幅だけでなく、数値と完了メッセージを表示します。
  • アニメーションを使う場合はprefers-reduced-motionに応じて抑制します。
  • 「最新の情報を取得」のような手動更新ボタンをキーボードで操作できるようにします。

達成基準全体の位置づけを確認したい場合は、WCAG 2.2の実務入門も役立ちます。

通知、チャット、進捗表示の設計パターン

通知センター

通知イベントは「新着がある」という合図にとどめ、通知本文、既読状態、有効期限はサーバー側で管理します。
再接続後に通知一覧を取得すれば、配信中にイベントを逃しても状態を復元できます。

Laravel Notificationsと既読管理を組み合わせる実装は、Laravel NotificationsとBroadcastingの解説で詳しく扱っています。

チャット

メッセージは通常のHTTPリクエストで保存し、保存成功後にイベントを配信します。
Presenceチャンネルは参加者表示に使えますが、参加権限の判定と、公開してよい利用者情報の範囲を決めてから導入します。

処理進捗

進捗イベントにはジョブIDと現在値を含め、画面ではprogress要素または適切なprogressbarを更新します。
完了時は短いステータスメッセージと次の操作を示し、ダウンロードリンクなどへフォーカスを強制移動しません。

テストと監視で確認する項目

イベントを発火できることだけでは、リアルタイム機能全体の品質を確認できません。
サーバー、通信、画面、支援技術の各層を分けて試験します。

  • 認可テスト:所有者はprivateチャンネルへ参加でき、第三者は拒否されることを確認します。
  • ペイロードテスト:必要な項目が含まれ、個人情報や不要なモデル属性が含まれないことを確認します。
  • 再接続テスト:切断中の更新を再同期でき、同じイベントを二重反映しないことを確認します。
  • UIテスト:接続状態、件数、進捗、代替ボタンがキーボード操作でも利用できることを確認します。
  • アクセシビリティテスト:自動テストでロールと名前を確認し、実際のブラウザーと支援技術で通知順と割り込み方を確認します。

DuskなどのブラウザーテストはDOMの更新や属性を検証できますが、スクリーンリーダーが期待どおり読み上げることまで保証するものではありません。
少なくとも主要なブラウザーと支援技術の組み合わせで、通常通知、連続通知、エラー、再接続を操作して確認します。

運用では、同時接続数、チャンネル認可の失敗、キュー待ち時間、配信エラー、再接続の頻度を観測します。
Reverbを複数サーバーへ拡張する場合は、Redisを介した水平スケール、ロードバランサー、プロセス管理、ファイル記述子の上限も公式文書に沿って設計します。

公開前チェックリスト

  • イベント、チャンネル、通信方式、UI更新の責任を分けたか。
  • privateまたはpresenceチャンネルで対象リソースへの権限を確認したか。
  • イベントのデータを画面更新に必要な項目へ絞ったか。
  • 再接続後に最新状態を取得し、重複と取りこぼしを処理できるか。
  • ライブ更新が使えない場合も、手動更新や通常のAPIで主要操作を続けられるか。
  • 接続状態とエラーを、色だけに頼らず短い文で示したか。
  • 通常通知にstatus、緊急通知にalertを使い分けたか。
  • 自動試験に加えて、キーボードと支援技術で確認したか。
  • 接続数、認可失敗、キュー遅延、配信エラーを監視できるか。

リアルタイム機能を壊れにくい体験にする

Laravel BroadcastingとReverbは、サーバーイベントをブラウザーへ届ける仕組みを整えます。
利用者が安心して使える画面にするには、認可、データ最小化、再接続後の同期、代替操作、ライブリージョンを同じ要件として設計します。

通信方式は機能名や流行で選ばず、通信の向き、更新頻度、運用体制、障害時に残す操作から決めます。
リアルタイム配信を情報の正本にせず、通常のAPIと保存済みデータから状態を復元できる構成が、保守しやすい実装につながります。

参考資料

リアルタイム機能は、配信できるだけでなく、切断時にも操作を続けられ、更新内容が誰にでも伝わる設計が必要です。
株式会社greedenは、Laravelを含むWebシステムの企画、実装から公開後の保守、改善まで支援しています。

モバイルバージョンを終了