LaravelでWebSocket

# 目次
Laravelの記事一覧は下記
[PHPフレームワークLaravelの使い方](https://qiita.com/toontoon/items/c4d0371e504c37f6576e)

# Laravelバージョン
動作確認はLaravel Framework 7.19.1で行っています

# 前提条件
[eclipseでLaravel開発環境を構築する。デバッグでブレークポイントをつけて止める。(WindowsもVagrantもdockerも)](https://qiita.com/toontoon/items/76fa7f8845b20776077c)
本記事は上記が完了している前提で書かれています
プロジェクトの作成もapacheの設定も上記で行っています

[LaravelでDIを使う](https://qiita.com/toontoon/items/ebf53e4053be6f663730)
本記事は上記で作成したフォルダとファイルを使用します

[Laravelの認証(web画面)](https://qiita.com/toontoon/items/34c871da8660e7d7c075)
本記事は上記が完了している前提で書かれています

[LaravelでRedisを操作する](https://qiita.com/toontoon/items/02bd35266dae6c6640ad)
本記事は上記で作成したフォルダとファイルを使用します。LaravelでRedisを使う設定は完了済みで、sessionもRedisに格納されている前提で書かれています

# Ratchetのインストール
今回はRatchetを使っていきます
[Ratchet (http://socketo.me/)](http://socketo.me/)
コマンドラインで
`cd sample`
`composer require cboden/ratchet`
xdebugの設定をしているとeclipseが実行していいですかというプロンプトを出すのでOKを押します
eclipseのプロジェクトを右クリック→リフレッシュ

# サービスクラス修正
[LaravelでRedisを操作する](https://qiita.com/toontoon/items/02bd35266dae6c6640ad)で作成したサービスクラスを修正します
このサービスクラスは上記記事でServiceProviderに登録してあります

(1) /sample/app/Services/Interfaces/RedisService.php修正

```RedisService.php
<?php
namespace App\Services\Interfaces;

interface RedisService
{
    public function __construct();

    public function setKey1($value);
    public function getKey1();
    public function getSession($key);

}
```
getSessionメソッドを追加しました

(2) /sample/tests/Services/Impl/RedisServiceImpl.php修正

```Tests\Services\Impl\RedisServiceImpl.php
‥‥
    public function getSession($key){
    }
‥‥
```
getSessionメソッドを追加しました

(3) /sample/app/Services/Impl/RedisServiceImpl.php修正

```App\Services\Impl\RedisServiceImpl.php
‥‥
    private $sessionConn = null;

    public function __construct()
    {
‥‥
        $this->sessionConn = Redis::connection(config('session.connection'));
    }
‥‥
    public function getSession($key)
    {
        $key = config('cache.prefix') . ':' . $key;
        $value = $this->sessionConn->get($key);
        return $value;
    }
‥‥
```
`$sessionConn`プロパティを追加しました
コンストラクタを修正しました
getSessionメソッドを追加しました

# WebSocket処理作成
(1) /sample/app/Http/Controllers/ChatController.php作成

```ChatController.php
<?php
namespace App\Http\Controllers;
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;

class ChatController implements MessageComponentInterface  {

    protected $userList;

    public function __construct() {
        $this->userList = [];
    }

    public function addUser($resourceId, $user) {
        $this->userList[$resourceId] = ["user" => $user];
    }

    public function onOpen(ConnectionInterface $from) {
        if (!array_key_exists($from->resourceId, $this->userList)) {
            $this->userList[$from->resourceId] = ["user" => null];
        }
        $this->userList[$from->resourceId]["conn"] = $from;
        $msg = 'ゲストさんが入室しました';
        if (!is_null($this->userList[$from->resourceId]["user"])) {
            $msg = $this->userList[$from->resourceId]["user"]->name . 'さんが入室しました';
        }
        foreach ($this->userList as $userInfo) {
            $data = ['msg' => $msg, 'position' => 'center'];
            $userInfo["conn"]->send(json_encode($data));
        }
    }

    public function onMessage(ConnectionInterface $from, $msg) {
        foreach ($this->userList as $userInfo) {
            $data = ['msg' => $msg];
            if (!is_null($this->userList[$from->resourceId]["user"])) {
                $data['name'] = $this->userList[$from->resourceId]["user"]->name;
            } else {
                $data['name'] = 'ゲスト';
            }
            if ($from === $userInfo["conn"]) {
                $data['position'] = 'right';
            } else {
                $data['position'] = 'left';
            }
            $userInfo["conn"]->send(json_encode($data));
        }
    }

    public function onClose(ConnectionInterface $conn) {
        unset($this->userList[$conn->resourceId]);
    }

    public function onError(ConnectionInterface $conn, \Exception $e) {
        $conn->close();
    }
}
```
onOpenはクライアントが接続してきたときに自動的に実行されます
onMessageはクライアントがsendしてきたときに自動的に実行されます
onCloseはクライアントが切断したときに自動的に実行されます
`$this->userList`にEloquentのユーザーモデルとWebSocketのコネクションが格納されます
`$this->userList`をforeachで回してWebSocket接続している全ユーザーにJSONをsendしています

(2) /sample/app/Serversフォルダ作成
/sample/app/Servers/WsServer.php作成

```WsServer.php
<?php
namespace App\Servers;

use Ratchet\ComponentInterface;
use Ratchet\ConnectionInterface;
use Psr\Http\Message\RequestInterface;
use App\Services\Interfaces\RedisService;
use App\Models\User;

class WsServer extends \Ratchet\WebSocket\WsServer
{

    private $component;

    public function __construct(ComponentInterface $component) {
        parent::__construct($component);
        $this->component = $component;
    }

    public function onOpen(ConnectionInterface $conn, RequestInterface $request = null) {

        $sessionName = config('session.cookie');
        $sessKey = null;
        $sessValue = null;

        $headerCookie = $request->getHeader('Cookie');
        if (isset($headerCookie[0])) {
            $cookieList = explode(';', $headerCookie[0]);
            foreach ($cookieList as $cookie) {
                $cookie = trim($cookie);
                $cookieKeyValue = explode('=', $cookie, 2);
                if ($sessionName === $cookieKeyValue[0]) {
                    $sessKey = decrypt(urldecode($cookieKeyValue[1]), false);
                    break;
                }
            }
        }

        $redisService = app()->makeWith(RedisService::class);
        if (!is_null($sessKey)) {
            $sessValue = $redisService->getSession($sessKey);
            $sessValue = unserialize(unserialize($sessValue));
            foreach ($sessValue as $k => $v) {
                if (strpos($k, 'login_web_') === 0) {
                    $user = User::find($v);
                    $this->component->addUser($conn->resourceId, $user);
                }
            }
        }

        return parent::onOpen($conn, $request);
    }

}
```
コンストラクタに渡されてくる`$component`は先ほど作成したChatControllerです
onOpenはクライアントが接続してきたときに自動的に実行されます
Cookieを取得し、その中からsessionに使っているCookie値を取りだします(`if ($sessionName === $cookieKeyValue[0])`)。
取り出したCookie値を使いRedisからsessionを取得します(`$sessValue = $redisService->getSession($sessKey);`)。
[LaravelでRedisを操作する](https://qiita.com/toontoon/items/02bd35266dae6c6640ad)でsessionはRedisに格納されるようにしました。
そこからユーザーIDをとります。sessionの中のlogin_web_から始まるキーにユーザーIDがvalueとして格納されています(`if (strpos($k, 'login_web_') === 0)`)。

(3) /sample直下にwsServer.php作成

```wsServer.php
<?php

use Ratchet\Server\IoServer;
use Ratchet\Http\HttpServer;
use Illuminate\Contracts\Console\Kernel;
use App\Http\Controllers\ChatController;
use App\Servers\WsServer;

require dirname(__FILE__) . '\vendor\autoload.php';


$app = require __DIR__.'/bootstrap/app.php';
$app->make(Kernel::class)->bootstrap();

$server = IoServer::factory(
    new HttpServer(
        new WsServer(
            new ChatController()
            )
        ),
    8282
    );

$server->run();
```
webSocketのエントリポイントになります
8282は今回WebSocketで使うポートです。firewallで許可しているポートにしてください

# Controllerにメソッド追加
(1) /sample/app/Http/Controllers/SampleController.phpにwebSocketメソッドを追記

```SampleController.php
    public function webSocket(Request $request)
    {
        return view('sample.webSocket');
    }
```

(2) /sample/routes/web.phpに下記を追記
`Route::get('sample/web-socket', 'SampleController@webSocket');`

# viewの作成
(1) /sample/resources/views/sample/webSocket.blade.phpファイル作成

```webSocket.blade.php
<html>
    <head>
        <title>sample</title>
        <style type="text/css">
        .container {
            width: 100%;
            height: 100%;
            box-sizing: border-box;
        }
        .msg-log {
            width: 100%;
            height: 92%;
            vertical-align:top;
            box-sizing: border-box;
            padding: 0px;
            border: black solid 1px;
            overflow-y: scroll;
        }
        .input-area {
            width: 100%;
            box-sizing: border-box;
        }
        .msg {
            width: 90%;
            height: 8%;
            vertical-align:top;
            box-sizing: border-box;
            padding: 0px;
            float: left;
        }
        .btn {
            width: 10%;
            height: 8%;
            vertical-align:top;
            box-sizing: border-box;
            padding: 0px;
        }
        .receive-msg-left {
            border-radius: 10px;
            border: black solid 1px;
            padding: 10px;
            margin: 0px 10px 10px 10px;
            display: inline-block;
            float: left;
            background-color: #FFFFFF;
        }
        .receive-msg-right {
            border-radius: 10px;
            border: black solid 1px;
            padding: 10px;
            margin: 0px 10px 10px 10px;
            display: inline-block;
            float: right;
            background-color: #00FF00;
        }
        .receive-msg-center {
            border: none;
            padding: 0px;
            margin: 10px;
            display: block;
            text-align: center;
            background-color: transparent;
        }
        .name-left {
            border: none;
            padding: 0px;
            margin: 0px 0px 0px 10px;
            display: inline-block;
            float: left;
            background-color: transparent;
        }
        .name-right {
            border: none;
            padding: 0px;
            margin: 0px 10px 0px 0px;
            display: inline-block;
            float: right;
            background-color: transparent;
        }
        .br {
            line-height: 0px;
            clear: both;
        }
        </style>
        <script type="text/javascript">

          var conn = "";

          function open(){

        	  conn = new WebSocket('ws://localhost:8282');

        	  conn.onopen = function(e) {
              };

              conn.onerror = function(e) {
                alert("エラーが発生しました");
              };

              conn.onmessage = function(e) {
            	  var data = JSON.parse(e.data);
            	  var msgLog = document.getElementById("msg_log");
                  var divObj = document.createElement("DIV");
                  var msg = null;
                  var msgSplit = null;
                  var nameObj = null;
                  var rowObj = null;
                  var br = null;
                  if (data["name"]) {
                      msg = document.createTextNode(data["name"]);
                      nameObj = document.createElement("DIV");
                      if (data["position"] == "left") {
                    	  nameObj.className = 'name-left';
                      } else {
                    	  nameObj.className = 'name-right';
                      }
                      nameObj.appendChild(msg);
                	  msgLog.appendChild(nameObj);

                	  br = document.createElement("BR");
                      br.className = 'br';
                	  msgLog.appendChild(br);
                  }
                  if (data["position"] == "left") {
                      divObj.className = 'receive-msg-left';
                  } else if (data["position"] == "center") {
                      divObj.className = 'receive-msg-center';
                  } else {
                      divObj.className = 'receive-msg-right';
                  }
            	  msgSplit = data["msg"].split('\n');
            	  for (var i in msgSplit) {
                      msg = document.createTextNode(msgSplit[i]);
                      rowObj = document.createElement("DIV");
                      rowObj.appendChild(msg);
                      divObj.appendChild(rowObj);
            	  }

            	  msgLog.appendChild(divObj);

            	  br = document.createElement("BR");
                  br.className = 'br';
            	  msgLog.appendChild(br);

                  msgLog.scrollTop = msgLog.scrollHeight;

              };

              conn.onclose = function() {
                  alert("切断しました");
                  setTimeout(open, 5000);
              };

          }

          function send(){
        	  conn.send(document.getElementById("msg").value);
          }

          function close(){
        	  conn.close();
          }

          open();

        </script>
    </head>
    <body>
        <div class="container">
            <div id="msg_log" class="msg-log"></div>
            <div class="input-area">
                <textarea id="msg" class="msg"></textarea>
                <button class="btn" onclick="send();" >送信</button>
            </div>
        </div>
    </body>
</html>
```
`conn = new WebSocket('ws://localhost:8282');`で接続してます。8282は先ほどwsServer.phpに書いたポートです
`conn.send(document.getElementById("msg").value);`でWebSocketサーバーにデータ送信しています
`conn.onmessage`はwebSocketサーバーからsendがあった場合に自動的に実行されます

# 動作確認
コマンドラインで
`cd sample`
`php wsServer.php`
xdebugの設定をしているとeclipseが実行していいですかというプロンプトを出すのでOKを押します
これでWebSocketサーバーが起動します

Chromeから
`http://localhost/laravelSample/`
左上のLOGINリンクからログインする
Firefoxから
`http://localhost/laravelSample/`
左上のLOGINリンクからログインする
Chromeから
`http://localhost/laravelSample/sample/web-socket`
にアクセスする
Firefoxから
`http://localhost/laravelSample/sample/web-socket`
にアクセスする

Chromeで
`あいうえお`
`かきくけこ`
と入力し、送信ボタンをクリックします

Firefoxで
`さしすせそ`
と入力し、送信ボタンをクリックします

![a.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/238103/2c540457-971a-641b-543b-0d4d04ecd8a0.png)

動きました