LaravelにおいてWebSocket接続経由のチャット機能を構築してみます。
開発環境
Laravel Framework 7.19.0
本記事ではログインしたユーザーのみが利用できる簡単なチャットを作成します。フロント側ではVue.jsを利用します。なおバリデーションの処理は省いています。
イベントブロードキャストアプリの概要
LaravelにおいてWebSocketによるアプリを構築する場合、主に以下2つの枠組が考えられます。
・Redis + laravel-echo-server
・Pusherチャンネルという外部サービスの利用
下記イラストは上記2つの枠組みにおいて、イベント(event)をブロードキャストする際のざっくりとした構成を表しています。
本記事では「Pusherチャンネル」を利用したアプリ構築のみを説明しますが、比較として「Redis + laravel-echo-server」について簡単に説明しておきます。
laravel-echo-serverはNode.js製のサーバーで、Redisと接続し、Socket.ioによってクライアントとの間にWebsocketを確立することができます。クライアント側では2つのパッケージを利用します。Socket.ioと接続するsocket.io-client、そしてチャンネルをサブスクライブしてイベントをリッスンするlaravel-echoです。
Pusherチャンネル(とpusher/pusher-php-server)を利用すれば、Redisとlaravel-echo-serverを用意することなしにWebSocketによるアプリを構築できます。
プロジェクトの準備
プロジェクトを作成します。
1 |
$ composer create-project laravel/laravel broadcast-project --prefer-dist |
laravel/uiパッケージをインストールします。
1 |
$ composer require laravel/ui |
認証機能の構築に必要なビューなどのファイルを生成します。
1 |
$ php artisan ui vue --auth |
マイグレーションを実行します。※すでにデータベースと接続していることを前提に説明します。
1 |
$ php artisan migrate |
パッケージをインストールし、watchでコンパイルを自動化します。
1 |
$ npm install && npm run watch |
以上でログイン機能が実装されます。サーバーを起動し、適当なユーザーを登録しておきます。
関連ページ
ブロードキャスト実装の準備
ブロードキャストを有効にするために、下記ファイル/部分のコメントを外します。
config/app.php
1 |
App\Providers\BroadcastServiceProvider::class, |
PusherチャンネルPHP SDKをインストールします。
1 |
$ composer require pusher/pusher-php-server |
クライアント側で必要な2つのパッケージをインストールします。
1 |
$ npm install --save laravel-echo pusher-js |
bootstrap.jsの下記コードのコメントアウトを解除します。
resources/js/bootstrap.js
1 2 3 4 5 6 7 8 9 10 |
import Echo from 'laravel-echo'; window.Pusher = require('pusher-js'); window.Echo = new Echo({ broadcaster: 'pusher', key: process.env.MIX_PUSHER_APP_KEY, cluster: process.env.MIX_PUSHER_APP_CLUSTER, forceTLS: true }); |
Pusherチャンネルの設定
PUSHER にサインアップ/サインインをしたら、Create app の画面において、フロントエンドでVue.js、バックエンドでLaravelを選択します。
App keysを取得できる画面があるので、それをコピーして、.envの下記項目にそれぞれ設定します。
1 2 3 4 |
PUSHER_APP_ID= PUSHER_APP_KEY= PUSHER_APP_SECRET= PUSHER_APP_CLUSTER= |
また.envのブロードキャストドライバー項目をpusherに設定します。.envを編集したらキャッシュのクリアを忘れずに。
1 |
BROADCAST_DRIVER=pusher |
Messageモデルの作成
チャット機能のメッセージを保存するためのMessageモデルを作成します。すでに存在するUserが親でMessageが子の関係となる「1対多」のリレーションとして構築していきます。
関連ページ
Messageモデルとマイグレーションファイルを作成し、下記のように編集します。
1 |
$ php artisan make:model Message -m |
database/migrations/2020_07_09_142245_create_messages_table.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
<?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreateMessagesTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('messages', function (Blueprint $table) { $table->id(); $table->integer('user_id'); $table->text('message'); $table->timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('messages'); } } |
app/Message.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Message extends Model { protected $fillable = ['message']; public function user() { return $this->belongsTo('App\User'); } } |
またUserモデルのクラスに下記関数を追記します。
app/User.php
1 2 3 4 |
public function messages() { return $this->hasMany('App\Message'); } |
マイグレーションを実行します。
1 |
$ php artisan migrate |
ルーティング・コントローラの作成
web.phpに下記コードを追記します。
routes/web.php
1 2 3 |
Route::get('post', 'ChatsController@index'); Route::get('messages', 'ChatsController@fetchMessages'); Route::post('messages', 'ChatsController@sendMessage'); |
コントローラを作成し、下記のように編集します。
1 |
$ php artisan make:controller ChatsController |
app/Http/Controllers/ChatsController.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use App\Message; use Illuminate\Support\Facades\Auth; use App\Events\MessageSent; class ChatsController extends Controller { public function __construct() { $this->middleware('auth'); } public function index() { return view('post'); } public function fetchMessages() { return Message::with('user')->get(); } public function sendMessage(Request $request) { $user = Auth::user(); $message = $user->messages()->create([ 'message' => $request->input('message') ]); event(new MessageSent($user, $message)); return ['status' => 'Message Sent!']; } } |
35行目
MessageSentイベント(後述で作成)を発行しています。引数にはログインしているユーザーとチャットのメッセージを指定しています。
event関数はbroadcast関数に置き換えることも可能です。
1 |
broadcast(new MessageSent($user, $message)); |
またtoOthersで自分以外のユーザーにブロードキャストできるようになります。
1 |
broadcast(new MessageSent($user, $message))->toOthers(); |
イベントの作成・チャンネル
関連ページ
イベントを作成します。
1 |
$ php artisan make:event MessageSent |
生成されたSampleEvent.phpを下記のように編集します。
app/Events/MessageSent.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
<?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; use App\User; use App\Message; class MessageSent implements ShouldBroadcast { use Dispatchable, InteractsWithSockets, SerializesModels; public $user; public $message; /** * Create a new event instance. * * @return void */ public function __construct(User $user, Message $message) { $this->user = $user; $this->message = $message; } /** * Get the channels the event should broadcast on. * * @return \Illuminate\Broadcasting\Channel|array */ public function broadcastOn() { return new PrivateChannel('chat'); } } |
15行目 ShouldBroadcastインターフェイスを実装します。
37〜40行目
イベントは任意のチャンネル上でブロードキャストされます。チャンネルは大まかにパブリックとプライベートに分けられます。パブリックなチャンネルは全てのユーザーが利用でき、ある任意のユーザーが利用するにはプライベートを利用します。本記事ではログインユーザーが利用するためのchatという名前のチャンネルをPrivateChannel()関数に設定しています。パブリックな場合はChannel()関数を利用します。
プライベートチャンネルをリッスンする場合、channels.phpに許可ルールを記述する必要があります。下記コードを追記します。
routes/channels.php
1 2 3 |
Broadcast::channel('chat', function ($user) { return Auth::check(); }); |
chatチャンネルを利用できるのは、現在ログインしているユーザー(コールバックの引数$user)に対して、trueを返すように記述する必要があります。この場合、Auth::check()を返しています。
フロントエンド
Vue.jsを利用した開発については下記ページもご覧下さい。
post.blade.phpを作成し、下記のように編集します。
resources/views/post.blade.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<!DOCTYPE html> <html lang="{{ str_replace('_', '-', app()->getLocale()) }}"> <head> <meta charset="UTF-8"> <title>チャット</title> <link href="{{ mix('css/app.css') }}" rel="stylesheet" type="text/css"> <meta name="csrf-token" content="{{ csrf_token() }}"> </head> <body> <div id="app"> <example-component></example-component> </div> <script src="{{ mix('js/app.js') }}"></script> </body> </html> |
本記事ではすでに登録・生成されているexample-componentコンポーネントをそのまま利用します。下記のように編集します。
resources/js/components/ExampleComponent.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
<template> <div> <ul> <li v-for="(message, key) in messages" :key="key"> <strong>{{ message.user.name }}</strong> {{ message.message }} </li> </ul> <input v-model="text" /> <button @click="postMessage" :disabled="!textExists">送信</button> </div> </template> <script> export default { data() { return { text: "", messages: [] }; }, computed: { textExists() { return this.text.length > 0; } }, created() { this.fetchMessages(); Echo.private("chat").listen("MessageSent", e => { this.messages.push({ message: e.message.message, user: e.user }); }); }, methods: { fetchMessages() { axios.get("/messages").then(response => { this.messages = response.data; }); }, postMessage(message) { axios.post("/messages", { message: this.text }).then(response => { this.text = ""; }); } } }; </script> |
29〜34行目
イベントのリッスンをおこなっています。今回はプライベートチャンネルなのでprivateメソッドを利用しています。listenメソッドではイベントを指定し、値を受け取ります。
以上でユーザーを適当に作成し ~/post にアクセスするとチャット画面が表示されるようになります。ログインしていない場合は、ログイン画面に遷移します。
参照ページ