📣

Laravel BroadcastingとPusherでリアルタイムUI更新を試す

2023/12/10に公開

記事について

Laravel の Broadcasting と Pusher でリアルタイムの UI 更新をためしてみた記事です 😺
何か改善点などあればご指摘ください 🙏

Laravel Broadcasting についての説明

Laravel 10.x Broadcasting

In many modern web applications, WebSockets are used to implement realtime, live-updating user interfaces. When some data is updated on the server, a message is typically sent over a WebSocket connection to be handled by the client. This provides a more robust, efficient alternative to continually polling your application for changes.

最近の多くのウェブアプリケーションでは、リアルタイムでライブ更新されるユーザーインターフェイスを実装するために WebSocket が使用されています。サーバー上のデータが更新されると、通常、WebSocket 接続を介してメッセージが送信され、クライアントで処理されます。これは、アプリケーションの変更を継続的にポーリングするよりも、より堅牢で効率的な代替手段を提供します。

抜き出すと、↓ という感じでしょうか。

  • リアルタイムに UI を更新するために WebSocket 接続によるメッセージの送信が利用されている
    • クライアントが定期的にアプリケーションや DB を見にいくよりも効率的
  • Laravel では WebSocket 接続を介してイベントをブロードキャスティングできる

Pusher の登録

https://pusher.com/ から登録します。

Laravel 側の設定

Laravel アプリケーションの作成

今回は Laravel Sail を用いて環境を構築します。

↓ を実行するだけ。
curl -s "https://laravel.build/realtime_laravel" | bash

Provider 有効化

イベントをブロードキャストするために、config/app.phpBroadcastServiceProviderの行のコメントアウトを外します。

config/app.php
        App\Providers\AppServiceProvider::class,
        App\Providers\AuthServiceProvider::class,
        // App\Providers\BroadcastServiceProvider::class, ←これ
        App\Providers\EventServiceProvider::class,
        App\Providers\RouteServiceProvider::class,
    ])->toArray(),

Laravel に Pusher ライブラリをインストール

ブロードキャストドライバに Pusher を使用するため、Pusher ライブラリをインストールします。
composer require pusher/pusher-php-server

.env に Pusher の情報を記載する

Pusher に記載されている情報を.envに追記します。

PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_APP_CLUSTER=

また、ブロードキャストドライバもlogpusherに変更しておきます。
BROADCAST_DRIVER=pusher

Event の作成

php artisan make:event MyEventでイベントクラスを作成します。

出来上がったapp/Events/MyEvent.phpを以下のように編集します。

app/config/MyEvent.php
<?php

namespace App\Events;

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

class MyEvent implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public $message;

    public function __construct($message)
    {
        $this->message = $message;
    }

    public function broadcastOn()
    {
        return ['my-channel'];
    }

    public function broadcastAs()
    {
        return 'my-event';
    }
}

イベントクラスではShouldBroadcastインターフェースを実装し、broadcastOnメソッドも定義する必要があります。

boradcastAsでは、ブロードキャストの名前をmy-eventという名前にしています。
デフォルトだと、ブロードキャスト名はクラス名になるようです。

クライアント側の設定

View の作成

今回はお試しなので、welcome.blade.phpを改造します。

resources/views/welcome.blade.php
<!DOCTYPE html>

<head>
    <title>Pusher Test</title>
    <script src="https://js.pusher.com/8.2.0/pusher.min.js"></script>
    <script>

        // Enable pusher logging - don't include this in production
        Pusher.logToConsole = true;

        var pusher = new Pusher("{{ $key }}", {
            cluster: "{{ $cluster }}"
        });

        var channel = pusher.subscribe('my-channel');
        channel.bind('my-event', function (data) {
            console.log(JSON.stringify(data));
        });
    </script>
</head>

<body>
    <h1>Pusher Test</h1>
    <p>
        Try publishing an event to channel <code>my-channel</code>
        with event name <code>my-event</code>.
    </p>
</body>

↓ だけのシンプルな画面です。
クライアント側の画面

Hello Wolrd を送ってみるテスト

ルーティングを設定します。
今回は、http://localhost/testにリクエストを送ったら、イベントがブロードキャストされるようにしています。

routs/web.php
<?php

use Illuminate\Support\Facades\Route;
use \App\Events\MyEvent;

Route::get('/', function () {
    return view('welcome', ['key' => env('PUSHER_APP_KEY'), 'cluster' => env('PUSHER_APP_CLUSTER')]);
});

Route::get('/test', function () {
    event(new MyEvent('Hello World'));
});


Hello Worldのテスト

コンソールに「Hello World」が表示されました 😼

受け取ったものを画面に表示させる

ちょっと受け取ったものを画面に表示させてみましょう!

resources/views/welcome.blade.php
channel.bind('my-event', function (data) {
    document.getElementById('message').innerHTML = data.message;
});

<body>
    <h1>Pusher Test</h1>
    <div id="message"></div>
</body>

Hello Worldを表示させてみる

表示されました!

DB が更新されたら UI も更新されるように

今度は、DB に変更があったときにリアルタイムに UI を更新してみます。

以下のようなテーブル・データを作成しました。
テーブル

現在の在庫(stock_quantity)を表示しておき、在庫数に変更があったら UI を更新するのが目標です 😺

モデルは ↓ です。

app/Models/Product.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    use HasFactory;
}

Model の修正

Model の更新があったらブロードキャストするために、Illuminate\Database\Eloquent\BroadcastsEventsトレイトのbroadcastOn()を実装する必要があります。

app/Models/Product.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    use HasFactory;

    public function broadcastOn(string $event): array
    {
        return ['products'];
    }
}

DB を更新する処理を追加

routes/web.phptestを改造しました。
Product テーブルの id が 3 のレコードのstock_quantityを+1 しています。
保存後、productsイベントを dispatch しています。

getEventDispatcherは Model が利用しているHasEventsトレイトで定義されているようです 🧐

routs/web.php
Route::get('/test', function () {
    $product = Product::find(3);
    $product->increment('stock_quantity');
    $product->save();
    Product::getEventDispatcher()->dispatch('products');
});

クライアント側の修正

ひとまず、受け取った内容を console に出力してみます。

resources/views/welcome.blade.php
const channel = pusher.subscribe('products');
  channel.bind('ProductUpdated', function (data) {
    console.log("在庫" + data.model.stock_quantity);
});

ログに出力してみる

ID が 3 の店長の在庫数に+1 した値がログに表示されました!
(色々ためしていたらいつの間にかだいぶ増えてました)

ログに出力

受け取った値で UI を更新してみる

View を ↓ のような形にします。

resouces/views/welcome.blade.php
<!DOCTYPE html>

<head>
    <title>Pusher Test</title>
    <script src="https://js.pusher.com/8.2.0/pusher.min.js"></script>
    <script>

        // Enable pusher logging - don't include this in production
        Pusher.logToConsole = true;

        const pusher = new Pusher("{{ $key }}", {
            cluster: "{{ $cluster }}"
        });

        const channel = pusher.subscribe('products');
        channel.bind('ProductUpdated', function (data) {
            const productId = data.model.id;
            const stockQuantity = data.model.stock_quantity;

            document.getElementById(`stock_quantity_${productId}`).innerText = stockQuantity;
        });
    </script>
</head>

<body>
    <h1>Pusher Test</h1>
    <table>
        <thead>
            <tr>
                <th>商品名</th>
                <th>在庫数</th>
            </tr>
        </thead>
        <tbody>
            @foreach ($products as $product)
            <tr>
                <td>{{$product->product_name}}</td>
                <td id="stock_quantity_{{ $product->id }}">{{$product->stock_quantity}}</td>
            </tr>
            @endforeach
        </tbody>
    </table>
</body>

店長の数が 109→110 に更新されました!

おわり

今回は、Laravel のブロードキャスト・Pusher を使用してリアルタイムの UI 更新をためしてみました。

まだまだ理解できていない部分も多いですが、↓ という認識です 🧐

  • クライアント:Pusher ライブラリを使用してproductsチャンネルを購読、productUpdatedイベントがブロードキャストされると、指定した処理を実行する(今回だと受け取った在庫数で更新する)
  • Laravel:イベント(productUpdated)を発火させて、WebSocket サーバー(Pusher)にブロードキャストを依頼する
  • Pusher:Laravel から依頼があればブロードキャストして、チャンネルを購読しているクライアントにデータを送る

ドキュメントを読んでいると、認証が必要なプライベートチャンネルや、Pusher 以外のブロードキャストドライバ(Ably や Laravel WebSocket など)もあるようなので、色々ためしてみるのもいいかもしれません…!

https://github.com/Masato0405/realtime_laravel

GitHubで編集を提案

Discussion