Laravel Broadcasting (Laravel Echo)を試してみる


Laravel Broadcastingを試してみます。とりあえずMacのローカルで試してみます。

## やりたいこと

* 全体配信（接続しているクライアント全体にメッセージを送る）
* 個別配信（特定のクライアントにメッセージを送る）
  * 配信はuser id等で指定したい
  
という感じです。

>素のSocket.ioではユーザーと通信の紐づけを自分で管理しないといけないのでLaravelのユーザー管理機能と連携したい。

## 最初に知っておいた方がいいこと

### PrivateChannelの配信先は認証済みユーザーであること

個別配信（PrivateChannel）の利用はLaravelで認証済みのユーザーが前提のようです。最初、それを理解しておらずしばらくハマりました。また、標準ではweb middlewareが適用されており、API経由で利用（認証）するにはmiddlewareの変更等も必要です。

### SocketサーバはPusherとLaravel-Echo-Serverが利用できる

その他も利用できますが、更新サンプルなどはPusherの利用前提で書かれていたりするので随時読み替えが必要です。
Pusherを利用した記事は[こちら](https://qiita.com/zaburo/private/a0ec6d8eb786b742866d)をどうぞ。

### 各種ツールの相関関係

下記のような感じ。

![スクリーンショット 2018-11-11 11.39.37.png](https://qiita-image-store.s3.amazonaws.com/0/55188/58aef309-d40c-9a10-689f-0ae3fea07437.png)

## 作るもの

consoleに購読したチャネルの内容が表示されるだけのもの。

![スクリーンショット 2018-11-17 6.23.17.png](https://qiita-image-store.s3.amazonaws.com/0/55188/b0155d05-243f-8e73-8532-d168942fe241.png)


## 準備

まず、必要なものをインストールしておきます。

### Laravel

何は無くてもLaravel。

```bash
composer create-project laravel/laravel echo
cd echo
```

### 関連ライブラリ

次に、ライブラリ群をインストールします。jsとphp用。

```bash
npm install --save laravel-echo socket.io-client
composer require predis/predis
```

### Redis

Laravel-Echo-Serverを利用する場合、Redisが必須となります。
インストールがまだであれば、[こちら](https://qiita.com/zaburo/items/37cb9d5d40a86422716d)を参考にインストールしてください。

インストール後、redisを起動させておきます。

```bash
redis-server
```

### Laravel-Echo-Server

Laravel−Echo-Serverをインストールします。

```bash
npm install -g laravel-echo-server
```

インストールしたら設定（ファイルの定義）を行います。initコマンドで対話形式で設定できます。

```bash
laravel-echo-server init
```

今回は下記のような感じで返答しました。serverの位置（IPやポート）などは環境に応じて適切に設定してください。

```bash
? Do you want to run this server in development mode? Yes
? Which port would you like to serve from? 6001
? Which database would you like to use to store presence channel members? redis
? Enter the host of your Laravel authentication server. http://localhost:8000
? Will you be serving on http or https? http
? Do you want to generate a client ID/Key for HTTP API? Yes
? Do you want to setup cross domain access to the API? Yes
? Specify the URI that may access the API: http://localhost:8000
? Enter the HTTP methods that are allowed for CORS: GET, POST
? Enter the HTTP headers that are allowed for CORS: Origin, Content-Type, X-Auth-Token, X-Requested-With, Accept, Authorizatio
n, X-CSRF-TOKEN, X-Socket-Id
appId: 15efabbfxxxxxxxx
key: 366fc3b105d867a3990b5b57xxxxxxxx
```

>なお、設定はlaravel-echo-server.jsonというファイルの記録されるようです。ソースの管理の都合上、Laravelのプロジェクトファイル内に保存したほうがいい感じです。

設定が終わったら起動します。

```bash
laravel-echo-server start

L A R A V E L  E C H O  S E R V E R

version 1.4.2

⚠ Starting server in DEV mode...

✔  Running at localhost on port 6001
✔  Channels are ready.
✔  Listening for http events...
✔  Listening for redis events...

Server ready!
```

## Laravel側の準備

具体的な実装を行う前にLaravelの設定を行います。

### config/app.php（抜粋）

config/app.phpの下記行がコメントアウトされているので、コメントインします。

```php
.....
App\Providers\BroadcastServiceProvider::class,
.....
```

### .env（抜粋）

続いて.envの設定をします。BROADCAST_DRIVERをlogからredisに変更します。
なお、後々DBも利用するので、DBの設定も済ましておきましょう（migrateできるよう）。

```bash
.....
BROADCAST_DRIVER=redis
.....
```

### 実装１：Eventの追加（Channel：全員と通信する）

まずは全体配信（通信）の機能を実装してみます。
通信はEventを介して行われるので、Eventを作成します。

```bash
php artisan make:event PublicEvent
```

では、ファイルを下記のようにします。やってることは、

* ShouldBroadcastをimplements
* public-eventというChannelを定義
* messageという名称でPUBLICという文字列を配信

という感じ。

```diff
<?php

namespace App\Events;

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

+class PublicEvent implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public function __construct()
    {
        //
    }

    public function broadcastOn()
    {
+        return new Channel('public-event');
    }

+    public function broadcastWith()
+    {
+        return [
+            'message' => 'PUBLIC',
+        ];
+    }
}
```

Eventを発火させる機能を作ります。今回は特定のURLにアクセスしたら発火するようにしました。
真っ白な画面になるので、いちおうpublicとだけ表示させておきます（無くてもいいです）。

```php
Route::get('/public-event', function(){
    broadcast(new \App\Events\PublicEvent);
    return 'public';
});
```

### クライアント側機能の実装

#### 標準JS環境をVueからReactに変更する（必須ではない）

私はReactを利用しるので、プリセット環境をVueからReactに切り替えておきます。

```bash
php artisan preset react
```

#### bootstrap.js（抜粋）への記述

続いてクライアント側機能の実装を行います。
今回はブラウザのコンソールにメッセージを表示させるだけなので、resources/js/bootstrap.jsの最後に下記のコードを書きます。


```js:bootstrap.js
//for Echo
import Echo from 'laravel-echo';
window.io = require('socket.io-client');

//接続情報
window.Echo = new Echo({
    broadcaster: 'socket.io',
    host: 'http://localhost:6001',
});

//購読するチャネルの設定
window.Echo.channel('public-event')
    .listen('PublicEvent', (e) => {
        console.log(e);
    });
```

#### ソースのコンパイル

JSの記述が反映されるようコンパイルします。

```bash
npm install
npm run dev
```

### welcome.blade.php

メッセージの配信を確認するための記述をwelcome.blade.phpに対して行います。
コンソールに出力するため、HTML等の編集はほとんどありませんが、JSを実行するために２つの記述を追加します。

#### 1. csrf_tokenのための記述

下記をheadのmetaタグに追加します。

```diff
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
+<meta name="csrf-token" content="{{ csrf_token() }}">
```

#### 2. コンパイルしたJSの実行

書きを\</body>の手前にでも挿入しておきます。

```diff
+<script src="{{ asset('js/app.js')}}"></script>
</body>
```

### 動作確認

では、Laravelを起動して動作を確認してみます。

#### Laravelの起動

```bash
php artisan serve
```

#### 確認画面を開く

/（webcome.blade.php)にアクセスして(Chromeの）デバッグコンソールを開きます。

#### Eventの発火

/public-eventにアクセスしてEventを発火させ、welcome.blade.phpのコンソールにPUBLICと表示されるのを確認します。

## 実装２：Eventの追加（PrivateChannel：ユーザーを指定して通信する）

続いて特定のユーザーに対して通信を行う方法(PrivateChannel)を利用してみます。
Eventを作成します。

```bash
php artisan make:event PrivateEvent
```

実装します。基本的にはPublicEventと同じですが、チャネルの指定をPrivateChannel()として行っているところが違います。
メッセージとしてPRIVATEを送るようにします。

### PrivateEvent

```diff:PrivateEvent.php
<?php

namespace App\Events;

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

+class PrivateEvent implements ShoudBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public function __construct()
    {
        //
    }

    public function broadcastOn()
    {
+        return new PrivateChannel('private-event');
    }

    public function broadcastWith()
    {
        return [
+            'message' => 'PRIVATE',
        ];
    }
}
```

### routes/channel.php

なお、PrivateChannelでは、channel.phpにおいて認証？処理を行う必要があります。
特定のchannelのパターン毎にロジックを定義します。

trueかfalseをreturnするのですが、一旦全てOKというこで、trueを返しておきます。

>応用は後ほどやります。

```php
Broadcast::channel('private-event', function () {
    return true;
});
```

### ルートの追加

PrivateEventを発火するためのルートを定義します。

```php
Route::get('/private-event', function(){

    broadcast(new \App\Events\PrivateEvent());
    return 'private';
    
});
```

### bootstrap.js

bootstrap.jsにPrivateChannelを処理するための記述を追加します。Echo.private()となります。

```diff:bootstrap.js
//for Echo
import Echo from 'laravel-echo';
window.io = require('socket.io-client');

window.Echo = new Echo({
    broadcaster: 'socket.io',
    host: 'http://localhost:6001'
});

window.Echo.channel('public-event')
    .listen('PublicEvent', (e) => {
        console.log(e);
    });

+window.Echo.private('private-event')
+    .listen('PrivateEvent', (e) => {
+        console.log(e);
+    });
```

jsを編集したら忘れずにnpm run devを。

### Laravel-Echo-Serverのコンソール

なお、この時点でブラウザを更新すると、クライアント側のjsがサーバにサブスクリプト(join）のための通信を行いますが認証エラーが出ているようです。
つまり、PrivateChannelを利用するには（Laravel）の認証が必要なようです。

```bash
[09:04:21] - 0MCfTL-LAYuqaXdIAAAC joined channel: public-event
⚠ [09:04:21] - 0MCfTL-LAYuqaXdIAAAC could not be authenticated to private-private-event
{
    "message": "",
```

>上記はpublicへの参加はできてますが、privateへの参加が認証エラーとなっています。

### 認証の準備

では、認証機能を追加します。
私のユースケースではAPI経由でのアクセスとなるのでAPI認証を行えるようにしてみます。

#### migrateの準備

usersテーブル用のマイグレーションファイルを下記のように編集します。api_tokenを追加してるだけです。

```diff
<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateUsersTable extends Migration
{
    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name');
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->rememberToken();
            $table->timestamps();
+            $table->string('api_token')->unique()->nullable();
        });
    }

    public function down()
    {
        Schema::dropIfExists('users');
    }
}
```

migrateを実行します。

```bash
php artisan migrate
```

認証に利用するuserを1名追加しておきます（make:authで対応してもいいかと思います）。

```bash
php artisan tinker

>>> $user = new App\User;
=> App\User {#2907}
>>> $user->name = 'test1';
=> "test1"
>>> $user->email = 'test1@test.com';
=> "test1@test.com"
>>> $user->password = Hash::make('testtest');
=> "$2y$10$pN7.zZyJ87zgusHvkjkMv.bnFyT/g174WTi7kMzezES8V5KWPMGp."
>>> $user->api_token = 'hogehoge';
=> "hogehoge"
>>> $user->save();
=> true
```

>hogehogeは単にAPI認証だけでなくUserの特定に利用されるのでUser毎に一意である必要があります。

### 認証方法の変更

標準ではweb (group) middlewareが適用されているようなのでAPIに変更します。
app/Providers/BroadcastServiceProvider.phpを下記のように変更します。これによりBearerによるtoken認証が行われるようになります。

```diff
<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Broadcast;

class BroadcastServiceProvider extends ServiceProvider
{
    public function boot()
    {
+        Broadcast::routes(['middleware' => 'auth:api']);

        require base_path('routes/channels.php');
    }
}
```

クライアント側にBearerを添付する記述を追加します（抜粋）

```diff
window.Echo = new Echo({
    broadcaster: 'socket.io',
    host: 'http://localhost:6001',
+    auth: {
+        headers: {
+            'Authorization' : 'Bearer hogehoge',
+        }
+    }
});
```

クライアント側を更新するとエラーが消え、joinedとなりました。

```bash
[09:25:52] - 3oy2dVcqrlot8kvdAAAH left channel: public-event (transport error)
[09:25:52] - H9OVvfM5zRJbf52xAAAI joined channel: public-event
[09:25:52] - H9OVvfM5zRJbf52xAAAI authenticated for: private-private-event
[09:25:52] - H9OVvfM5zRJbf52xAAAI joined channel: private-private-event
```

## 実装３：改良する

publicもprivateも動くようになりましたが、ここまでの記述ではprivateも全員が同じchannelを購読しているため、PrivateChannelを利用しても全員に配信されてしまう実装になっています。なので、それを直します。

やりたいことは、id=1の人にだけメッセージを送りたい。といったことです。

では、PrivateEventの発火元であるルートの記述を下記のように変更します。
id=1のuserを取得し、PrivateEventにuserを渡します。

```diff
Route::get('/private-event', function(){

    //id=1のuserを取得
+    $user = App\User::find(1);

    //Eventをuserを渡して発火
+    broadcast(new \App\Events\PrivateEvent($user));
    return 'private';
    
});
```

続いてPrivateEvent.phpを変更します。渡されたuser情報を利用してuser専用のチャネルを設定します。

```diff
<?php

namespace App\Events;

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

use App\User;

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

+    public $user;

    public function __construct(User $user)
    {
+        $this->user = $user;
    }

    public function broadcastOn()
    {
+        return new PrivateChannel('private-event.' . $this->user->id);
    }

    public function broadcastWith()
    {
        return [
            'message' => 'PRIVATE',
        ];
    }
}
```

チャネル名がprivate-event.user_idとなるように定義します。
id=1の場合、チャネルはprivate-event.1となり、id=1の人しか購読しないチャネルとなる想定です。

続いてchannel.phpのchannelのマッチングパターンを適切に変更します。
なおchannel()関数の第一引数には認証されたuser情報が入ります。その情報を利用して配信対象が適切であるかなどをチェックします。

```php
Broadcast::channel('private-event.{id}', function ($user, $id) {
	//認証したuserとチャネルを指定するidが一致するかチェック
    return (int) $user->id === (int) $id;
});
```

では、最後にクライアント側(js)を編集します。
クライアント側でも購読するチャネルがprivate-event.user_idとなるよう実装します。

```js
//user_idを取得する（下記はダミー）
const user_id = 1;
window.Echo.private('private-event.' + user_id.toString())
    .listen('PrivateEvent', (e) => {
        console.log(e);
    });
```

## 応用

* [Reactと連携してみる](https://qiita.com/zaburo/private/b2d80dc274447ec07da2)
* [React Nativeと連携してみる](https://qiita.com/zaburo/private/c1a250c3d396d3c3ea1e)

以上です。