Laravelで作るリアルタイムWebソケットアプリ:Reverb+Broadcasting実装パターン

Laravelで作るリアルタイムWebソケットアプリ:Reverb+Broadcasting実装パターン

概要:WebSocketで「ページを再読み込みせずに更新される体験」をLaravelで組み立てる手順を、イベント設計・認可・購読・キュー運用・本番デプロイまで含めてまとめます。Reverb(Laravel公式系のWebSocketサーバ)を軸に、Broadcastingとフロント購読(Laravel Echo相当)を接続し、チャット/通知/管理画面のライブ更新を1つの構成で回せる形に落とします。

リアルタイム化で得られる要件

・チャット、在庫変動、審査ステータス、営業進捗などの即時反映
・通知(管理者/担当者/ユーザー)をPushで届ける
・複数オペレーターが同じ画面を見ている前提で同期する(管理画面)
・ポーリング(数秒ごとのAPI叩き)を減らして負荷を下げる

全体アーキテクチャ(HTTP+Queue+WebSocket)

Client (Browser)
  ├─ HTTP: 認証/初期表示/操作
  └─ WS : subscribe(channels) -> receive(events)

Laravel App
  ├─ Event: implements ShouldBroadcast
  ├─ Channel Auth: routes/channels.php
  ├─ Queue Worker: broadcast jobs(構成次第)
  └─ Reverb Server: WS接続を保持し配信

準備:Broadcastingの基本設定

環境変数と接続先を揃え、WSサーバとアプリの“配信先”を一致させます。

# .env 例(名前は環境に合わせて)
BROADCAST_CONNECTION=reverb
QUEUE_CONNECTION=redis

# Reverb系(例)
REVERB_APP_ID=local
REVERB_APP_KEY=localkey
REVERB_APP_SECRET=localsecret
REVERB_HOST=127.0.0.1
REVERB_PORT=8080
REVERB_SCHEME=http

# フロント側が参照するWS(例)
VITE_REVERB_HOST=127.0.0.1
VITE_REVERB_PORT=8080
VITE_REVERB_SCHEME=http

WSサーバ起動と疎通確認

ローカルではWSサーバ起動→ブラウザ購読→イベント送信の順に確認します。

# 例:WSサーバ起動(コマンドは環境のReverb導入方法に合わせる)
php artisan reverb:start

# 例:キューワーカー(配信をキューに流す場合)
php artisan queue:work

イベント設計:ShouldBroadcastで配信する

“何が起きたか”をイベントに閉じ込め、payload(配信内容)とchannel(配信先)を固定します。

namespace App\Events;

use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class OrderStatusUpdated implements ShouldBroadcast
{
    use Dispatchable, SerializesModels;

    public function __construct(
        public int $orderId,
        public string $status,
        public int $updatedByUserId
    ) {}

    public function broadcastOn(): Channel
    {
        // 注文IDごとのプライベートチャンネル
        return new PrivateChannel("orders.{$this->orderId}");
    }

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

    public function broadcastWith(): array
    {
        return [
            'order_id' => $this->orderId,
            'status' => $this->status,
            'updated_by' => $this->updatedByUserId,
        ];
    }
}

エラーの発生条件:broadcastOnがPublicなのに秘匿情報を載せる

PublicChannelは誰でも購読できる前提で運用されます。ユーザーIDや内部ステータス、金額などを載せたpayloadをPublicで飛ばすと情報漏えいになります。
対策:Private/Presenceに寄せ、channel認可で購読者を限定し、payloadを最小化します。

チャンネル認可:routes/channels.php

PrivateChannel/PresenceChannelはここで購読可否を決めます。

use App\Models\Order;
use Illuminate\Support\Facades\Broadcast;

Broadcast::channel('orders.{orderId}', function ($user, $orderId) {
    $order = Order::find($orderId);
    if (!$order) return false;

    // 例:注文の所有者 or 管理者のみ購読OK
    return $order->user_id === $user->id || $user->can('manage-orders');
});

送信タイミング:更新処理の直後にdispatch

ドメイン更新 → 監査ログ → WS配信 の順で一貫させると追跡しやすくなります。

use App\Events\OrderStatusUpdated;

DB::transaction(function () use ($order, $newStatus) {
    $order->update(['status' => $newStatus]);

    event(new OrderStatusUpdated(
        orderId: $order->id,
        status: $order->status,
        updatedByUserId: auth()->id()
    ));
});

フロント購読:Laravel Echo相当で受信する

Echo互換の購読クライアント(Vite+JS)でPrivateChannelを購読し、イベント名でハンドリングします。

import Echo from 'laravel-echo';

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

window.Echo.private(`orders.${orderId}`)
  .listen('.order.status.updated', (e) => {
    // e: { order_id, status, updated_by }
    renderStatus(e.status);
  });

Presenceで“誰が見ているか”を出す

共同編集や管理画面で「閲覧中ユーザー一覧」を出す場合にPresenceが便利です。

# サーバ側(例)
use Illuminate\Support\Facades\Broadcast;

Broadcast::channel('room.{roomId}', function ($user, $roomId) {
    return ['id' => $user->id, 'name' => $user->name];
});

# クライアント側(例)
window.Echo.join(`room.${roomId}`)
  .here((users) => renderUsers(users))
  .joining((user) => addUser(user))
  .leaving((user) => removeUser(user));

キュー運用:配信を同期にしない

配信を同期処理にすると、WS接続状況や瞬間負荷でHTTPレスポンスが遅くなります。
運用方針:イベント配信はキューに寄せ、ワーカー数とRedisの監視を前提にします。

本番デプロイ:リバースプロキシとWSS

・TLS終端(Nginx/ALB等)でWSSを必須にする
・WebSocketのUpgradeヘッダを通す
・スケール時はWSサーバのセッション保持方式と水平分散(Sticky/共有PubSub)を検討
・CORS/CSRF、認証クッキーのSameSite設定を合わせる

障害対応:よくある詰まりポイント

・購読はできるがイベントが来ない:QUEUE未起動、broadcast driver不一致、イベントがShouldBroadcast未実装
・403で購読できない:channels.phpの認可NG、認証ガード不一致、CSRF/セッション設定
・ローカルはOKで本番NG:WSS未対応、プロキシがUpgradeを遮断、ポート閉鎖

まとめ:Reverb+Broadcastingで“Laravelだけ”に寄せる

・イベントにpayloadと配信先を閉じ込める
・Private/Presenceで認可し、Publicに秘匿を載せない
・配信はキュー前提で遅延・再試行・監視を組み込む
・本番はWSSとプロキシ設定が最重要