こんにちは。松本です。
今回は、Laravelの機能を使って「チャット」機能を実装してみようと思います。

はじめに

今回はPUSHERという外部サービスを使用して、リアルタイム通信機能を実装します。
普段Laravelを使用して開発されている方に向けて記載しています。
あらかじめ、ご了承ください。

環境については下記です。
Laravel : 5.5
php : 7.1
MySQL : 5.7.19

上記の環境をHomesteadを用いて構築しております。

構成は、下記のようになります。

今回やりたいこと

・チャットメッセージが届いたらブラウザにPush通知
・チャットメッセージが届いたらメールでも通知

必要な画面及び機能

・チャットを利用するユーザの登録画面
・チャットを利用するユーザを登録するテーブル
・チャット画面
・チャットのメッセージを登録するテーブル
・メッセージのイベント発行機能

完成型

完成型は下記になります。

チャット

メール送信

ではやっていきましょう!

PUSHERの設定

PUSHER」にアクセスし、アプリを作成します。

アプリを作成すると、下記画面に遷移します。

各ユーザによって、扱う環境は異なるため
Javascriptや、React Nativeで書く場合など、サンプルを用意されています。
親切ですね。

今回はクライアント側は「Javascript」サーバー側は「Laravel」を使うように設定していきます。

プロジェクト作成

今回作業するプロジェクトを、以下のコマンドで作成します。
今回の例でいうと、「chat」という名前でプロジェクトを作成します。

composer create-project --prefer-dist laravel/laravel chat "5.5.*"

次に、PUSHERのライブラリが必要であるため、下記のコマンドでインストールします。

composer require pusher/pusher-php-server "~3.0"

chatへアクセスできるように、Homestead.yamlの設定を変更します。
Homestead.yamlで設定したIPへ、アクセスできるようにhostsの設定を追加します。

Homestead.yaml
keys:
    - ~/.ssh/id_rsa

folders:
    - map: ~/dev_works/chat
      to: /home/vagrant/code/chat

sites:
    - map: chat.test
      to: /home/vagrant/code/chat/public
      php: "7.1"

databases:
    - homestead

# blackfire:
#     - id: foo
#       token: bar
#       client-id: foo
#       client-token: bar

# ports:
#     - send: 50000
#       to: 5000
#     - send: 7777
#       to: 777
#       protocol: udp
hosts
192.168.30.10 chat.test

画面表示

http://chat.testにアクセスすると、下記の画面が表示されます。

認証機能

下記コマンドを使用して、作成します。

php artisan make:auth

 

上記コマンド実行後、migrationファイルが2つ生成されます。
下記コマンドで、migrationを実行します。

php artisan migrate

chat.test/register にアクセスすると、ユーザ登録画面が表示されるので
チャットを使用するユーザを登録します。

テーブル作成

メッセージを保存するためのテーブルを作成します。
migrationファイルを作成し、下記のように修正します。

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateMessagesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('messages', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->bigInteger('send')->comment('送信者');
            $table->bigInteger('recieve')->comment('受信者');
            $table->text('message')->comment('メッセージ');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('messages');
    }
}

上記のファイルが完成したら、migrationファイルを実行します。

テーブルが作成できたら、簡易的なチャットの画面を作成します。

チャットユーザ選択画面

ユーザの登録を完了すると、/homeという画面に遷移にします。

また、ログイン後も/homeという画面に遷移するため、
ログイン後はチャットユーザを選択する画面を表示するように、
HomeControllerを下記のように修正します。

HomeController.php
<?php

namespace App\Http\Controllers;

use App\User;
use Illuminate\Support\Facades\Auth;

class HomeController extends Controller
{
    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('auth');
    }

    /**
     * Show the application dashboard.
     *
     * @return \Illuminate\Http\Response
     */
    public function index()
    {

        $user = Auth::user();

        // ログイン者以外のユーザを取得する
        $users = User::where('id' ,'<>' , $user->id)->get();
        // チャットユーザ選択画面を表示
        return view('chat_user_select' , compact('users'));
    }
}

チャットユーザ選択画面は、下記のようになります。

chat_user_select.blade.php
@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">

        </div>
    </div>

    {{--  チャット可能ユーザ一覧  --}}
    <table class="table">
        <thead>
        <tr>
            <th>#</th>
            <th>Name</th>
            <th></th>
        </tr>
        </thead>
        <tbody>
        @foreach($users as $key => $user)
        <tr>
            <th>{{$loop->iteration}}</th>
            <td>{{$user->name}}</td>
            <td><a href="/chat/{{$user->id}}"><button type="button" class="btn btn-primary">Chat</button></a></td>
        </tr>
        @endforeach
        </tbody>
    </table>

</div>

@endsection

ユーザ登録画面(/register)より、ユーザを登録し、
http://chat.test/homeにアクセスすると、下記のようになるかと思います。

ルーティング定義

メッセージを保存、イベントを発行するルーティングを定義します。

web.php
Route::get('/chat/{recieve}' , 'ChatController@index')->name('chat');
Route::post('/chat/send' , 'ChatController@store')->name('chatSend');

チャット画面

チャットユーザ選択画面から「chat」のボタンを押下すると、実際にチャットする画面に遷移するので
遷移した際の処理を記載します。

chat.blade.php
@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
        </div>
    </div>

    {{--  チャットルーム  --}}
    <div id="room">
        @foreach($messages as $key => $message)
            {{--   送信したメッセージ  --}}
            @if($message->send == \Illuminate\Support\Facades\Auth::id())
                <div class="send" style="text-align: right">
                    <p>{{$message->message}}</p>
                </div>

            @endif

            {{--   受信したメッセージ  --}}
            @if($message->recieve == \Illuminate\Support\Facades\Auth::id())
                <div class="recieve" style="text-align: left">
                    <p>{{$message->message}}</p>
                </div>
            @endif
        @endforeach
    </div>

    <form>
        <textarea name="message" style="width:100%"></textarea>
        <button type="button" id="btn_send">送信</button>
    </form>

    <input type="hidden" name="send" value="{{$param['send']}}">
    <input type="hidden" name="recieve" value="{{$param['recieve']}}">
    <input type="hidden" name="login" value="{{\Illuminate\Support\Facades\Auth::id()}}">

</div>

@endsection
@section('script')
    <script type="text/javascript">

       //ログを有効にする
       Pusher.logToConsole = true;

       var pusher = new Pusher('[YOUR-APP-KEY]', {
           cluster  : '[YOUR-CLUSTER]',
           encrypted: true
       });

       //購読するチャンネルを指定
       var pusherChannel = pusher.subscribe('chat');

       //イベントを受信したら、下記処理
       pusherChannel.bind('chat_event', function(data) {

           let appendText;
           let login = $('input[name="login"]').val();

           if(data.send === login){
               appendText = '<div class="send" style="text-align:right"><p>' + data.message + '</p></div> ';
           }else if(data.recieve === login){
               appendText = '<div class="recieve" style="text-align:left"><p>' + data.message + '</p></div> ';
           }else{
               return false;
           }

           // メッセージを表示
           $("#room").append(appendText);

           if(data.recieve === login){
               // ブラウザへプッシュ通知
               Push.create("新着メッセージ",
                   {
                       body: data.message,
                       timeout: 8000,
                       onClick: function () {
                           window.focus();
                           this.close();
                       }
                   })

           }


       });


        $.ajaxSetup({
            headers : {
                'X-CSRF-TOKEN' : $('meta[name="csrf-token"]').attr('content'),
            }});


        // メッセージ送信
        $('#btn_send').on('click' , function(){
            $.ajax({
                type : 'POST',
                url : '/chat/send',
                data : {
                    message : $('textarea[name="message"]').val(),
                    send : $('input[name="send"]').val(),
                    recieve : $('input[name="recieve"]').val(),
                }
            }).done(function(result){
                $('textarea[name="message"]').val('');
            }).fail(function(result){

            });
        });
    </script>

@endsection

ログインユーザが「送信者」であれば「右側」、

ログインユーザが「受信者」であれば「左側」に寄せてメッセージを表示しています。

また、PUSHERとpush.jsを使用するため
app.blade.phpに下記を追記します。

app.blade.php
{{--  追加  --}}
<script src=“https://js.pusher.com/3.2/pusher.min.js“></script>
<script src=“https://cdnjs.cloudflare.com/ajax/libs/push.js/0.0.11/push.min.js”></script>

コントローラー側にメッセージを保存する処理、メールを送信する処理、
イベントを発行する処理を記載します。

ChatController.php
<?php

namespace App\Http\Controllers;

use App\Mail\SampleNotification;
use Illuminate\Http\Request;
use App\Events\ChatMessageRecieved;
use App\Message;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Mail;

class ChatController extends Controller
{

    public function __construct()
    {
    }


    public function index(Request $request , $recieve)
    {
        // チャットの画面
        $loginId = Auth::id();

        $param = [
          'send' => $loginId,
          'recieve' => $recieve,
        ];

        // 送信 / 受信のメッセージを取得する
        $query = Message::where('send' , $loginId)->where('recieve' , $recieve);;
        $query->orWhere(function($query) use($loginId , $recieve){
            $query->where('send' , $recieve);
            $query->where('recieve' , $loginId);

        });

        $messages = $query->get();

        return view('chat' , compact('param' , 'messages'));
    }

    /**
     * メッセージの保存をする
     */
    public function store(Request $request)
    {

        // リクエストパラメータ取得
        $insertParam = [
            'send' => $request->input('send'),
            'recieve' => $request->input('recieve'),
            'message' => $request->input('message'),
        ];


        // メッセージデータ保存
        try{
            Message::insert($insertParam);
        }catch (\Exception $e){
            return false;

        }


        // イベント発火
        event(new ChatMessageRecieved($request->all()));

        // メール送信
        $mailSendUser = User::where('id' , $request->input('recieve'))->first();
        $to = $mailSendUser->email;
        Mail::to($to)->send(new SampleNotification());

        return true;

    }
}

イベント発行

下記のコマンドを使用しEventを作成します。

php artisan make:event ChatMessageRecieved

作成されたイベントを修正します。

ChatMessageRecieved.php
<?php

namespace App\Events;

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

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

    protected $message;
    protected $request;

    /**
     * Create a new event instance.
     *
     * @return void
     */
    public function __construct($request)
    {
        $this->request = $request;

    }

    /**
     * イベントをブロードキャストすべき、チャンネルの取得
     *
     * @return Channel|Channel[]
     */
    public function broadcastOn()
    {

        return new Channel('chat');

    }

    /**
     * ブロードキャストするデータを取得
     *
     * @return array
     */
    public function broadcastWith()
    {

        return [
            'message' => $this->request['message'],
            'send' => $this->request['send'],
            'recieve' => $this->request['recieve'],
        ];
    }

    /**
     * イベントブロードキャスト名
     *
     * @return string
     */
    public function broadcastAs()
    {

        return 'chat_event';
    }
}

メール設定

メールについては「MailTrap」を使用します。

MailTrapでアカウントを作成していただき、SMTPの設定を確認します。

その設定を.envファイルに記載します

# メールの設定
MAIL_DRIVER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=[YOUR-MAILTRAP-USERNAME]
MAIL_PASSWORD=[YOUR-MAILTRAP-PASSWORD]
MAIL_ENCRYPTION=null

メール送信については、Mailableクラスを継承して作成します。

<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Contracts\Queue\ShouldQueue;
class SampleNotification extends Mailable
{
   use Queueable, SerializesModels;

   /**
    * Create a new message instance.
    *
    * @return void
    */
   public function __construct()
   {
   }
   /**
    * Build the message.
    *
    * @return $this
    */
   public function build()
   {
       return $this->view(‘emails.mail_send’);
   }
}

メール内容

非常にシンプルですが、下記の内容でメールのテンプレートを保存します。

mail_send_blade.php
あなた宛てに新着メッセージがありました

最後に

LaravelとPUSHERを使って実装してみましたが、いかがだったでしょうか。

ブラウザへのpush通知に限らず、Slackへの連携など様々な機能に連携が可能なので
お問い合わせ機能なども、リアルタイム通信機能を使って行うことでより
スムーズに問い合わせできるようになるので、弊社のサービスにも取り入れていこうと思いました。

次回は、各サービスへの連携やプレゼンスチャンネル機能の実装など
より便利になるような記事にしたいと思います。

松本
CSVIT事業部 IT部 松本
現在は保育の案件や雇用事業案件に携わっております。