九保すこひです(フリーランスのITコンサルタント、エンジニア)
さてさて、皆さんもご存知の通りLaravel
は多方面に渡って機能追加がやってくるので、嬉しい半面、こんなことになったりするんじゃないでしょうか。
「えっ、そんなのあったの😲❗」
まさに少し前の私がこれなのですが、公式ドキュメントであることに気が付きました。
それは・・・・・・
ブロードキャスト機能に Ably(えいぶりー) というサイトが追加になっている
というものでした。
ちなみに「ブロードキャスト」機能とは、シンプルに言うと「リアルタイムで同期できる」機能のことで、例えばPHP
の動作をリアルタイムでJavaScript
側に伝えたりすることができます。
そして、このブロードキャスト機能で思いつくのが、
チャット機能
ですね。
そこで❗
今回は、「Laravel + React + Ably」でチャット機能を実装してみたいと思います。
ぜひ何かの参考になりましたら嬉しいです。😊✨
「えっ、VanillaJS
って素のJS
なの!?
もう歴15年超なんですが…
(& 私の基準はチョコなんですが…)」
開発環境: Laravel 9.x、React、InertiaJS、Vite
目次
前提として
今回は他のユーザーとやりとりをするチャットをつくりますので、ログイン機能がインストールされていることが前提になります。
もしまだの方は以下ページを参考にして先にログイン機能をインストールしておいてください。
📝参考ページ: 【Laravel】Vite + Inertia + React でログイン機能をインストールする
Ably に登録する
まずは Ably(Pusher
互換)のブロードキャスト・サービスに登録します。
※ ちなみに、Ably
の料金は こちら から確認できます。
(無料枠は、2022.08.31 現在で「600万/月」メッセージ😲❗です。個人開発ぐらいだとほぼ使いきれませんね😊 )
まずトップページ右上にある「Sign up free」ボタンをクリック。
すると、ユーザー登録フォームが表示されるので、必要事項を入力して「Get your free account」ボタンをクリック。
そして、ページが移動すると「どんなことに Ably を使うの?」と聞かれるので、今回は「Real-time chat system.」と入力し「Complete sign up」ボタンをクリックします。(ここ、省略できませんでした…😅)
これでアカウント作成は完了です。
では、次にプロジェクト(アプリ)の登録です。
ページ右側に「+ Create New App」というボタンがあるのでこれをクリック。
以下のようなポップアップが表示されるので、Name
に「Real-time chat」と入力し、「Create app」ボタンをクリックします。
すると、プロジェクト(アプリ)が作成され「プライベート API キー」が表示されるので、これをLaravel
の.env
へ登録しておきましょう。
なお、少し説明が難しいのですが、Ably
のプライベート API キーは以下のような文字列で構成されています。
プライベートキー = (公開部分の文字列)+ (それ以降の文字列)
そして、「公開部分」と「それ以降」はコロン:
によって繋がれています。
例えば、以下のような文字列です。
P_12345:67890
そして、この場合公開部分はコロンより前の部分「P_12345」になります。
これを踏まえて.env
へ登録をします。
.env
ABLY_KEY=(プライベートAPIキー全体) VITE_ABLY_PUBLIC_KEY="(公開部分のAPIキー)"
※ 2行目は、Vite
側へ送信するためのものです。
※ ちなみにこのキーは全ての権限が許可されたルートキーです。今回はテストなので使っていますが、もし権限周りが心配な場合はご自身でキーを作ってください。
また、ついでなので同じく.env
内にあるBROADCAST_DRIVER
を「ably」へ変更しておきましょう。
BROADCAST_DRIVER=ably
Pusher 互換にする
Ably
はPusher
と互換性がありますが、初期状態では有効になっていません。
そのため、この互換性をもたせる設定変更をします。
まず、API キーを取得したページ上部にある「Settings」リンクをクリックします。
そして、ページ中程にある「Protocol Adapter Settings」の中にある「Pusher protocol support」にチェックを入れ、「Save settings」ボタンをクリックします。
ポップアップ表示が出るので「Create default namespaces」ボタンをクリック。
これでAbly
本体での作業は完了です❗
パッケージをインストールする
次に、必要になるパッケージをインストールしておきましょう。
以下のコマンドを実行してください。
composer require ably/ably-php
npm install --save-dev laravel-echo pusher-js
※ ちなみに、(これは公式ドキュメントにも書いていますが)なぜnpm
パッケージにpusher-js
をインストールしているかというと、Ably
はPusher
と互換性があるため、同じパッケージで問題ないからなんですね👍
Laravel Echo を有効にする
続いては、Laravel Echo
が使えるように設定していきます。
resources/js/bootstrap.js
// 省略 // Ably import Echo from 'laravel-echo'; import Pusher from 'pusher-js'; window.Pusher = Pusher; window.Echo = new Echo({ broadcaster: 'pusher', key: import.meta.env.VITE_ABLY_PUBLIC_KEY, wsHost: 'realtime-pusher.ably.io', wsPort: 443, disableStats: true, encrypted: true, });
※ Pusher
の場合はすでにコードが書かれていてコメントアウトするだけで使えますが、今回はAbly
なので上のコードを追加してください。
そして、今回はプライベート通信をしますので、認証ができるようにします。
config/app.php
// 省略 /* * Application Service Providers... */ App\Providers\AppServiceProvider::class, App\Providers\AuthServiceProvider::class, App\Providers\BroadcastServiceProvider::class, // 👈 ここのコメント化を解除しました App\Providers\EventServiceProvider::class, App\Providers\RouteServiceProvider::class, // 省略
routes/channels.php
// 認証 Broadcast::channel('chat-message', function ($user) { return (auth()->check() === true); // ログイン中ならOK });
※ なお、chat-message
はブロードキャストで通信する際の名前です。もちろん自由に変更できますが、他の場所に書かれているものも全て変更してください。
これでLaravel Echo
が有効になりました❗
ルートをつくる
次にチャット用のルートをつくっていきます。
routes/web.php
use App\Http\Controllers\ChatController; // 省略 Route::prefix('chat')->middleware('auth')->controller(ChatController::class)->group(function(){ Route::get('/', 'index')->name('chat.index'); Route::get('/list', 'list')->name('chat.list'); Route::post('/', 'store')->name('chat.store'); });
なお、各ルートの内容は次のとおりです。
- index: ブラウザで表示される部分
- list: Ajax を通してチャットデータを取得する部分(※)
- store: チャットメッセージを新規追加する部分
※ なお、Laravel
からデータ取得することもできますが、入力した内容が失われないようAjax
を通してデータ取得しています。(ページリロードがなくなるため)
モデル&マイグレーションをつくる
そして、モデル&マイグレーションです。
以下のコマンドを実行してください。
php artisan make:model ChatMessage -m
すると、モデルとマイグレーションのファイル2つが作成されるので中身をそれぞれ次のように変更します。
app/Models/ChatMessage.php
<?php namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class ChatMessage extends Model { use HasFactory; // Relationship public function user() { return $this->belongsTo(User::class, 'user_id', 'id'); } }
database/migrations/****_**_**_******_create_chat_messages_table.php
// 省略 public function up() { Schema::create('chat_messages', function (Blueprint $table) { $table->id(); $table->unsignedBigInteger('user_id')->comment('ユーザーID'); $table->text('message')->comment('メッセージ'); $table->timestamps(); $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); }); } // 省略
では、この状態でマイグレーションを実行しましょう。
php artisan migrate
すると、実際のテーブルはこうなりました。
これでDB
まわりはOKです❗
API Resource をつくる
なお、Ajax
でデータ取得する際に気をつけておかないといけないのが「外に出してはいけないデータ」です。
例えば、今回のケースで言うとリレーションシップでユーザー情報も一緒に取得しますが、そうなるとユーザーのメールアドレスが関係のない人に送信されることになります。
そして、これはAPI Resource
をつくることで予防すると便利です。
では、以下のコマンドでAPI Resource
をつくりましょう。
php artisan make:resource ChatResource
するとファイルが作成されるので中身を次のように変更します。
app/Http/Resources/ChatResource.php
<?php namespace App\Http\Resources; use Illuminate\Http\Resources\Json\JsonResource; class ChatResource extends JsonResource { /** * Transform the resource into an array. * * @param \Illuminate\Http\Request $request * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable */ public function toArray($request) { return [ 'id' => $this->id, 'message' => $this->message, 'created_at' => $this->created_at, 'user' => [ 'name' => $this->user->name, ] ]; } }
これで、ChatResource
を通したデータ提供にはユーザーのメールアドレスやIDは含まれないようになりました❗
ブロードキャスト実行用のイベントをつくる
続いてLaravel
側からチャットメッセージをAbly
へ伝えるイベントをつくります。以下のコマンドを実行してください。
php artisan make:event ChatMessageCreated
するとファイルが作成されるので、中身を次のように変更します。
app/Events/ChatMessageCreated.php
<?php namespace App\Events; use App\Http\Resources\ChatResource; use App\Models\ChatMessage; 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 ChatMessageCreated implements ShouldBroadcast { use Dispatchable, InteractsWithSockets, SerializesModels; private $chat_message; /** * Create a new event instance. * * @return void */ public function __construct(ChatMessage $chat_message) { $this->chat_message = $chat_message; } /** * Get the channels the event should broadcast on. * * @return \Illuminate\Broadcasting\Channel|array */ public function broadcastOn() { return new PrivateChannel('chat-message'); } /** * Get the data to broadcast. * * @return array */ public function broadcastWith() { return [ 'chat_message' => new ChatResource($this->chat_message), ]; } }
※ ちなみに、今回はサンプルなのでbroadcastWith()
をつけていますが、実際のところ不要です。
コントローラーをつくる
そして、コントローラーです。
以下のコマンドを実行してください。
php artisan make:controller ChatController
すると、ファイルが作成されるので中身を次のように変更します。
app/Http/Controllers/ChatController.php
<?php namespace App\Http\Controllers; use App\Events\ChatMessageCreated; use App\Http\Resources\ChatResource; use App\Models\ChatMessage; use Illuminate\Http\Request; use Inertia\Inertia; class ChatController extends Controller { public function index() { return Inertia::render('Chat/Index'); } public function list() { $chat_messages = ChatMessage::with('user') ->limit(10) // 10件だけ ->latest() // 新着順 ->get(); return ChatResource::collection($chat_messages); } public function store(Request $request) { // 注: バリデーションは省略してます $chat_message = new ChatMessage(); $chat_message->user_id = auth()->id(); $chat_message->message = $request->message; $chat_message->save(); ChatMessageCreated::dispatch($chat_message); // ブロードキャストを実行 return to_route('chat.index'); // リダイレクト } }
ビューをつくる
では、ブラウザで実際に表示される部分の「ビュー」をつくっていきましょう。
resources/js/Pages/Chat/Index.jsx
import {useState, useEffect} from "react"; import {Inertia} from "@inertiajs/inertia"; import ChatMessageHeader from "@/Components/ChatMessageHeader"; import ChatMessageFooter from "@/Components/ChatMessageFooter"; export default function Index(props) { // Data const userName = props.auth.user.name; const [chatMessages, setChatMessages] = useState([]); const [chatMessage, setChatMessage] = useState(''); // Methods const handleMessageChange = e => { // メッセージ入力したとき const message = e.target.value; setChatMessage(message); }; const handlerSubmit = () => { // 送信したとき const url = route('chat.store'); const data = { message: chatMessage }; Inertia.post(url, data, { onSuccess() { setChatMessage(''); // 成功したらメッセージをリセット } }); }; const getChatMessages = () => { // チャットメッセージを取得する axios.get(route('chat.list')) .then(response => { const chatMessages = response.data; setChatMessages(chatMessages); }); }; // Effects useEffect(() => { // ページを読み込んだ時 getChatMessages(); // ブロードキャスト受信 Echo.private('chat-message') .listen('ChatMessageCreated', e => { console.log(e); getChatMessages(); // ブロードキャスト通知が来たら再読込みする }); }, []); return ( <div className="p-5"> <h1 className="mb-2 font-bold"> Laravel + React + Ably でチャット機能 </h1> <div className="p-1"> <small> <strong>{userName}</strong> さんでログイン中 </small> </div> {/* メッセージ部分 */} <div className="p-4 bg-gray-100"> {chatMessages.length > 0 && chatMessages.map(chatMessage => ( <div key={chatMessage.id} className="bg-white border mb-2 p-3 rounded"> <ChatMessageHeader name={chatMessage.user.name} /> <div className="whitespace-pre mt-2">{chatMessage.message}</div> <ChatMessageFooter chatMessage={chatMessage}></ChatMessageFooter> </div> ))} {chatMessages.length === 0 && ( <div className="text-center"> <div className="text-gray-500">まだメッセージはありません</div> </div> )} </div> {/* フォーム部分 */} <div className="py-3"> <small>💬 チャットへ投稿</small> <textarea rows="4" className="block p-2.5 w-full text-sm text-gray-900 border border-gray-400 rounded mb-3" value={chatMessage} onChange={e => handleMessageChange(e)} autoFocus /> <button type="button" className="px-4 py-2.5 text-sm font-medium text-center text-white bg-blue-700 rounded-lg" onClick={handlerSubmit}>送信する</button> </div> </div> ); }
ちょっとコードが長いですが、やっているのは、以下2つだけです。
- チャットデータを Ajax で取得して表示
- 新規メッセージを送信(して保存)
なお、新しいチャットメッセージが登録されたときAbly
からの通知が来るのがEcho.private('chat-message').listen( ... )
の部分になります。
コンポーネントをつくる
では、先ほどのビューで使った2つのコンポーネントがまだ用意できていないのでつくっていきます。(アトミックデザインの場合はもっと細かくなると思いますがテストなのでご容赦くださいm(_ _)m)
チャット・メッセージのヘッダー(ChatMessageHeader)
resources/js/Components/ChatMessageHeader.jsx
export default function ChatMessageHeader(props) { const name = props.name; const firstNameCharacter = name.charAt(0); return ( <> <span className="bg-red-400 text-red-50 rounded-full px-1.5 py-1 text-xs mr-1 font-bold"> {firstNameCharacter} </span> <small>{name} さん</small> </> ); };
resources/js/Components/ChatMessageFooter.jsx
export default function ChatMessageHeader(props) { const chatMessage = props.chatMessage; const dt = new Date(chatMessage.created_at); const date = dt.toLocaleString('ja-JP'); return ( <> <small className="text-gray-400">{date}</small> </> ); };
テストしてみる
では実際にテストしてみましょう❗(動画は こちら から)
まずは以下のコマンドでVite
を起動します。
npm run dev
そして、通常のブラウザとシークレットモードのブラウザを用意します。(別環境として扱われるため)
そして、それぞれ「太郎さん」と「次郎さん」でログイン後「http://******/chat」へアクセスします。(左が太郎さんで、右が次郎さん)
各ログインユーザーの名前が表示されました。
では、この状態で太郎さんの方でメッセージを「テストです from 太郎」と入力し、送信してみます。
すると・・・・・・
はい❗
太郎さんのページだけでなく、次郎さんの方も操作していないのに、送信した内容が追加されました。
成功です😊✨
では、逆に次郎さんの方から「返信です from 次郎」と送り返してみましょう。
どうなったでしょうか・・・・・・
はい❗
今度は太郎さんの方が自動でチャットメッセージが更新されました。
すべて成功です✨😊👍
企業様へのご提案
今回のようにブロードキャスト機能を使うと、リアルタイムで他のユーザーとページを同期をすることができます。
例えば、チャット以外では以下のような機能が考えられます。
- 決済が完了したことをリアルタイムで管理者ページへ伝える
- 複数のスタッフで共同編集するページをつくる
- リアルタイムで各ユーザーのログイン状態を知らせる
などなど。
もしこういった機能をご希望でしたらぜひお問い合わせからご相談くださいませ。お待ちしております。😊✨
おわりに
ということで、今回は「Laravel + React + Ably」でリアルタイム・チャット機能をつくってみました。
いろいろと設定する部分は多かったですが、Laravel
が用意してくれている部分のことを考えると、とても簡単に実装ができると言っていいんじゃないでしょうか。
なお、今回途中で変更した部分はチャット・データを取得する部分です。
当初は直接テンプレートへ送る(index()
で取得する)形にしていましたが、これだと新規メッセージが投稿されたときにページをリロードする必要があり、そうなると、現在入力中のメッセージが消えてしまうことになります。
もちろんlocalStorage
などを使えばいいのですが、よりシンプルさを考えてAjax
を使うようにしました。
それでは、ぜひ皆さんもチャット機能で楽しんでみてくださいね。
ではでは〜❗
「記事書くためにmailgun
登録したら、
放置してて$27.24のお支払い💦
円安…」