Laravel on Dockerでのブロードキャスト

# TL;DL

Docker環境のLaravelでのブロードキャストを実装しました:thumbsup:

# Dockerを使ったLaravel環境構築

インストールするのはLaravel 6.xです。

```sh
$ mkdir broadcast
$ cd broadcast
$ composer create-project --prefer-dist laravel/laravel src
```

```
├── docker-compose.yml
├── docker
│   ├── echoserver
│   │   └── Dockerfile
│   ├── nginx
│   │   └── default.conf
│   ├── php
│   │   ├── Dockerfile
│   │   ├── cmd.sh
│   │   └── php.ini
│   └── redis
│       └── data
├── logs
└── src  # Laravelの置き場所
```

```docker-compose.yml 
version: '3'

services:
  web:
    image: nginx:1.17.8-alpine
    ports:
     - 80
    volumes:
     - ./src:/var/www:cached
     - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf
     - ./logs/nginx:/var/log/nginx:cached
    depends_on:
     - php
    environment:
      VIRTUAL_HOST: localhost

  php:
    image: broadcast-php
    build: ./docker/php
    volumes:
     - ./src:/var/www:cached
     - ./logs/php:/var/log/php
     - ./docker/php/cmd.sh:/usr/local/bin/cmd.sh

  nodejs:
    image: node:13.8-alpine
    volumes:
     - ./src:/work:cached
    working_dir: /work

  echo-server:
    image: broadcast-echo-server
    build: ./docker/echoserver
    ports:
     - "6001:6001"
    command: laravel-echo-server start
    volumes:
     - ./src:/work:cached
    working_dir: /work

  redis:
    image: redis:latest
    ports:
     - "6379:6379"
    volumes:
     - "./docker/redis/data:/data"
```

```sh:docker/echoserver/Dockerfile 
FROM node:13.8-alpine

# timezone
RUN apk add --update --no-cache --virtual build-dependencies tzdata && \
    cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime && \
    echo "Asia/Tokyo" > /etc/timezone && \
    apk del build-dependencies

RUN npm install -g laravel-echo-server

WORKDIR /work

CMD ["laravel-echo-server", "start"]
```

```sh:docker/php/Dockerfile 
FROM php:7.3-fpm-alpine
COPY php.ini /usr/local/etc/php/

# lib
RUN apk add --no-cache --virtual build-dependencies gcc make autoconf libc-dev libtool \
 && apk add --no-cache --virtual zlib1g-dev libxml2-dev

# composer
COPY --from=composer /usr/bin/composer /usr/bin/composer

# php extension
RUN docker-php-ext-install xml pdo_mysql

# timezone
RUN apk add --update --no-cache --virtual build-dependencies tzdata && \
    cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime && \
    echo "Asia/Tokyo" > /etc/timezone && \
    apk del build-dependencies

WORKDIR /var/www
```

```docker/php/cmd.sh 
#!/bin/sh
php-fpm
```

```docker/nginx/default.conf 
server {
  listen 80;
    index index.php index.html;
    root /var/www/public;
    charset utf-8;

  location / {
    root /var/www/public;
    try_files $uri $uri/ /index.php$is_args$args;
  }

  location ~ \.php$ {

    try_files $uri =404;
    fastcgi_split_path_info ^(.+\.php)(/.+)$;
    fastcgi_pass php:9000;
    fastcgi_index index.php;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_param PATH_INFO $fastcgi_path_info;
    fastcgi_param HOGE $realip_remote_port;
  }
}
```

ビルドして、最初の`docker-compose up -d`ではLaravel Echo Serverは起動しません。

```sh
$ docker ps -a
CONTAINER ID        IMAGE                   COMMAND                  CREATED             STATUS                      PORTS                               NAMES
                    nginx:1.17.8-alpine     "nginx -g 'daemon of…"   14 seconds ago      Up 12 seconds               0.0.0.0:32774->80/tcp               broadcast_web_1
                    broadcast-php           "docker-php-entrypoi…"   16 seconds ago      Up 13 seconds               9000/tcp                            broadcast_php_1
                    node:13.8-alpine        "docker-entrypoint.s…"   16 seconds ago      Exited (0) 13 seconds ago                                       broadcast_nodejs_1
                    broadcast-echo-server   "docker-entrypoint.s…"   16 seconds ago      Exited (0) 12 seconds ago                                       broadcast_echo-server_1
                    redis:latest            "docker-entrypoint.s…"   16 seconds ago      Up 13 seconds               0.0.0.0:6379->6379/tcp              broadcast_redis_1
```

そこで初期化コマンドを実行する必要があります。

```sh
$ docker-compose run --rm echo-server laravel-echo-server init
? 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
? Will you be serving on http or https? http
? Do you want to generate a client ID/Key for HTTP API? No
? Do you want to setup cross domain access to the API? No
? What do you want this config to be saved as? laravel-echo-server.json
Configuration file saved. Run laravel-echo-server start to run server.
```

これで`src/laravel-echo-server.json`という設定ファイルが作られます。
この中のRedisの接続情報でRedisコンテナを指定します。

```src/laravel-echo-server.json
{
    ...

    "databaseConfig": {
        "redis": {
            "host": "redis",
            "port": 6379
        },
        "sqlite": {
            "databasePath": "/database/laravel-echo-server.sqlite"
        }
    },

    ...
}
```

落として、再度立ち上げます。

```sh
$ docker-compose down
$ docker-compose up -d
$ docker ps -a
CONTAINER ID        IMAGE                   COMMAND                  CREATED             STATUS                      PORTS                               NAMES
                    nginx:1.17.8-alpine     "nginx -g 'daemon of…"   10 seconds ago      Up 7 seconds                0.0.0.0:32775->80/tcp               broadcast_web_1
                    node:13.8-alpine        "docker-entrypoint.s…"   11 seconds ago      Exited (0) 9 seconds ago                                        broadcast_nodejs_1
                    broadcast-php           "docker-php-entrypoi…"   11 seconds ago      Up 9 seconds                9000/tcp                            broadcast_php_1
                    broadcast-echo-server   "docker-entrypoint.s…"   11 seconds ago      Up 9 seconds                0.0.0.0:6001->6001/tcp              broadcast_echo-server_1
                    redis:latest            "docker-entrypoint.s…"   11 seconds ago      Up 9 seconds                0.0.0.0:6379->6379/tcp              broadcast_redis_1
```

これでLaravel Echo Serverも起動しました。

http://localhost:32775/

にアクセスするとLaravelインストール直後の画面が表示されます。

<img width="981" alt="スクリーンショット 2020-02-24 13.51.58.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/123511/a322cd67-78be-7f15-7f92-cb297d802b49.png">

# ブロードキャスト

ここからが本題のブロードキャストです。
以下が公式のドキュメント。

https://laravel.com/docs/6.x/broadcasting
https://readouble.com/laravel/6.x/ja/broadcasting.html

今回はRedisを使います。
ほぼほぼ公式の手順でうまくいきます（そして公式ドキュメントに載っていない設定で躓く。。）

## `App\Providers\BroadcastServiceProvider`の登録

コメントを外します。

```sh
$ diff src/config/app.php.orig src/config/app.php
174c174
<         // App\Providers\BroadcastServiceProvider::class,
---
>         App\Providers\BroadcastServiceProvider::class,
```

## HTMLへのCSRFトークンの追加とJSの読み込み

```sh
$ diff src/resources/views/welcome.blade.php.orig src/resources/views/welcome.blade.php
8a9,10
>         <meta name="csrf-token" content="{{ csrf_token() }}">
> 
98a101,102
> 
>         <script src="{{ mix('js/app.js') }}"></script>
```

## Redisのクライアントライブラリのインストール

```sh
$ cd src
$ composer require predis/predis
```

## イベントの作成

```sh
$ docker-compose exec php php artisan make:event HelloWorldEvent
Event created successfully.
```

イベントの`broadcastOn()`でチャンネルを返すのですが、今回作るのは認証が必要ないチャンネルとします。

```src/app/Events/HelloWorldEvent.php 
<?php

namespace App\Events;

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

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

    public $message = 'Hello World!!';

    /**
     * Create a new event instance.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Get the channels the event should broadcast on.
     *
     * @return \Illuminate\Broadcasting\Channel|array
     */
    public function broadcastOn()
    {
        return new Channel('hello-world-channel');
    }
}
```

## イベント発行のコマンドの作成

```sh
$ docker-compose exec php php artisan make:command BroadcastHelloWorld
Console command created successfully.
```

```src/app/Console/Commands/BroadcastHelloWorld.php 
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use App\Events\HelloWorldEvent;

class BroadcastHelloWorld extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'command:broadcast-hello-world';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Command description';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        event(new HelloWorldEvent());
    }
}
```

## フロントエンド

`laravel-echo`と`socket.io-client`が必要です。

```sh
$ docker-compose run --rm nodejs npm install
$ docker-compose run --rm nodejs npm install --save laravel-echo socket.io-client
```

なんか`npm install`で警告いっぱい出た。。。
チャンネルとイベントを指定してリッスンします。

```sh
$ diff src/resources/js/app.js.orig src/resources/js/app.js
1a2,14
> 
> import Echo from 'laravel-echo';
> 
> window.io = require('socket.io-client');
> 
> window.Echo = new Echo({
>     broadcaster: 'socket.io',
>     host: window.location.hostname + ':6001'
> });
> 
> window.Echo.channel('hello-world-channel').listen('HelloWorldEvent', e => {
>     console.log(e.message);
> });
```

## .envの変更

```sh
$ diff src/.env.orig src/.env
16c16
< BROADCAST_DRIVER=log
---
> BROADCAST_DRIVER=redis
18c18
< QUEUE_CONNECTION=sync
---
> QUEUE_CONNECTION=redis
22c22,24
< REDIS_HOST=127.0.0.1
---
> REDIS_HOST=redis
> REDIS_CLIENT=predis
> REDIS_PREFIX=""
```

**!!重要!!**
ここでRedisの`prefix`を空文字にすることが重要です。
これが公式ドキュメントに書かれていなくて、調べてやっと見つけました。

## ブロードキャストできるか確認

```sh
$ docker-compose run --rm nodejs npm run dev
```

```sh
$ docker-compose exec php php artisan command:broadcast-hello-world
$ docker-compose exec php php artisan queue:work --stop-when-empty
 Processing: App\Events\HelloWorldEvent
 Processed:  App\Events\HelloWorldEvent
```

<img width="981" alt="スクリーンショット 2020-02-24 13.56.50.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/123511/ec191412-592c-46c9-b6d1-420d4eb9cd7f.png">

成功しました！！
`laravel-echo-server.json`のRedisコンテナ指定の修正や、Redisの`prefix`の変更など、Redis周りで難しかったですが、なんとかDocker環境でブロードキャストできました:beer:

# 参考URL

この記事がなかったら自分は諦めてました。:pray::pray::pray:感謝:pray::pray::pray:

https://nori-life.com/laravel-broadcast-redis-socket-io/