Docker×Laravel×Redis×Laravel Echoを使ってリアルタイム通信する

#はじめに
Googleで検索すると、Laravelの公式ドキュメントや他の記事でLaravelとRedisを用いたWebSocket通信のやり方がたくさんでてきましたが、どれを試してなかなか上手くいかず苦戦したので、メモ代わりに記事に残します。各ツールの説明は省略します。検索した方が分かりやすい記事がたくさん出てくると思うので...。


#環境
Docker Desktop for Mac 3.6.0
PHP 8.0.9
Laravel 8.40
redis-server 6.2.5
Laravel-echo-server 1.6.2
node 14.16.0
npm 7.8.0
composer 1.10.19

#dockerでコンテナを構築
今回は下記の記事で作成したDokcer環境をベースに必要なものを追加しました。@ucan-lab さん、ありがとうございます。
[【超入門】20分でLaravel開発環境を爆速構築するDockerハンズオン](https://qiita.com/ucan-lab/items/56c9dc3cf2e6762672f4)

##プロジェクト構成

```
.
├── infra
│   ├── echo-server
│   │   └──Dockerfile
│   ├── mysql
│   │   ├── Dockerfile
│   │   └── my.cnf
│   ├── nginx 
│   │   └── default.conf
│   ├── php
│   │   ├── Dockerfile (この名前にするとファイル名の指定を省略できる)
│   │   └── php.ini
│   └── redis
│       └── data(ディレクトリです)
├── docker-compose.yml (この名前にするとファイル名の指定を省略できる)
└── backend
    └── Laravelをインストールするディレクトリ
```

## 各ファイルの中身

```docker-compose.yml
version: "3.9"
services:
  app:
    build: ./infra/php
    volumes:
      - ./backend:/work
  web:
    image: nginx:1.20-alpine
    ports:
      - 8080:80
    volumes:
      - ./backend:/work
      - ./infra/nginx/default.conf:/etc/nginx/conf.d/default.conf
    working_dir: /work
  db:
    build: ./infra/mysql
    ports:
      - 33060:3306
    volumes:
      - db-store:/var/lib/mysql
  echo-server:
    image: broadcast-echo-server
    build: ./infra/echo-server
    ports:
    - "6001:6001"
    command: laravel-echo-server start
    volumes:
    - ./backend:/work
    working_dir: /work
  redis:
    image: redis:latest
    ports:
      - 6379:6379
    volumes:
      - "./infra/redis/data:/data"
volumes:
  db-store:
```

```Dockerfile:./infra/echo-server/Dockerfile
FROM node:13.8-alpine
RUN npm install -g laravel-echo-server
WORKDIR /work
CMD ["laravel-echo-server", "start"]
```

```Dockerfile:./infra/mysql/Dockerfile
FROM mysql/mysql-server:8.0
ENV MYSQL_DATABASE=laravel_local \
  MYSQL_USER=phper \
  MYSQL_PASSWORD=secret \
  MYSQL_ROOT_PASSWORD=secret \
  TZ=Asia/Tokyo
COPY ./my.cnf /etc/my.cnf
RUN chmod 644 /etc/my.cnf
```

```conf:./infra/mysql/my.cnf
[mysqld]
# default
skip-host-cache
skip-name-resolve
datadir=/var/lib/mysql
socket=/var/lib/mysql/mysql.sock
secure-file-priv=/var/lib/mysql-files
user=mysql
pid-file=/var/run/mysqld/mysqld.pid
# character set / collation
character_set_server = utf8mb4
collation_server = utf8mb4_ja_0900_as_cs_ks
# timezone
default-time-zone = SYSTEM
log_timestamps = SYSTEM
# Error Log
log-error = mysql-error.log
# Slow Query Log
slow_query_log = 1
slow_query_log_file = mysql-slow.log
long_query_time = 1.0
log_queries_not_using_indexes = 0
# General Log
general_log = 1
general_log_file = mysql-general.log
[mysql]
default-character-set = utf8mb4
[client]
default-character-set = utf8mb4
```

```conf:./infra/nginx/default.conf
server {
    listen 80;
    server_name example.com;
    root /work/public;
    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-XSS-Protection "1; mode=block";
    add_header X-Content-Type-Options "nosniff";
    index index.php;
    charset utf-8;
    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }
    location = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt  { access_log off; log_not_found off; }
    error_page 404 /index.php;

    location ~ \.php$ {
        fastcgi_pass app:9000;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
    }
    location ~ /\.(?!well-known).* {
        deny all;
    }
}
```

```Dockerfile:./infra/php/Dockerfile
FROM php:8.0-fpm-buster
SHELL ["/bin/bash", "-oeux", "pipefail", "-c"]
ENV COMPOSER_ALLOW_SUPERUSER=1 \
  COMPOSER_HOME=/composer
COPY --from=composer:2.0 /usr/bin/composer /usr/bin/composer
RUN apt-get update && \
  apt-get -y install git unzip libzip-dev libicu-dev libonig-dev && \
  apt-get clean && \
  rm -rf /var/lib/apt/lists/* && \
  docker-php-ext-install intl pdo_mysql zip bcmath
COPY ./php.ini /usr/local/etc/php/php.ini
RUN curl -sL https://deb.nodesource.com/setup_14.x | bash -
RUN apt-get install -y nodejs
RUN npm install npm@latest -g
WORKDIR /work
```

```ini:./infra/php/php.ini
zend.exception_ignore_args = off
expose_php = on
max_execution_time = 30
max_input_vars = 1000
upload_max_filesize = 64M
post_max_size = 128M
memory_limit = 256M
error_reporting = E_ALL
display_errors = on
display_startup_errors = on
log_errors = on
error_log = /dev/stderr
default_charset = UTF-8
[Date]
date.timezone = Asia/Tokyo
[mysqlnd]
mysqlnd.collect_memory_statistics = on
[Assertion]
zend.assertions = 1
[mbstring]
mbstring.language = Japanese
```

#Laravelをインストール
Dockerコンテナを立ち上げた後、Laravelをインストールします。
Laravelのインストールから環境構築までは下記記事と同様のやり方なので、参考にしてください。
[【超入門】20分でLaravel開発環境を爆速構築するDockerハンズオン](https://qiita.com/ucan-lab/items/56c9dc3cf2e6762672f4)

#Laravel-echo-serverの設定
初回コンテナ起動時、Laravel-echo-serverの初期設定がすんでないため、エラーが発生するので、初期設定を行います。

```
$ docker compose run 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. https://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.
```
上を実行したら、Laravelプロジェクト直下にlaravel-echo-server.jsonというファイルが作成されているので、以下のように書き換えます。

```json:laravel-echo-server.json
{
	"authHost": "http://localhost",
	"authEndpoint": "/broadcasting/auth",
	"clients": [],
	"database": "redis",
	"databaseConfig": {
		"redis": {
			"host": "redis",
            "port": 6379
		},
		"sqlite": {
			"databasePath": "/database/laravel-echo-server.sqlite"
		}
	},
	"devMode": true,
	"host": null,
	"port": "6001",
	"protocol": "http",
	"socketio": {},
	"secureOptions": 67108864,
	"sslCertPath": "",
	"sslKeyPath": "",
	"sslCertChainPath": "",
	"sslPassphrase": "",
	"subscribers": {
		"http": true,
		"redis": true
	},
	"apiOriginAllow": {
		"allowCors": false,
		"allowOrigin": "",
		"allowMethods": "",
		"allowHeaders": ""
	}
}
```

#必要なライブラリを追加
```json:composer.json
"require": {
    // 他省略
    "predis/predis": "^1.1"
}

```

```json:package.json
"dependencies": {
　　　　　　　　// 他省略
    "laravel-echo": "^1.11.1",
    "laravel-echo-server": "^1.6.2",
    "socket.io": "^2.4.0",
    "socket.io-client": "^2.4.0",
}
```

#各種設定

###プロバイダーの設定

app/config/app.phpの以下の部分のコメントアウトを外す

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

###redisの設定

```php:app/config/database.php
'redis' => [
        'client' => env('REDIS_CLIENT', 'predis'),
        'default' => [
            'host' => env('REDIS_HOST', '127.0.0.1'),
            'password' => env('REDIS_PASSWORD', null),
            'port' => env('REDIS_PORT', 6379),
            'database' => 0,
        ],
        'cache' => [
            'host' => env('REDIS_HOST', '127.0.0.1'),
            'password' => env('REDIS_PASSWORD', null),
            'port' => env('REDIS_PORT', 6379),
            'database' => 1,
        ],
        'options' => [
            'prefix' => env('REDIS_PREFIX', ''),
        ],
    ],
```


###.envファイルの変更

```:.env
BROADCAST_DRIVER=redis
QUEUE_CONNECTION=redis
REDIS_HOST=redis
REDIS_CLIENT=predis
REDIS_PASSWORD=null
REDIS_PORT=6379
REDIS_PREFIX=""
```




ここまででWebSocketによるリアルタイム通信を行えるようになりました。


#実際に送ってみる
LaravelのEvent機能を使ってクライアント側にリアルタイムでデータを送信します。

##サーバーサイドの準備

###Eventの作成

```command
php artisan make:event MessageRecieved
```

```php:app/Events/MessageRecieved.php
<?php

namespace App\Events;

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

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


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

    public function broadcastWith()
    {
        return [
            'data' => 'test',
        ];
    }
}

```

###Event発火の準備
今回はルートにアクセスしたときにEventを発火させます。

```php:routes/web.php
Route::get('/', function () {
    event(new \App\Events\MessageReceived());
    return view('welcome');
});
```
### ワーカーの起動
Redisによる処理をキューで実行するのでワーカーを起動しておく

```
php artisan queue:work
```

## クライアントサイドの準備

### ビューの準備
ルートで表示するビューを以下のようにします。

```php:resources/views/welcome.blade.php
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="csrf-token" content="{{ csrf_token() }}">
    <title>Laravel</title>
    <link rel="stylesheet" href="{{ mix('css/app.css') }}">
</head>
<body>
    <div id="app">
      test
    </div>
    <script src="{{ mix('js/app.js') }}"></script>
    <script src="http://{{ Request::getHost() }}:6001/socket.io/socket.io.js"></script>
</body>
</html>
```

###スクリプトの準備
bootstrap.jsに以下を追記します。

```js:resources/js/bootstrap.js
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("test").listen("MessageReceived", e => {
    console.log("メッセージ受け取り成功！！！");
});
```

###コンパイルする

```
npm install && npm run dev
```


##実行
chromeのデベロッペーツールのConsole画面を開いて
http://127.0.0.7:8080 に接続すると
![スクリーンショット 2021-08-18 23.26.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/402129/95332970-b88a-3a51-db77-4e0e5d62b6a6.png)
無事、クライアントでメッセージを受け取れてます。

##動かないとき
Dockerの再起動でうまくいく場合があります。
それでもうまくいかない場合は、Dockerのログを確認しましょう。

```
docker compose logs
```


#さいごに
今回はメモ代わりということでコードをベタ貼りしていく形になりました。ちなみにPusherというサービスを使えば、超簡単にリアルタイム通信が可能です。Laravelの公式ドキュメントにも記載されているので、気になった方はぜひ。