1
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

laravel Pusherチャンネルの使い方を整理してみる。(編集中)

Last updated at Posted at 2021-09-12

リファレンスサイト

dashboard.pusherの環境設定

滅茶苦茶、丁寧に解説してくれてます。
Pusherのアカウント登録
↑これを見ながら、下の公式サイトでキーを取得できる。
フロントエンドの選択は何でもいいと思います。
結局みんな、laravel-echo pusher-jsを使ってますし・・・・。
vue.jsを選択しても、laravel-echo pusher-jsは問題なく使える。
だから、なんでもいいと思う。

pusherの公式サイト
start project

laravel new laravel-echo
composer require laravel/ui:3.3.3
php artisan ui bootstrap --auth

Pusher Channels PHP SDKをインストールと環境構築

laravel8
composer require pusher/pusher-php-server
通信はもうこれで決まり。
npm install --save-dev laravel-echo pusher-js
resources\js\bootstrap.js
/**
 * コメントアウトを解除
 */

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
});

envのPUSHER_APP_●とBROADCAST_DRIVERに入力

envの編集(keyはenvに既に書かれてある)
#(上の方に外れておいてある忘れずにlogからpusherに書き換える。)
BROADCAST_DRIVER=pusher

PUSHER_APP_ID=5565431
PUSHER_APP_KEY=395bdfe72a63c666dfaee96e
PUSHER_APP_SECRET=defaeab0f150493166ed94
PUSHER_APP_CLUSTER=ap3

config/app.phpのコメントアウトを解除

BroadcastServiceProvider::classのコメントを解除する。
BroadcastServiceProviderは2個あるから注意、上は外れている。

config/app.phpの編集
//こっちは外れている
Illuminate\Broadcasting\BroadcastServiceProvider::class,
/*
* Application Service Providers...
*/
App\Providers\BroadcastServiceProvider::class, // コメントを解除しました

とりあえずイベントをつくる

イベントを作成して、コントローラー等からイベントを発火させる。
イベントに登録されてあるチャンネルが
同じチャンネルのlaravel-echo-pusher-js
を発火させる。
と今は理解している。

php artisan make:event helloWorld
app\Events\helloWorld.php
class helloWorld
/*忘れずにimplements ShouldBroadcast忘れずに*/
implements ShouldBroadcast
/*\(^o^)/忘れたら時間を無駄にするで\(^o^)/*/
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public function __construct()
    {
        //
    }
    /**
     *取り敢えずプライベートチャンネルから
     *パブリックチャンネルに切り替えて検証
     */
    public function broadcastOn()
    {
        // return new PrivateChannel('channel-name');
        return new Channel('hello');
    }

}

イベントを発火するページ(ルート)を作る。

routes\web.php
Route::get('/', function () {
    //イベントを発火
    broadcast(new \App\Events\helloWorld());

    //welcomeページをリターン
    return view('welcome');
});

resources\views\welcome.blade.php
                の(/body)の上に追記

resources\views\welcome.blade.php
{{-- welcom.blade.phpの</bod>タグの上に追記 --}}
    <script src="{{ asset('js/app.js') }}"></script>
    <script>
            window.Echo.channel('hello')
            .listen('helloWorld', (e) => {
                console.log(e)
                console.log('\(^o^)/');
                console.log('(;O;)');
                console.log('٩(♡ε♡ )۶');
                console.log('(●`ε´●)');
            });
    </script>
</body>

ひとまず、検証する。
トップページにアクセス。
あれ・・・何も発火しないが、ネットワークを確認したら、websocketには繋がっていた。
image.png
npm run devの結果がうまく反映されていないのではないか。
version()を使うため、asset関数からmix関数へ変更する。

webpack.mix.js
mix.js('resources/js/app.js', 'public/js')
    .sass('resources/sass/app.scss', 'public/css')
    .version()   /*追加 */
    .sourceMaps();
resources\views\welcome.blade.php
    //asset()からmix()へ変更
    <script src="{{ mix('js/app.js') }}"></script>
config\app.php
    'mix_url' => env('MIX_ASSET_URL', '/laravel-echo/public/'),
npm run dev

さいど、検証してみる。
あっ・・・console.log()にチラッとでて遷移されて消える。
laravelのキャッシュ対策のためには、mix関数を優先して使うべき。
image.png
これはjsの読み込むタイミングが早いためだと思う。しっかりとイベントが発火して、jsを発火させている。のか、それとも、イベントとは関係なくjsが発火しているのか・・
確認するため、
Chrome とedgeで立ち上げて、edgeでアクセスして、Chrome でjsが発火するか見てみる。
image.png
イベントがjsを発火させていた。ヤッターマン\(^o^)/
今度は、ajaxでイベントを発火させてみる。

routes\web.php
Route::get('/', function () {
    // broadcast(new \App\Events\helloWorld());
    //welcomeページをリターン
    return view('welcome');
});

Route::get('/hello', function () {
    //ajaxでイベントを発火させる。
    broadcast(new \App\Events\helloWorld());
});
resources\views\welcome.blade.php
    <script src="{{ mix('js/app.js') }}"></script>
    <script>
        //ajaxで'/hello'にget送信するだけ。
        $.ajax({
                type: 'get',
                datatype: 'json',
                url: '/laravel-echo/public/hello', // パス
                timeout: 3000,
            })

        window.Echo.channel('hello')
            .listen('helloWorld', (e) => {
                console.log(e)
                console.log('\(^o^)/');
                console.log('(;O;)');
                console.log('٩(♡ε♡ )۶');
                console.log('(●`ε´●)');
            });
    </script>
</body>

無事Chromeもedgeからも、jsが発火している。
image.png

###broadcastWith()を使用してみる。
broadcastWith()のlaravelの公式サイトの説明
イベントのペイロードとしてpublicプロパティはすべて自動的にシリアライズされます。これによりJavaScriptアプリケーションより、publicデータにアクセスできます。ですから、たとえば、あるイベントにEloquentモデルを含む、publicの$userプロパティがあれば、そのイベントのブロードキャストペイロードは、次のようになります。

なにコレ
{
    "user": {
        "id": 1,
        "name": "Patrick Stewart"
        ...
    }
}

しかしながら、ブロードキャストペイロードをより上手くコントロールしたければ、そのイベントへbroadcastWithメソッドを追加してください。このメソッドから、イベントペイロードとしてブロードキャストしたいデータの配列を返してください。
意味不明。
そもそも論、ペイロードってなにやねん。
それをコントロールするってどういうこと。

けど、使えば何となく分かるはず。
取り敢えず、使えるのか確認する。
そういえば、
console.log(e)がカラの配列を返していた。

image.png

公式サイトのプロジェクトページ
image.png
なんのための、eやねん。
取り敢えず、broadcastWith()を使ってみる。
###helloWorldイベントにbroadcastWith()を追加

app\Events\helloWorld.php
    public function broadcastOn()
    {
        // return new PrivateChannel('channel-name');
        return new Channel('hello');
    }
    /**
     *追加
     */
    public function broadcastWith()
    {
        return [
            'message' => 'hello world 2',
        ];
    }

image.png
image.png

●eにdataをペイロードしてしまいましたか?。そういう意味か?。
●ちょっと違うよな、コントロールするってどういう意味やねんってなる。

次に、あるイベントにEloquentモデルを含む、publicの$userプロパティがあれば
そのイベントのブロードキャストペイロードは、次のようになります。
とあるので、helloWorldイベントにUserモデルのプロパティを作成する。
データーベースを作成せなあかんめんどくさい。

env
DB_CONNECTION=sqlite
# DB_HOST=127.0.0.1
# DB_PORT=3306
# DB_DATABASE=laravel_echo2
# DB_USERNAME=root
# DB_PASSWORD=
win
type nul > database\database.sqlite
mac
touch database/database.sqlite
php artisan migrate
database\seeders\DatabaseSeeder.php
//コメントアウトを解除
        \App\Models\User::factory(10)->create();
php artisan db:seed
app\Events\helloWorld2.php
// +
use App\Models\User;

class helloWorld2
-------
// +
    public $users;
    public function __construct()
    {
// +
       $this->users = User::all();
    }
//-
    /**
     *動作確認のためコメントアウト
     */
    // public function broadcastWith()
    // {
    //     return [
    //         'message' => 'hello world 2',
    //     ];
    // }

image.png
あっなんか、デフォルトだと、イベントのプロパティのデータを全部返すみたい。
じゃないわ、ペイロードするみたい。これを、細かくコントロールするのが、
broadcastWith()メソッド。

app\Events\helloWorld2.php
    /**
     *先程のコメントアウトを解除
     */
    public function broadcastWith()
    {
        return [
            'message' => 'hello world 3',
        ];
    }

image.png

はい、無事ペイロードのコントロールを確認できました。
\(^o^)/ヤッターマン

これは、メッセージを送って相手側でリアルタイム発火することができるんじゃねーか。
検証してみる。
###まずはイベントの作成
これがなくては、始まらない。

php artisan make:event MessageSend

作ったら、なにがなんでもこれが最初やろ

app\Events\MessageSend.php
------------------------------
class MessageSend
implements ShouldBroadcast // +
{
------------------------------
}
app\Events\MessageSend.php
<?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;

class MessageSend
// +
implements ShouldBroadcast
{
// +
    public $message;
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public function __construct($message)
    {
// +
        $this->message = $message;
    }

    public function broadcastOn()
    {
// +
        return new Channel('message');
        // return new PrivateChannel('channel-name');
    }
}

メッセージを送信するフォームがいるわ。
受け取るのは取り敢えずconsoleでええやろう。

resources\views\layouts\app.blade.php
<!-- Scripts これはコメントアウト-->
    <!--  <script src="{{ asset('js/app.js') }}" defer></script> -->

<!-- Styles mixに変更-->
    <link href="{{ mix('css/app.css') }}" rel="stylesheet">
----------------------------
<!--+-->
    <script src="{{ mix('js/app.js') }}"></script>
    @stack('js')
<!--+-->
</body>
</html>

ルートの作成

routes\web.php
Route::get('/message_send', [App\Http\Controllers\HomeController::class, 'message_send'])->name('message_send');
Route::post('/message_send', [App\Http\Controllers\HomeController::class, 'message_send_post'])->name('message_send_post');

サーバーの作成(HomeControllerを使い回す)

app\Http\Controllers\HomeController.php
public function message_send()
{
    return view('message_send');
}

public function message_send_post(Request $request)
{
    // 引数を入れて、イベントを発火させる。
    broadcast(new \App\Events\MessageSend($request->message));
}

フロントの作成(message_send.blade.phpを作成)

resources\views\message_send.blade.php
@extends('layouts.app')

@section('content')
    <div class="container">
        <div class="row justify-content-center">
            <div class="col-md-8">
                <div class="card">
                    <div class="card-header">{{ __('Dashboard') }}</div>

                    <div class="card-body">
                        {{-- @csrf --}}

                        <div class="form-group row">
                            <label for="message" class="col-md-4 col-form-label text-md-right">メッセージ</label>

                            <div class="col-md-6">
                                <textarea id="message" class="form-control @error('message') is-invalid @enderror"
                                    name="message" value="{{ old('message') }}" required autocomplete="message"
                                    autofocus></textarea>

                                @error('message')
                                    <span class="invalid-feedback" role="alert">
                                        <strong>{{ $message }}</strong>
                                    </span>
                                @enderror
                            </div>
                        </div>
                        <div class="form-group row mb-0">
                            <div class="col-md-6 offset-md-4 text-right">
                                <button id="messageBtn" class="btn btn-primary">
                                    送信する
                                </button>
                            </div>
                        </div>
                        </form>
                    </div>
                </div>
            </div>
        </div>
    </div>
@endsection
@push('js')
    <script>
        $.ajaxSetup({
            timeout: 3000,
            headers: {
                'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
            }
        });
        $(document).on('click', '#messageBtn', function() {
            var message = $('#message').val();
            $.ajax({
                    type: 'post',
                    datatype: 'json',
                    url: 'message_send',
                    timeout: 3000,
                    data: {
                        message: message
                    }
                })
                .done(function(data, textStatus, jqXHR) {
                    $('#message').val('');
                });
        });

        window.Echo.channel('message')
            .listen('MessageSend', (e) => {
                console.log(e)
                console.log(e.message)
            });
    </script>
@endpush

エッジから送信してみる。
image.png
無事、Chrome で通信確認。
image.png
スマホからも通信してみる。
image.png
image.png
通信できてるな。\(^o^)/ヤッターマン

うーん・・・・。
通信といえば、通信しているが・・・。
やったことは、イベントのプロパティにデータを持たせただけ、
感覚的には、それを表示しているに過ぎない・・・・。
これでいいのか。

なんか、適当なアプリを作って、確認したい。
九保すこひ様のリアルタイム・チャットをつくるに挑戦してみる

[九保すこひ様のリアルタイム・チャットをつくる]
(https://blog.capilano-fw.com/?p=1418)
↑データーベースとの連携もあって本当にちょうどよい。
本当に九保すこひ様いつもありがとうございます。
九保やすこひ様のサイトは他のどのサイトよりも安心して挑戦できる。
その他にも、laravelのチートシートが沢山あって、公式サイトなんていらないぐらいだ。
form等は先程のformを使い回すとして、保存するテーブルがいる。

###モデルとテーブルを作成する。

php artisan make:model Message -a
app\Models\Message.php
    protected $fillable = [
        'message',
    ];
database\migrations\xxx_create_messages_table.php
    public function up()
    {
        Schema::create('messages', function (Blueprint $table) {
            $table->id();
            $table->string('message');
            $table->timestamps();
        });
    }
php artisan migrate

###ルートを作成する

routes\web.php
これは先程のルートを使い回す

###コントローラーを作成する
これも先程のメソッドを使い回して編集する。

app\Http\Controllers\HomeController.php
public function message_send(Request $request)
{
    if($request->ajax()){
        $messages = Message::get();
        return $messages;
    }
    return view('message_send');
}

public function message_send_post(Request $request)
{
    $message = Message::create([
        'message' => $request->message
    ]);

    // 引数を入れて、イベントを発火させる。
    // broadcast(new \App\Events\MessageSend($request->message));
    event(new MessageCreated($message));
}

###そして、イベントの作成をする。

php artisan make:event MessageCreated

作成したら、すぐにコレimplements ShouldBroadcast

app\Events\MessageCreated.php
class MessageCreated
// +
implements ShouldBroadcast
{

もう、ここは、チャンネル名を変えてコピペ。
うん、イベントのプロパティにデータを渡してるだけ。

app\Events\MessageCreated.php
<?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;

class MessageCreated
// +
implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public $message;

    public function __construct($message)
    {
        $this->message = $message;
    }

    public function broadcastOn()
    {
        return new Channel('kuho_chan_nel');
    }
}

resources\views\message_send.blade.phpを書き換える。

resources\views\message_send.blade.php
@extends('layouts.app')

@section('content')
    <div class="container">
        <div class="row justify-content-center">
            <div class="col-md-8">
                <div class="card">
                    <div class="card-header">{{ __('Dashboard') }}</div>

                    <div class="card-body">
                        {{-- @csrf --}}

                        <div class="form-group row">
                            <label for="message" class="col-md-4 col-form-label text-md-right">メッセージ</label>

                            <div class="col-md-6">
                                <textarea id="message" class="form-control @error('message') is-invalid @enderror"
                                    name="message" value="{{ old('message') }}" required autocomplete="message"
                                    autofocus></textarea>

                                @error('message')
                                    <span class="invalid-feedback" role="alert">
                                        <strong>{{ $message }}</strong>
                                    </span>
                                @enderror
                            </div>
                        </div>
                        <div class="form-group row mb-0">
                            <div class="col-md-6 offset-md-4 text-right">
                                <button id="messageBtn" class="btn btn-primary">
                                    送信する
                                </button>
                            </div>
                        </div>
                        </form>
                    </div>
                </div>{{-- <div class="card"> --}}
{{-- 追加 --}}
                <div class="card mt-5">
                    <div class="card-header">メッセージ一覧</div>
                    <div class="card-body">
                        <ul id="message_body">
                        </ul>
                    </div>
                </div>
{{-- 追加 --}}
            </div>
        </div>
    </div>
@endsection
@push('js')
    <script>
        $.ajaxSetup({
            timeout: 3000,
            headers: {
                'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content'),
            }
        });

        $(document).on('click', '#messageBtn', function() {
            var message = $('#message').val();
            $.ajax({
                    type: 'post',
                    datatype: 'json',
                    url: 'message_send',
                    timeout: 3000,
                    data: {
                        message: message
                    }
                })
                .done(function(data, textStatus, jqXHR) {
                    $('#message').val('');
                });
        });

        window.Echo.channel('message')
            .listen('MessageSend', (e) => {
                console.log(e.message)
            });

 //追加
        getMessage();

        window.Echo.channel('kuho_chan_nel')
            .listen('MessageCreated', (e) => {
                // this.getMessages(); // 全メッセージを再読込
                getMessage();
            });

        function getMessage() {
            $.ajax({
                    type: 'get',
                    datatype: 'json',
                    url: 'message_send',
                    timeout: 3000,
                })
                .done(function(data, textStatus, jqXHR) {
                    $result = $("#message_body"),
                        li = [];
                    $.each(data, function(index, value) {
                        li.push(`<li>id:${value.id} 内容:${value.message}</li>`);
                    })
                    $result[0].innerHTML = li.join("");
                });
        }
    </script>
@endpush

image.png

無事、通信できている。\(^o^)/ヤッターマン
やったことは、イベントのプロパティにデータをもたせただけ
なんて、簡単なんや。
認証中ユーザーの回避

broadcast関数には->toOthers()メソッドってがあって、
ブロードキャストの受取人から現在のユーザーを除外できる
らしい。
取り敢えず、できるか試してみる。
今の状態は
image.png
エッジから送ってみると、送ったがわにも、ブロードキャストされているのがわかる。

->toOthers()を追記して検証してみる。

app\Http\Controllers\HomeController.php
public function message_send_post(Request $request)
{
    $message = Message::create([
        'message' => $request->message
    ]);

    // 引数を入れて、イベントを発火させる。
//->toOthers()メソッドを追加
    broadcast(new \App\Events\MessageSend($request->message))->toOthers();
//こっちはコメントアウト
    // event(new MessageCreated($message));
}
resources\views\message_send.blade.php
//コメントアウト
        //追加
        // getMessage();

では、試してみる。edgeから送信
image.png
受け取ったChrome ではjsが発火したが、送ったedgeではjsは発火しなかった。
今度は、Chrome から送信
image.png
今度は、逆の関係になった。
これは、\(^o^)/ヤッターマンではないか?
と思ったら。
設定のところに
VueとAxiosを使用しない場合、JavaScriptアプリケーションでX-Socket-IDヘッダを送信するように、設定する必要があります。
とある。
(゚Д゚)ハァ?
僕が、やったのは、$ajax送信
その上に
。VueとAxiosを使用していれば、X-Socket-IDヘッダとして、送信する全リクエストへ自動的に付加されます。そのため、toOthersメソッドを呼び出す場合、LaravelはヘッダからソケットIDを取り除き、そのソケットIDを使い全接続へブロードキャストしないように、ブロードキャスタに対し指示します。
とある。
これは、どういうこと?
今の所、送信時のheadersはX-CSRF-TOKENだけや。

resources\views\message_send.blade.php
       $.ajaxSetup({
            timeout: 3000,
            headers: {
                'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content'),
            }
        });

公式サイトの説明では、headersにX-Socket-IDを設定しなければ、
vue axios通信以外では->toOthers()は使えんよとある。
要するに、↓のような設定をする必要があるはずや。

resources\views\message_send.blade.php
        var socket_id = Echo.socketId();
        $.ajaxSetup({
            timeout: 3000,
            headers: {
                'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content'),
                'X-Socket-ID': socket_id
            }
        });

↑こんな設定はしておらん。
どういうことや。
いっかいこれでやってみる。
image.png
逆もOKや、動作公式サイトどうりに、機能している。

いや違うねん。
俺は見せたかったんや。
'X-Socket-ID': socket_id
がある場合とない場合で、

うーん普通にformで送信してみる。
あかん、遷移してまう、めんどくせぇ

ajax送信におけるheadersの中身を確認してみる。
普通こっちだわな。
image.png
普通に、自動で追記してくれてる・・・・。
結論、ajax送信のときも
X-Socket-IDをheaderに自動で追記してくれる。
(laravel8時)
あっおれ 5.6の公式サイト 見てる・・・・。
改めてlaravel8を見てみる怖い・・・
グローバルAxiosインスタンスを使用してJavaScriptアプリケーションからHTTPリクエストを作成している場合、ソケットIDはすべての送信リクエストにX-Socket-IDヘッダとして自動的に添付されます。

なに?
グローバルAxiosインスタンスを使用して
なに?コレ?
グローバルAxiosインスタンス??
そういえば、resources\js\bootstrap.jsにそれっぽいのがある。

resources\js\bootstrap.js
window.axios = require('axios');

いったん、これを外してみた。

npm run dev

あ、あかん。
エラーになって最初から動かん。
ということです。
イベントの方でも->toOthers()メソッドを指定できる。
$this->dontBroadcastToCurrentUser()
試してみる。toOthers()をコメントアウトして、

app\Http\Controllers\HomeController.php
    broadcast(new \App\Events\MessageSend($request->message))/*->toOthers()*/;

イベントに$this->dontBroadcastToCurrentUser();を追記

app\Events\MessageSend.php
    public function __construct($message)
    {
// +
        $this->dontBroadcastToCurrentUser();
        $this->message = $message;
    }

image.png
期待通り
ここで、\(^o^)/ヤッターマン

コネクションのカスタマイズ

(ここ読む必要ない)
複数のブロードキャスト接続とやりとりしており
viaメソッドを使ってどの接続にイベントをプッシュするか指定できます
例として、
broadcast(new OrderShipmentStatusUpdated($update))->via('pusher');
とある。
なんか使い方が、想定と違う。
イベントに複数チャンネルがあって、それを選択できるのかと思ったが、違う。
うーん、
とりあえず、もう一個チャンネルを公式サイトでつくってみる。

あかんつまった。
アプリケーションが複数のブロードキャスト接続とやりとりしており、デフォルト以外のブロードキャスタを使いイベントをブロードキャストしたい場合は、viaメソッドを使ってどの接続にイベントをプッシュするか指定できます。
image.png
デフォルトのチャンネルがcomposed-flower-493で、other_messageがデフォルト以外のブロードキャスタと仮定して、viaメソッドを使って切り替えるのかと思ったけど。

そもそも論、デフォルト以外のブロードキャスタをどうやって、登録するのか?
サンプルもないし。
ちょっと置いとく、
(ここまで)

プライベートチャンネル

概要
プライベートチャンネルをサブスクライブするには、ユーザーがそのチャンネルでリッスンする認証と認可を持っている必要があります。
(´Д`)ハァ…相変わらず意味不明

なんや、認証と認可ってまぁ、

認証と認可が2ついるんってことだけわかったわ。

まぁ、いつものごとく試してみたらわかるんや。

まずは、プライベートイベントの作成やろ・・・と思ったけど、

ここは、サーバー側から把握するよりもフロント側から把握した方が、
理解が早いんとちゃうか。

取り敢えず、先程のMessageSendイベントのchannelをprivateに書き換えてみるか。

resources\views\message_send.blade.php

// window.Echo.channel('message')
//     .listen('MessageSend', (e) => {
//         console.log(e.message)
//     });

//channelからprivateに変更しただけ。
    window.Echo.private('message')
    .listen('MessageSend', (e) => {
        console.log(e.message)
    });

ん?なんや、これで、アクセスすると、勝手にauthがないとかいってる。
イベントは何も発火させてへんのに、勝手にや、アクセス時や。
image.png
これが、認証って意味か?
とりあえず、404って言うことはルートがないってことや、これは、xamppから
アクセスしてるかやろ。

取り敢えず、ルートを修正してやるか。

承認エンドポイントのカスタマイズってのができるらしい。
公式サイトのコードauthEndpoint: '/custom/endpoint/auth'
                  ↑何これ。

resources\js\bootstrap.js
window.Echo = new Echo(
    {
        broadcaster: 'pusher',
        key: process.env.MIX_PUSHER_APP_KEY,
        cluster: process.env.MIX_PUSHER_APP_CLUSTER,
        forceTLS: true,
//+
        authEndpoint: '/laravel-echo/public/broadcasting/auth'
    },
);
npm run dev

再度、アクセスすると,403になった。

「403 Forbidden」は「認可失敗」に対するコードだが・・・。

ログイン認証に失敗したときでも返ってくることもある。

401(認証)と403(認可)を区別していないとも思えんし・・
image.png
routes\channels.php
403は401(認証)の勘違いだと判断して、
認証するにはサーバーサイドの処理も必要になる。
これは、公式サイトによれば、routes\channels.phpでやるみたい。

routes\channels.php
Broadcast::channel('message.*'/* *なんでもOK */,function(){
    return true;
});

フロント側でもサーバー側に認可コードを送ってやる必要がある。

resources\views\message_send.blade.php

    window.Echo.private("message.1"/*認可コード*/)
        .listen('MessageSend', (e) => {
            console.log(e)
        });

再度アクセスすると、エラーは表示されず、ソースを確認すると200になっている。
認証が成功しているのが確認できる。
image.png
でも、これでメッセージをポストしても動作しない。
200okが帰ってきてるのに。なんでや。
image.png
あっ認可か、認可がないからか。

Privateの認可判断するためには、PrivateChannel()を使用する必要がある。
よって、Channel()からPrivateChannel()に変更する。

app\Events\MessageSend.php
    public function broadcastOn()
    {
        // return new Channel('message');
       //PrivateChannelに変更して、認可番号をセットする。
        return new PrivateChannel('message.1'/*認可コード*/);

      /* (resources\views\message_send.blade.php)
       * //認可コードはアクセス時,サーバー側に届け出が行われている。
       * //PrivateChannelメソッドは引数と同じ認可コードを持っている
       * //                 ユーザーだけにイベントの発火を許可する。
       * window.Echo.private("message.1"/*認可コードは最初に届け出る*/)
       *     // .listen('PrivateMessage', (e) => {
       *      .listen('MessageSend', (e) => {
       *          console.log(e)
       *      });
       */
    }

再度メッセージを送ってやる。無事発火。
image.png
このコードでは、すべてのユーザーを認証しているため、
今度は、ログインユーザーを認証するよう変更する。

とりあえず、ログインしているユーザーだけにメッセージを送ってやる。
サーバー側は

routes\channels.php
公式サイトでは$userになっているがわかりにくいので$authに変更した
Broadcast::channel('message.{userId}'/* (*)でもOK */,function($auth /*$userから変更*/,$userId/*ここでは不要*/){
    /*ログイン認証してなければnull値となりfalseになる*/
      return $auth;
    // return (int) $user->id === (int) $userId;
});
app\Events\MessageSend.php
    public function broadcastOn()
    {
        // return new Channel('message');
        //('チャンネル.引数') 引数は絶対にいる{id}とか*ではあかん'
       //プライベートチャンネルでは認証と認可が必ず必要なため。
        return new PrivateChannel('message.fugafuga');
    }
resources\views\message_send.blade.php
//"message.fugafuga"これは、認可と同じでないとだめ。
window.Echo.private("message.fugafuga")
    .listen('MessageSend', (e) => {
        console.log(e)
    });

認証時(アクセス時)、チャンネルの引数が認可コードとして届けられている事がわかる。

画像をとるのを忘れたが、しっかりと、送信できた。

今度は特定のユーザーにだけイベントが発火するようにしたい。

app\Events\MessageSend.php
    public function broadcastOn()
    {
//本来ならメッセージを送るときにこの番号も一緒に送ってやる
        $userId = 12;
        // return new Channel('message');
        return new PrivateChannel('message.'.$userId);
    }
resources\views\message_send.blade.php
//本来的にはこれはやめたほうがいいらしい。
    window.Echo.private("message.{{ Auth::id() }}")
        .listen('MessageSend', (e) => {
            console.log(e)
        });

//色とりどりな書き方をみる。
//https://www.messiahworks.com/archives/19044
//ただ,この書き方が一般的かな。
 <script>
       window.Laravel = {!! json_encode([
    'user_id' => auth()->check() ? auth()->user()->id : null,
    'team_id' => auth()->check() ? auth()->user()->team_id : null,
     ]) !!};
 </script>
    window.Echo.private('kuho_chan_nel.' + window.Laravel.user_id)
//他にも
//https://github.com/alexeymezenin/laravel-best-practices/blob/master/japanese.md
//<input id="article" type="hidden" value='@json($article)'>
//let article = $('#article').val();
routes\channels.php
//ここのコードが走るのはアクセス時のみ、イベント発火時とうは、関係ない。
Broadcast::channel('message.*',function($user){
    return $user;
//$idがね相手に対して入力をもとめるpasswordとかやったらね。この処理はね理解できるねん。
//ログインしているかしていないかの処理でこれは違うと思う。今のところ。
//adminユーザーの場合とかguardsの種類もここで、判定できる。
    // return (int) $user->id === (int) $id;
});

これで、ログインIDが12番のユーザーだけのjsが発火する。
image.png

認証と認可を分けたことは、本当によく考えられている。
頭のええ人は違う。

うーんしかし、
認証と認可、わかったようなわからないような。

今回以外だったことは、
認証は認可と常にセットであるといえるが、
認可だけでいいなら、認証は必要がなかったことである。

パブリックチャンネルで特定のユーザにだけ、jsを発火したい場合。

resources\views\message_send.blade.php
//privateからchannelに書き換える。認可番号はいる。
        window.Echo.channel("message.{{ Auth::id() }}")
            .listen('MessageSend', (e) => {
                console.log(e.message)
            });
app\Events\MessageSend.php
//privateからchannelに書き換える。
    public function broadcastOn()
    {
        $userId = 12;
        return new Channel('message.'.$userId);
        // return new PrivateChannel('message.'.$userId);
    }
routes\channels.php
必要ないがとりあえずコメントアウト
// Broadcast::channel('message.*',function($user){
//     return $user;
//     // return (int) $user->id === (int) $id;
// });

image.png
認証のいらないパブリックチャンネルでも認可による制限が機能している。
なにも認証はbroadcastでやる必要はないような気がしてきた・・・。

なによりも、認証と認可を区別してるくせに、認可コードは認証とセットしてるし、
複数の認可コードを設定することもできないので、複数の認可コードをセットしたい時は下のように2つ書く必要がある。
よって、2回認証作業が走る。大した事ではないが・・・。不満だ。

ただ、pusherとのやり取りはサーバー側で送信している。
auth(front) <->sever<->pusher
vendor\laravel\framework\src\Illuminate\Broadcasting

resources\views\message_send.blade.php
    pusher = window.Echo.private("message.1").listen('MessageSend', (e) => {
        console.log(e)
    });
    pusher = window.Echo.private("message.3").listen('MessageSend', (e) => {
        console.log(e)
    });

この辺でまとめておいた方がいいコード

resources\js\bootstrap.js
window.Echo = new Echo(
    {
        broadcaster: 'pusher',
        key: process.env.MIX_PUSHER_APP_KEY,
        cluster: process.env.MIX_PUSHER_APP_CLUSTER,
        forceTLS: true,
//-
        // authEndpoint: '/laravel-echo/public/broadcasting/auth',

        authorizer: (channel, options) => {
            console.log(channel,options,channel.name);
            return {
                authorize: (socketId, callback) => {
                    axios.post('/laravel-echo/public/broadcasting/auth', {
                        socket_id: socketId,
                        channel_name: channel.name,

                  // +post送信を追加できる。
                        fugafuga: 'fugafuga',
                    })
                    .then(response => {
                        callback(false, response.data);
                    })
                    .catch(error => {
                        callback(true, error);
                    });
                }
            };
        },
    },
);
protected function verifyUserCanAccessChannel($request, $channel)
protected function extractAuthParameters($pattern, $channel, $callback)
protected function extractChannelKeys($pattern, $channel)



ヤッターマンでいいと思う。
認証と認可の区別はできてるし、・・・。

まぁ、動的な認可コードに対応してみる。

ユーザーにチームIDをつける。

同じ、チームIDをもった特定のユーザーに送信してみる。

database\migrations\xxx_create_users_table.php
        Schema::create('users', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
           //+
            $table->integer('team_id')->nullable();
            $table->rememberToken();
            $table->timestamps();
        });
app\Http\Controllers\Auth\RegisterController.php
    protected function create(array $data)
    {
        return User::create([
            'name' => $data['name'],
            'email' => $data['email'],
            'password' => Hash::make($data['password']),
            //+
            'team_id' => rand(1,3),
        ]);
    }
database\factories\UserFactory.php
    public function definition()
    {
        return [
            'name' => $this->faker->name(),
            'email' => $this->faker->unique()->safeEmail(),
            'email_verified_at' => now(),
//'password'から'Password'に変更
            'password' => '$2y$10$L1XJ1Gqg.PKVdJtfAG7jVuR/vRjmXvFuV4z9e2KJcx5/9NdaP2W0.', 
            'remember_token' => Str::random(10),
            //+
            'team_id'=>rand(1,3)
        ];
    }
php artisan migrate:refresh
php artisan db:seed
php artisan tinker
User::find(1)
=> App\Models\User {#4310
     id: "1",
     name: "Garland Carroll I",
     email: "sample1@test.com",
     email_verified_at: "2021-09-13 08:28:44",
     #password: "$2y$10$L1XJ1Gqg.PKVdJtfAG7jVuR/vRjmXvFuV4z9e2KJcx5/9NdaP2W0.",
     team_id: "1",
     #remember_token: "uhtKOUMWyu",
     created_at: "2021-09-13 08:28:44",
     updated_at: "2021-09-13 08:28:44",
   }

ログインメールは: sample1@test.com
password: Password

resources\views\message_send.blade.php
@extends('layouts.app')

@section('content')
    <div class="container">
        <div class="row justify-content-center">
            <div class="col-md-8">
                <div class="card">
                    <div class="card-header">{{ __('Dashboard') }}</div>


                    <div class="card-body">
                        <div class="form-group row">
                            <label for="message" class="col-md-4 col-form-label text-md-right">送信先ユーザー番号</label>

                            <div class="col-md-6">
                                <input id="userId" class="form-control @error('user_id') is-invalid @enderror" name="user_id"
                                    value="{{ old('user_id') }}">

                                @error('user_id')
                                    <span class="invalid-feedback" role="alert">
                                        <strong>{{ $message }}</strong>
                                    </span>
                                @enderror
                            </div>
                        </div>
                        <div class="form-group row">
                            <label for="message" class="col-md-4 col-form-label text-md-right">メッセージ</label>

                            <div class="col-md-6">
                                <textarea id="message" class="form-control @error('message') is-invalid @enderror"
                                    name="message" value="{{ old('message') }}" required autocomplete="message"
                                    autofocus></textarea>

                                @error('message')
                                    <span class="invalid-feedback" role="alert">
                                        <strong>{{ $message }}</strong>
                                    </span>
                                @enderror
                            </div>
                        </div>
                        <div class="form-group row mb-0">
                            <div class="col-md-6 offset-md-4 text-right">
                                <button id="messageBtn" class="btn btn-primary">
                                    送信する
                                </button>
                            </div>
                        </div>
                    </div>
                </div>{{-- <div class="card"> --}}
                {{-- 追加 --}}
                <div class="card mt-5">
                    <div class="card-header">メッセージ一覧</div>
                    <div class="card-body">
                        <ul id="message_body">
                        </ul>
                    </div>
                </div>
                {{-- 追加 --}}
            </div>
        </div>
    </div>
@endsection
@push('js')
    <script>
        window.Laravel = {!! json_encode([
    'user_id' => auth()->check() ? auth()->user()->id : null,
    'team_id' => auth()->check() ? auth()->user()->team_id : null,
]) !!};
    </script>

    <script>
        console.log(window.Laravel);

        // var socket_id = Echo.socketId();
        $.ajaxSetup({
            timeout: 3000,
            headers: {
                'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content'),
                // 'X-Socket-ID': socket_id,
            }
        });
        // window.Echo.private("message.{{ Auth::user()->team_id }}")
        window.Echo.private("message.{{ Auth::user()->team_id }}.{{ Auth::id() }}")
            .listen('MessageSend', (e) => {
                console.log(e)
            });

        // var CSRF_TOKEN = $('meta[name="csrf-token"]').attr('content');
        $(document).on('click', '#messageBtn', function() {
            // window.Echo.private("message.1");
            // window.Echo.private("message.{{ Auth::id() }}.{{ Auth::user()->team_id }}")
            var message = $('#message').val();
            var userId = $('#userId').val();
            $.ajax({
                    type: 'post',
                    datatype: 'json',
                    url: 'message_send',
                    timeout: 3000,
                    data: {
                        message: message,
                        teamId: {{ Auth::user()->team_id }},
                        userId: userId,
                        // _token: CSRF_TOKEN,
                    }
                })
                .done(function(data, textStatus, jqXHR) {
                    $('#message').val('');
                });

        });
        // window.Echo.private("message.{{ Auth::id() }}.{{ Auth::user()->team_id }}")

        // window.Echo.private('kuho_chan_nel.' + window.Laravel.user)
        // window.Echo.private("chat.{{ Auth::id() }}") //ok

        // window.Echo.private("message2")/*----1*/



        // window.Echo.private("message.{{ Auth::id() }}.{{ Auth::user()->team_id }}")



        // window.Echo.channel("message.{{ Auth::id() }}")
        //     .listen('MessageSend', (e) => {
        //         console.log(e.message)
        //     });

        // //追加
        // // getMessage();

        // window.Echo.channel('kuho_chan_nel')
        //     .listen('MessageCreated', (e) => {
        //         // this.getMessages(); // 全メッセージを再読込
        //         getMessage();
        //     });

        function getMessage() {
            $.ajax({
                    type: 'get',
                    datatype: 'json',
                    url: 'message_send',
                    timeout: 3000,
                })
                .done(function(data, textStatus, jqXHR) {
                    $result = $("#message_body"),
                        li = [];
                    $.each(data, function(index, value) {
                        li.push(`<li>id:${value.id} 内容:${value.message}</li>`);
                    })
                    $result[0].innerHTML = li.join("");
                });
        }
    </script>
@endpush
routes\channels.php
Broadcast::channel('message.{id}',function($user,$id){

    return $user;
    // return (int) $user->id === (int) $id;
});
app\Events\MessageSend.php
<?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;

class MessageSend
// +
implements ShouldBroadcast
{
    public $message;
    public $team_id;
    protected $user_id;


    use Dispatchable, InteractsWithSockets, SerializesModels;

    public function __construct($request)
    {
        // +
        // $this->dontBroadcastToCurrentUser();
        $this->message = $request["message"];
        $this->team_id = $request["teamId"];
        $this->user_id = $request["userId"];
    }

    public function broadcastOn()
    {
        return new PrivateChannel('message.' . $this->team_id . ".{$this->user_id}");
    }
}
app\Http\Controllers\HomeController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Message;
use App\Events\MessageCreated;

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

    /**
     * Show the application dashboard.
     *
     * @return \Illuminate\Contracts\Support\Renderable
     */
    public function index()
    {
        return view('home');
    }

public function message_send(Request $request)
{
    if($request->ajax()){
        $messages = Message::get();
        return $messages;
    }

    return view('message_send');
}

public function message_send_post(Request $request)
{
    // $message = Message::create([
    //     'message' => $request->message
    // ]);

// プライベートチャンネル
    // broadcast(new \App\Events\PrivateMessage($request->message))/*->toOthers()*/;

    //チャンネルイベント
    broadcast(new \App\Events\MessageSend($request->all()))->toOthers()/**/;
    // event(new MessageCreated($message));
}
}

ここで、なにかアプリを作りたい。
めちゃくちゃおもしろいサイトを発見した。

これは、挑戦せなアカン。
正直これが、紹介したくて、書いた。

キャスレーコンサルティング株式会社の松本様のアプリに挑戦してみる。

あっ、テーブル名が被っとるので、
chatsテーブルに変更する。ついでにモデルとかも作っとく
メールの送信もあるのでメールのenv設定も必要

断り
同一テーブルの多対多で挑戦して書いて見ましたが、 多対多だからといってリレーションはhasbelongsTo()だろうと考えるよりは、 最終的にはどのようにデータを取得したいかによって、データのリレーションを 考えるべきだとの結論に達しました。 リレーションの仕方が、混在していますが、ご容赦してください。 使ってみて、selefによる多対多は、ウ~ン、になると思います。 いいような、悪いような。まぁ、今回のケースでは絶対に必要なかった。 中間テーブルに'message'のような本命のデータがある場合は、素直に 中間テーブルとしての利用はよくないってことがわかった。

メールの設定

MAIL_MAILER=smtp
MAIL_HOST=smtp.gmail.com
MAIL_PORT=587
MAIL_USERNAME=sample@gmail.com
MAIL_PASSWORD=qrewrffafafyfafafa
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=exsample@gmail.com
MAIL_FROM_NAME="メッセージが届いています"

モデル・テーブル等の作成

php artisan make:model Chat -a

chatsテーブルの編集

database\migrations_create_chats_table.php
    public function up()
    {
            $table->id();
            $table->foreignId('from_user_id')
            ->constrained('users')
            ->onUpdate('cascade')
            ->onDelete('cascade');
            $table->foreignId('to_user_id')
            ->constrained('users','id')
            ->onUpdate('cascade')
            ->onDelete('cascade');
            $table->text('message');
            $table->timestamps();
        });
    }

Chatモデルの編集は
編集は不要、今回はセルフリレーションを利用して保存するため必要がない。
ユーザーモデルとのリレーションでは利用したいので、モデル自体は必要。

app\Models\Chat.php
そのまま

ルートの作成

routes\web.php
//ユーザー一覧画面
Route::get('/chat',[App\Http\Controllers\ChatController::class, 'index'])->name('chat.index');
//チャットメッセージを保存・イベントの発火・メールを送信
Route::post('/chat',[App\Http\Controllers\ChatController::class, 'store'])->name('chat.store');
//ユーザー個人とチャットする画面
Route::get('/chat/{user}',[App\Http\Controllers\ChatController::class, 'show'])->name('chat.show');

コントローラーの作成

app\Http\Controllers\ChatController.php
<?php

namespace App\Http\Controllers;

use App\Models\Chat;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use App\Models\User;
use Illuminate\Support\Facades\Mail;
use App\Events\ChatMessageRecieved;
use App\Mail\SampleNotification;
use Illuminate\Support\Arr;

class ChatController extends Controller
{
    /**
     * ログインユーザーだけがアクセス
     */
    public function __construct()
    {
        $this->middleware('auth');
    }

    /**
     * ユーザー一覧画面を表示
     */
    public function index()
    {
        $users = User::where('id', '<>', Auth::id())->get();
        return view('chat.index', compact('users'));
    }

    /**
     * 1.チャットメッセージを保存
     * 2.app\Events\ChatMessageRecieved.phpをブロードキャスト
     * 3.メールを送信
     */
    public function store(Request $request)
    {
        $auth = User::find(Auth::id());
        try {
            // メッセージデータ保存
            $auth->from_users()->attach($request->to_user_id, ['message' => $request->message]);
        } catch (\Exception $e) {
            return false;
        }
        // イベント発火
        broadcast(new ChatMessageRecieved($request->all()))->toOthers();

        // メール送信
        $mailSendUser = User::where('id', $request->input('to_user_id'))->first();
        $to = $mailSendUser->email;
        Mail::to($to)->send(new SampleNotification());
        return 'success';
    }
    /**
     * チャットルームを表示
     */
    public function show(User $user)
    {
        $messages = $user->chats()->get();
        // $messages = $user->having_mails->toArray();
        //dd($messages) データーを見比べて見てほしい。
   
        return view('chat.show', compact('user','messages'));
    }
}

リンクをナビに設定
laravelのロゴのなびをurl('/')->url('/chat')に変更

resources\views\layouts\app.blade.php
<nav class="navbar navbar-expand-md navbar-light bg-white shadow-sm">
    <div class="container">
        <a class="navbar-brand" href="{{ url('/chat') }}">
            {{ config('app.name', 'Laravel') }}
        </a>

ユーザー一覧画面の作成と編集

resources\views\chat\index.blade.php
@extends('layouts.app')
@section('title','ユーザー一覧')
@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="{{ route('chat.show',$user) }}"><button type="button" class="btn btn-primary">Chat</button></a></td>
        </tr>
        @endforeach
        </tbody>
    </table>
</div>
@endsection

image.png
チャット画面の作成と編集

resources\views\chat\show.blade.php
@extends('layouts.app')
@section('title', '{{ $user->name }}のルーム')
@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) --}}
            @foreach ($messages as $message)
                @if ($message->from_user_id == Auth::id())

                    {{-- 送信したメッセージ --}}
                    {{-- @if ($message['chats']['from_user_id'] == Auth::id()) --}}
                    <div class="send" style="text-align: right">
                        {{-- <p>{{$message['chats']['message']}}</p> --}}
                        <p>{{ $message->message }}</p>
                    </div>

                @else

                    {{-- 受信したメッセージ --}}
                    {{-- @if ($message['chats']['to_user_id'] == Auth::id()) --}}
                    <div class="receive" style="text-align: left">
                        {{-- <p>{{$message['chats']['message']}}</p> --}}
                        <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="to_user_id" value="{{ $user->id }}">
    </div>
@endsection
@push('js')
    <script src="https://cdnjs.cloudflare.com/ajax/libs/push.js/0.0.11/push.min.js"></script>
    <script type="text/javascript">
        $.ajaxSetup({
            headers: {
                'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content'),
            }
        });

        //func_1 メッセージの追加とchat.storeメソッドの発火dataは引数みたいなもんやろ
        $('#btn_send').on('click', function() {
            message = $('textarea[name="message"]').val();
            to_user_id = $('input[name="to_user_id"]').val();

            //func_1_1:textareaの値を$("#room")にappendしているだけ
            appendText = '<div class="send" style="text-align:right"><p>' + message + '</p></div> ';
            $("#room").append(appendText);

            //func_1_2:textareaの値を$("#room")にappendしているだけ
            $.ajax({
                type: 'POST',
                url: "{{ route('chat.store') }}",
                data: {
                    message: message,
                    to_user_id: to_user_id,
                }
            }).done(function(result) {
                $('textarea[name="message"]').val('');
            }).fail(function(result) {
                console.log(result);
            });
        });

        //func_2 チャンネルの購入とブロードキャストするイベントを設定する
        window.Echo.channel("chat.{{ Auth::id() }}") //ok

            .listen('.chat_event', (data) => { //.を忘れるな

                //func2_1 イベントから受け取ったデータを$("#room")にappendしている
                let appendText;
                appendText = '<div class="receive" style="text-align:left"><p>' + data.message + '</p></div> ';
                // メッセージを表示
                $("#room").append(appendText);

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

@endpush

イベントの作成

php artisan make:event ChatMessageRecieved
app\Events\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()
    {
//privatechannelからChannelへと変更 
//認可コードを設定することでイベントのブロードキャストを制限できる。
        return new Channel('chat.'.$this->request['to_user_id']);

//一回動作がどう異なるか見てほしい。
//制限しない場合、全てのユーザでイベントがブロードキャストされるのがわかる。
//return new Channel('chat');
//show.blade.phpも変更わすれずに、
//window.Echo.channel("chat") //ok
    }

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

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

    /**
     * イベントブロードキャスト名をクラス名から変更しる。
     *
     * @return string
     */
    public function broadcastAs()
    {
        return 'chat_event';
    }
}

メイラブルクラスの作成

php artisan make:mail SampleNotification
app\Mail\SampleNotification.php
<?php

namespace App\Mail;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;

class SampleNotification extends Mailable
{
    use Queueable, SerializesModels;

    public $user;

    public function __construct($user)
    {
        $this->user = $user;
    }

    /**
     * Build the message.
     *
     * @return $this
     */
    public function build()
    {
        return $this->view('mail_send')
            ->bcc('admin@sample.com')
            ->from('laravel@sample.co.jp', 'Laravel事務局')
            ->subject("{$this->user}からメールが届いているで");
    }
}

以上
機能を確認し見てる。
image.png
メールも届いている。
image.png

機能を追加してみる。
ユーザー一覧画面に未読数を表示してみる。

chatsテーブルに既読か未読かのカラムを追加するため、
chatsテーブルをリフレッシュする。
xxx_create_chats_table.phpを再編集

database\migrations\xxx_create_chats_table.php
        Schema::create('chats', function (Blueprint $table) {
            $table->id();
            $table->foreignId('from_user_id')
            ->constrained('users')
            ->onUpdate('cascade')
            ->onDelete('cascade');
            $table->foreignId('to_user_id')
            ->constrained('users','id')
            ->onUpdate('cascade')
            ->onDelete('cascade');
            $table->text('message');
//追加 isを前につけただけで疑問形になるって本当にすごいですね。
            $table->boolean('is_reading');

            $table->timestamps();
        });
相対パスを貼り付ければOKです。
php artisan migrate:refresh --path=database\migrations\xxx_create_chats_table.php

まず、一覧画面に未読数を表示できるようにする。

あっなんかバグを発見したかもしれん
->withCount('to_me_non_Reading')としているのに、
フロント側ではなぜか to_me_non__Readingになる。
なんでや。ちょっとこのままおいて置く。

app\Models\User.php
//追加
    public function to_me_non_Reading()
    {
        return $this->from_users()->where('to_user_id',Auth::id())->where('is_reading',false);
    }
app\Http\Controllers\ChatController.php
    /**
     * ユーザー一覧画面を表示
     */
    public function index()
    {
    //編集
        $users = User::where('id', '<>', Auth::id())->withCount('to_me_non_Reading')->get();

        return view('chat.index', compact('users'));
    }
    /**
     * 1.チャットメッセージを保存
     * 2.app\Events\ChatMessageRecieved.phpをブロードキャスト
     * 3.メールを送信
     */

    public function store(Request $request)
    {
        $auth = User::find(Auth::id());
        // リクエストパラメータ取得
        try {

// メッセージデータ保存 編集 is_reading = falseを追加
            $auth->from_users()->attach($request->to_user_id, ['message' => $request->message,'is_reading'=>false]);

        } catch (\Exception $e) {
            return false;
        }
        // イベント発火
        broadcast(new ChatMessageRecieved($request->all()))->toOthers();
        // メール送信
        $mailSendUser = User::where('id', $request->input('to_user_id'))->first();
        $to = $mailSendUser->email;
        Mail::to($to)->send(new SampleNotification($mailSendUser->name));

        return true;
    }

ユーザー一覧画面を編集する。chat\index.blade.php

resources\views\chat\index.blade.php
@extends('layouts.app')
@section('title','ユーザー一覧')
@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>
{{--  追加 --}}
            <th>未読数</th>
        </tr>
        </thead>
        <tbody>
        @foreach($users as $key => $user)
        <tr>
            <th>{{$loop->iteration}}</th>
            <td>{{$user->name}}</td>
            <td><a href="{{ route('chat.show',$user) }}"><button type="button" class="btn btn-primary">Chat</button></a></td>
{{--  追加 --}}
            <td id="">{{ $user->to_me_non__reading_count }}</td>

        </tr>
        @endforeach
        </tbody>
    </table>
</div>
@endsection

適当にsample1@test.com->sample2@test.comに送信してみる。
そうすると
sample2のテーブルへsample1の未読のメッセージが表示されるはず、
確認してみる。
image.png
何通か送ってみる。ちゃんとカウントされているのが確認できる。
image.png
このままでは、永遠に未読なので、既読機能をつける。
Userモデルに追加

app\Models\User.php
 // 追加
    public function non_reading_messages(){
        return  $this->hasMany(Chat::class,'from_user_id','id')->where('to_user_id',Auth::id())->where('is_reading',false);
    }

show()を編集

app\Http\Controllers\ChatController.php
    /**
     * チャットルームを表示
     * is_readingがfalseのメッセージをtrueに変更している。
     */
    public function show(User $user)
    {

// 追加
        $non_reading_messages = $user->non_reading_messages();
        $non_reading_messages->update(['is_reading'=>true]);

        $messages = $user->chats()->get();
        return view('chat.show', compact('user', 'messages'));
    }
    /**
     * チャットルームで受け取ったメッセージをreadにする
     */
    public function read(User $user)
    {
        $non_reading_messages = $user->non_reading_messages();
        $non_reading_messages->update(['is_reading' => true]);
        return 'success';
    }

機能を確認してみる。
image.png
未読数の追加をリアルタイムに変更してみる。
あ~~~。
from_user_idを外したんや。付け加えなあかん。
その修正をまずする。

resources\views\chat\show.blade.php
            //func_1_2:textareaの値を$("#room")にappendしているだけ
            $.ajax({
                type: 'POST',
                url: "{{ route('chat.store') }}",
                data: {
                    message: message,
                    to_user_id: to_user_id,
//+
                    from_user_id:{{ Auth::id() }}

                }
            }).done(function(result) {
                $('textarea[name="message"]').val('');
            }).fail(function(result) {
                console.log(result);
            });
        });

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

        return [
            'message' => $this->request['message'],
//+
            'from_user_id' => $this->request['from_user_id'],
        ];
    }

これでOK。
さっきいのチャンネルをindex画面でもブロードキャストされるように
すればいいだけ。

resources\views\chat\index.blade.php
@extends('layouts.app')
@section('title','ユーザー一覧')
@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>
            <th>未読数</th>
        </tr>
        </thead>
        <tbody>
        @foreach($users as $key => $user)
        <tr>
            <th>{{$loop->iteration}}</th>
            <td>{{$user->name}}</td>
            <td><a href="{{ route('chat.show',$user) }}"><button type="button" class="btn btn-primary">Chat</button></a></td>

 <!--  id="" を追加  -->
            <td id="user-{{ $user->id }}">{{ $user->to_me_non__reading_count }}</td>

        </tr>
        @endforeach
        </tbody>
    </table>
</div>
@endsection

{{-- 追加 --}}
@push('js')
    <script src="https://cdnjs.cloudflare.com/ajax/libs/push.js/0.0.11/push.min.js"></script>
    <script type="text/javascript">

        //func_1 チャンネルの購入とブロードキャストするイベントを設定する
        window.Echo.channel("chat.{{ Auth::id() }}") //ok
            .listen('.chat_event', (data) => { //.を忘れるな
            console.log(data);

                //func1_1 イベントが発火したときの処理
                 non_reading_count = Number($('#user-'+data.from_user_id).text());
               $('#user-'+data.from_user_id).text(non_reading_count + 1);

                // //func1_2 ブラウザへプッシュ通知
                // Push.create("新着メッセージ", {
                //     body:'メッセージが届いてます',
                //     timeout: 8000,
                //     onClick: function() {
                //         window.focus();
                //         this.close();
                //     }
                // })
            });
    </script>
@endpush

show.blade.phpで受け取ったメッセージもtrueにする必要があるため
メッセージを受け取ったときにtrueにするようajax送信を追加する。

        //func_2 チャンネルの購入とブロードキャストするイベントを設定する
        window.Echo.channel("chat.{{ Auth::id() }}") //ok

            .listen('.chat_event', (data) => { //.を忘れるな

                //func2_1 イベントから受け取ったデータを$("#room")にappendしている
                let appendText;
                appendText = '<div class="receive" style="text-align:left"><p>' + data.message + '</p></div> ';
                // メッセージを表示
                $("#room").append(appendText);

//ajax送信でサーバーのchat.readメソッドを発火させるてやる。
                $.ajax({
                    type: 'POST',
                    url: "{{ route('chat.read',$user) }}",
                }).done(function(result) {
                    console.log(result);
                }).fail(function(result) {
                    console.log(result);
                });

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

image.png
ちゃんと動作しているな。OK。IDを変更しても動作はいけるは。

プレゼンスチャネル

別のユーザーが同じページを表示しているときにユーザーに通知したり、チャットルームの住民を一覧表示したりするなど、強力なコラボレーションアプリケーション機能を簡単に構築できます
ユーザーがチャンネルへの参加を認可されている場合に、trueを返しません。代わりに、ユーザーに関するデータの配列を返す必要があります
ユーザーがプレゼンスチャンネルへの参加を許可されていない場合は、falseまたはnullを返す必要があります。

Laravel8.xブロードキャスト公式サイト
Broadcast::channel('chat.{roomId}', function ($user, $roomId) {
    if ($user->canJoinRoom($roomId)) {
        return ['id' => $user->id, 'name' => $user->name];
    }
});

ウ~ン
要するに、認証がOKの時はプライベートチャンネルの時とは違い 
ユーザーの配列を返さなあかんと
falseのときはええよと。

Laravel8.xブロードキャスト公式サイト
Broadcast::channel('chat.{roomId}', function ($user, $roomId) {
    if ($user->canJoinRoom($roomId)) {
        return ['id' => $user->id, 'name' => $user->name];
    }
});

↑これこのまま、利用できるよね。
問題はこれやcanJoinRoom($roomId)<-説明もなしにこんな事するな。
なんにしろ、
ユーザがこの部屋に入る権限があるのかどうかのif文をもうけて、
OKなら、配列を返せばいいのか。

routes\channels.php
Broadcast::channel('chat.{toilet}', function ($user,$toilet) {
    if ($user->sex == $toilet)) {
        return $user;
    }
});

↑公式サイトに載せてほしいぐらいだ。

なんか適当なアプリで試してみたい。

チームのチャットルームを作って確認してみる。
松本様のアプリにルーム機能を追加してみる。

まず、チームIDカラムをchatsテーブルに追加
chatsテーブルをリフレッシュする必要がある。

        Schema::create('chats', function (Blueprint $table) {
            $table->id();
            $table->foreignId('from_user_id')
            ->constrained('users')
            ->onUpdate('cascade')
            ->onDelete('cascade');

            //変更
            $table->foreignId('to_user_id')->nullable()->constrained("users")->cascadeOnUpdate()->nullOnDelete();

            //追加
            $table->integer('team_id')->nullable();

            $table->text('message');
            $table->boolean('is_reading');
            $table->timestamps();
        });
php artisan migrate:refresh --path=相対パス

ひとまずイベントは無視してルームでメッセージを送信して保存、表示できるようにする。

resources\views\chat\index.blade.php
@extends('layouts.app')
@section('title', 'ユーザー一覧')
@section('content')
    <div class="container">
{{-- 追加 --}}
        <nav aria-label="breadcrumb">
            <ol class="breadcrumb">
                <li class="breadcrumb-item">貴方は{{ Auth::user()->team_id }}番です</li>
                <li class="breadcrumb-item ml-auto"><a href="{{ route('chat.room',1) }}">1番のお部屋</a></li>
                <li class="breadcrumb-item"><a href="{{ route('chat.room',2) }}">2番のお部屋</a></li>
                <li class="breadcrumb-item"><a href="{{ route('chat.room',3) }}">3番のお部屋</a></li>
            </ol>
        </nav>

        {{-- チャット可能ユーザ一覧 --}}
        <table class="table">
            <thead>
                <tr>
                    <th>#</th>
                    <th>Name</th>
                    <th></th>
                    <th>未読数</th>
                    <th>チーム番号</th>
                </tr>
            </thead>
            <tbody>
                @foreach ($users as $key => $user)
                    <tr>
                        <th>{{ $loop->iteration }}</th>
                        <td>{{ $user->name }}</td>
                        <td><a href="{{ route('chat.show', $user) }}"><button type="button"
                                    class="btn btn-primary">Chat</button></a></td>
                        <td id="user-{{ $user->id }}"><span>{{ $user->to_me_non__reading_count }}</span></td>
{{-- 追加 --}}
                        <th>{{ $user->team_id }}</th>
                    </tr>
                @endforeach
            </tbody>
        </table>
    </div>
@endsection
@push('js')
    <script src="https://cdnjs.cloudflare.com/ajax/libs/push.js/0.0.11/push.min.js"></script>
    <script type="text/javascript">
        //func_2 チャンネルの購入とブロードキャストするイベントを設定する
        window.Echo.channel("chat.{{ Auth::id() }}") //ok
            .listen('.chat_event', (data) => { //.を忘れるな
                console.log(data);

                //func2_1 イベントが発火したときの処理
                non_reading_count = Number($('#user-' + data.from_user_id).text());
                $('#user-' + data.from_user_id).text(non_reading_count + 1);
            });
    </script>

@endpush

image.png
ルーム画面を作成する。
ルートの追加

routes\web.php
//チャットルーム画面
Route::get('/chat/room/{id}',[App\Http\Controllers\ChatController::class, 'room_show'])->name('chat.room_show');
//チャットルームでメッセージを保存・イベントの発火させるファンクション
Route::post('/chat/room/{id}',[App\Http\Controllers\ChatController::class, 'room_store'])->name('chat.room_store');

チャットルーム画面の作成
コントローラーの作成

app\Http\Controllers\ChatController.php
    /**
     * ルーム画面を表示
     * 自分が所属するチームのメッセージを取得
     */
    public function room_show($room_id)
    {
        $team_id = Auth::user()->team_id;
        if($room_id == $team_id){
            $messages = Chat::where('team_id', $team_id)->get();
        }else{
            $messages =[];
        }

        return view('chat.room', compact('room_id', 'messages'));
    }
    /**
     * メッセージの保存
     */
    public function room_store(Request $request,$room_id)
    {
        $team_id = Auth::user()->team_id;
        if($room_id != $team_id){
            return 'error';
        }
        try {
            $chat = Chat::create([
                'from_user_id' => Auth::id(),
                'message' => $request->message,
                'is_reading' => false,
                'team_id' => $team_id,
            ]);
        } catch (\Exception $e) {
            return $e;
        }
        return 'success';
    }

chat\room.blade.phpの作成と編集

resources\views\chat\room.blade.php
@extends('layouts.app')
@section('title', '{{ $room_id }}番号のお部屋')
@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 $message)
                @if ($message->from_user_id == Auth::id())

                    {{-- 送信したメッセージ --}}
                    <div class="send" style="text-align: right">
                        <p>{{ $message->message }}</p>
                    </div>

                @else
                    {{-- 受信したメッセージ --}}
                    <div class="receive" 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>
    </div>
@endsection
@push('js')
    <script src="https://cdnjs.cloudflare.com/ajax/libs/push.js/0.0.11/push.min.js"></script>

    <script type="text/javascript">
        $.ajaxSetup({
            headers: {
                'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content'),
            }
        });

        //func_1 メッセージの<body>への追加とajax送信
        $('#btn_send').on('click', function() {
            message = $('textarea[name="message"]').val();
            //func_1_1:textareaの値を$("#room")にappendしているだけ
            appendText = '<div class="send" style="text-align:right"><p>' + message + '</p></div> ';
            $("#room").append(appendText);

            //func_1_2:ajax送信でchat.room_storeメソッドを発火させる。
            $.ajax({
                type: 'POST',
                url: "{{ route('chat.room_store', $room_id) }}",
                data: {
                    message: message,
                    from_user_id: {{ Auth::id() }}
                }
            }).done(function() {
                $('textarea[name="message"]').val('');
            }).fail(function(result) {
                console.log(result);
            });
        });
    </script>
@endpush

メッセージの保存と、表示ができている。
image.png
通信を加えてやればいいだけだが、、取り敢えず公式サイトのコードをコピペやろ。

resources\views\chat\room.blade.php
        //func_2 チャンネルの購入とブロードキャストするイベントを設定する
        window.Echo.join("chat.{{ $room_id }}") //ok
            .here((users) => {
                console.log(users);
            })
            .joining((user) => {
                console.log(user.name);
            })
            .leaving((user) => {
                console.log(user.name);
            })
            .error((error) => {
                console.error(error);
            })
            .listen('.chat_event', (data) => { //.を忘れるな
                //func2_1 イベントから受け取ったデータを$("#room")にappendしている
                let appendText;
                appendText = '<div class="receive" style="text-align:left"><p>' + data.message + '</p></div> ';
                // メッセージを表示
                $("#room").append(appendText);

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

確認してみる。
image.png
あっ、認証エラーがでた。
チャンネルを登録する。

routes\channels.php
Broadcast::channel('chat.{room_id}',function($user,$room_id){
if($user->team_id == $room_id){
    return $user;
}
    // return (int) $user->id === (int) $id;
});

ユーザーの配列がコンソールされた。
image.png

これが発火したものと思われる。
            .here((users) => {
                console.log(users);
            })

公式サイトのそれぞれのメソッドの説明は以下の通り。

  • hereコールバックは、チャンネルへ正常に参加するとすぐに実行され、現在チャンネルにサブスクライブしている他のすべてのユーザーのユーザー情報を含む配列を受け取ります。
  • joiningメソッドは、新しいユーザーがチャンネルに参加したときに実行され、
  • leavingメソッドは、ユーザーがチャンネルを離れたときに実行されます。
  • errorメソッドは、認証エンドポイントが200以外のHTTPステータスコードを返した場合や、返されたJSONの解析で問題があった場合に実行されます。

edgeからもアクセスしてみる。
image.png
Chromeの方では 入室してきたユーザーの名前が表示された。

これが発火したものと思われる。
            .joining((user) => {
                console.log(user.name);
            })

edgeの方では、ユーザーの全データが返されてきた。
公式サイトの説明通りやね。ただ、ホンマに全データを返してきているので、
多分これでコントロールできるはず。

routes\channels.php
Broadcast::channel('chat.{room_id}',function($user,$room_id){
if($user->team_id == $room_id){

    return ['id' => $user->id, 'name' => $user->name];

    // return $user;
}

ok here(),
  joining(),
  leaving()
  の戻り値が無事コントロールできました。
image.png
leaving()を検証してみる。検証用コードに変更する。

resources\views\chat\room.blade.php
        //func_2 チャンネルの購入とブロードキャストするイベントを設定する
        window.Echo.join("chat.{{ $room_id }}") //ok
            .here((users) => {
                console.log(users);
            })
            .joining((user) => {
                console.log(user,'さんが入室しました。');
            })
            .leaving((user) => {
                console.log(user,'さんが退室しました。');
            })
            .error((error) => {
                console.error(error);
            })
            .listen('.chat_event', (data) => { //.を忘れるな
                //func2_1 イベントから受け取ったデータを$("#room")にappendしている
                let appendText;
                appendText = '<div class="receive" style="text-align:left"><p>' + data.message + '</p></div> ';
                // メッセージを表示
                $("#room").append(appendText);

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

image.png
全部で.leaving()が発火した。
image.png
ただ、気になったのが、リロードボタンを押すと。
joining() と leaving()が両方発火した。
image.png
以上 leaving()の検証終了
メッセージの保存時に、イベントを発火させる。

app\Http\Controllers\ChatController.php
    public function room_store(Request $request,$room_id)
    {
        $team_id = Auth::user()->team_id;
        if($room_id != $team_id){
            return 'error';
        }
        try {
            // メッセージデータ保存
            $chat = Chat::create([
                'from_user_id' => Auth::id(),
                'message' => $request->message,
                'is_reading' => false,
                'team_id' => $team_id,
            ]);
        } catch (\Exception $e) {
            return $e;
        }
//追加(引数に$team_idも追加している。)
        // イベント発火
        broadcast(new ChatMessageRecieved($request->all(),$team_id))->toOthers();
      return 'success';
    }

イベントにPresenceChannelを追加する。

app\Events\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 $request;
    protected $team_id;

    public function __construct($request,$team_id=null)
    {
        $this->request = $request;
//追加
        $this->team_id = $team_id;
     }

    /**
     * イベントをブロードキャストすべき、チャンネルの取得
     */
    public function broadcastOn()
    {
//if文で場合分け
        if(empty($this->team_id)){
            return new Channel('chat.'.$this->request['to_user_id']);
        }else{
//追加
            return new PresenceChannel('chat.'.$this->team_id);
        }
    }

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

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

    /**
     * イベントブロードキャスト名
     */
    public function broadcastAs()
    {
        return 'chat_event';
    }
}

通信もOK。
image.png
別のチームで検証してみる。検証用にコードを変更する。

    public function room_store(Request $request,$room_id)
    {
        $team_id = Auth::user()->team_id;
//ここをコメントアウト
        // if($room_id != $team_id){
            // return 'error';
        // }

1番のチームから3番の部屋で投稿してみる。
まず、入室しても、チーム3番のhere()は発火せず。
image.png
次は送信 よし、3番の部屋では発火せず。
image.png
代わりに1番のへやで発火したwので、コードを変更する。

resources\views\chat\room.blade.php
        //func_2 チャンネルの購入とブロードキャストするイベントを設定する
//変更
        window.Echo.join("chat.{{ $room_id }}.{{ Auth::user()->team_id }}") 
app\Http\Controllers\ChatController.php
//追加(引数に$room_idを追加している。)
        broadcast(new ChatMessageRecieved($request->all(),$room_id,$team_id))->toOthers();
app\Events\ChatMessageRecieved.php

    protected $request;
    protected $team_id;
//追加
    protected $room_id;

    public function __construct($request,$team_id=null,$room_id=null)
    {
        $this->request = $request;
        $this->team_id = $team_id;
//追加
        $this->room_id = $room_id;
     }

    /**
     * イベントをブロードキャストすべき、チャンネルの取得
     */
    public function broadcastOn()
    {
        if(empty($this->team_id)){
            return new Channel('chat.'.$this->request['to_user_id']);
        }else{
//変更
            return new PresenceChannel('chat.'.$this->room_id.".{$this->team_id}");
        }
    }

よしエラーも解消されている。
\(^o^)/ヤッターマン
ちがう、
これでは、プレゼンスチャンネルを使っている意味がまったくない。
ウ~ン、そういえば、この機能↓に応用できるんとちゃうか?
いやむしろ、このアプリはプレゼンスチャンネルで作るべきとちゃうか?
と思い。

上をプレゼンスチャンネルを使って書き換えてみる。
に挑戦してみる。
久保様プレゼンス、リアルタイムでオンライン通知する機能をつくるに挑戦してみる。

まずは、久保様のアプリをそのまま作ってから書き換えてみる。

まずは支持通りイベントを作る

php artisan make:event UserAccessed
<?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;

class UserAccessed
/* ㊦絶対に忘れたらあかん\(^o^)/ */
implements ShouldBroadcast // 👈 ここを追加しました
/* ↑👈確認よし(ΦωΦ) */
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    /**
     * Create a new event instance.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    public function broadcastOn()
    {
        $channel_name = 'online_users'; // 👈 ここを追加しました
        return new Channel($channel_name); // 👈 ここを追加しました
    }
}

え~と次は
ユーザーが最後にアクセスした日時」を保存できるようにしますか
ユーザーテーブルにlast_accessed_atカラムを追加

database\migrations\2014_10_12_000000_create_users_table.php
        Schema::create('users', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->integer('team_id')->nullable();
 // + 
            $table->dateTime('last_accessed_at')->nullable();

            $table->rememberToken();
            $table->timestamps();
        });

次はdbを初期化するのか。

php artisan migrate:fresh --seed

次は、

  1. last_accessed_atをCarbonインスタンスに変換
  2. is_online() 最終アクセスが15分以内かどうか判断するAccessor関数を作成
app\Models\User.php
    protected $casts = [
        'email_verified_at' => 'datetime',
//1. last_accessed_atをCarbonインスタンスに変換
        'last_accessed_at' => 'datetime' // 👈 ここを追加しました
    ];

//↓なんだコレ https://blog.capilano-fw.com/?p=2114#appends
    protected $appends = [
//これは意味がないような気がする・・・。<-後でこの関数の凄さを知りました。
        'is_online' // 👈 ここを追加しました
    ];

//2. is_online() 最終アクセスが15分以内かどうか判断するAccessor関数を作成
    // Accessor
    public function getIsOnlineAttribute() // 👈 ここを追加しました
    { 
// ユーザーのlast_accessed_atカラムを取得する。
        $last_accessed_at = $this->last_accessed_at;

        return (!is_null($last_accessed_at) &&
            //↓なんだコレ https://blog.capilano-fw.com/?p=867#diffInMinutes
//diffInMinutes()でnow()とlast_accessed_atとの分差を取得する
            now()->diffInMinutes($last_accessed_at) <= 15 // 最終アクセスが15分以内の場合
        );
    }

※ なお、「15分」の部分はconfig/app.phpなどで共通化しておくほうが後で便利かと思います。
とあるので、挑戦してみる。
https://blog.capilano-fw.com/?p=320#env_config

config\app.php
<?php

return [
//とりあえず一番上に追加した。
    'is_within' => '15',
Userモデル
    // Accessor
    public function getIsOnlineAttribute()
    {
        $last_accessed_at = $this->last_accessed_at;
        return (!is_null($last_accessed_at) &&
// 修正
            now()->diffInMinutes($last_accessed_at) <= config('app.is_within')
        );
    }

次は、
ログインしていたら必ず実行されるイベント・リスナーを追加してlast_accessed_atがその都度更新されるようにします。

ログイン時に実行されるイベントを作成する(イベント・リスナーを作成する)
php artisan event:generate

すると、ファイルが作成されますので、中身を以下のようにします。
app/Listeners/LogAuthenticated.php
とあるが・・・、ない。作成されない(T_T)。
あ~~~。
作成するにはまず、登録する必要があるみたい。

app\Providers\EventServiceProvider.php
    protected $listen = [
        Registered::class => [
            SendEmailVerificationNotification::class,
        ],
//+
        // ログイン成功したら実行するイベントファイル
        'Illuminate\Auth\Events\Authenticated' => [
            'App\Listeners\LogAuthenticated',
        ],
    ];
再度実行
php artisan event:generate

無事ファイルが作成されました。
app\Listeners\LogAuthenticated.php

app\Listeners\LogAuthenticated.php
<?php
namespace App\Listeners;

use App\Events\UserAccessed; // 👈 ここを追加しました
use Illuminate\Auth\Events\Authenticated;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;

class LogAuthenticated
{
    public function __construct()
    {
        //
    }

    public function handle(Authenticated $event)
    {
        $user = $event->user;
//  最終アクセスから15分以上なら、
        if(!$user->is_online) { 
//  イベントを発火して同じチャンネル購入者にイベントをブロードキャストする。
            UserAccessed::dispatch();
        }

//  ページが遷移される度に、アクセス日時を更新させる。
        $user->last_accessed_at = now(); 
        $user->save();
    }
}

今度は、ルートを作成すると

routes/web.php
Route::get('users', function(){ return \App\Models\User::get(); });
Route::get('online_users', function(){ return view('online_users'); });

今回はテストなので省略して書いていますが、本番環境ではコントローラーを使うことをおすすめします。
とあるので、編集する。

routes/web.php
Route::get('online_users',[App\Http\Controllers\HomeController::class, 'online_users'])->name('online_users.index');
Route::get('online_users/users',[App\Http\Controllers\HomeController::class, 'online_users_get'])->name('online_users_get');
app\Http\Controllers\HomeController.php
    public function online_users(){
        return view('online_users');
    }
    public function online_users_get(){
        $users = User::get(['id','name','last_accessed_at']);
        return $users;
    }
resources\views\online_users.blade.php
@extends('layouts.app')
@section('content')
    <div class="container">
        <div class="row justify-content-center">
            <div class="col-md-8">
                <div class="">
                <h1 class=" text-info">リアルタイムオンライン通知</h1>
                </div>
                <div class="card">
                    <div class="card-header">ユーザー一覧</div>

                    <div class="card-body" style="background-color:rgba(59, 130, 246, 0.1)">
                        <small class="text-muted">ユーザー</small>
                        <div id="users">


                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
@endsection
@push('js')

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

        Echo.channel('online_users')
            .listen('UserAccessed', e => {
               getUsers(); // 👈 リアルタイム通知があれば自動更新
            });
        function getUsers() {
            $.ajax({
                    type: 'get',
                    datatype: 'json',
                    url: 'online_users/users',
                    timeout: 3000,
                })
                .done(function(data, textStatus, jqXHR) {
                    $result = $("#users"),
                        users = [];
                    $.each(data, function(index, value) {
                        user = `<div id="online_userId_${value.id}" class="row justify-content-between no-gutters line-highlight mt-2">
                                <div class="text-primary">${value.name}</div>`;
                        if (value.is_online) {
                            user += `<div class="text-success">オンライン</div>`;
                        } else {
                            user += `<div class="text-white">オフライン</div>`;
                        }
                        user += `</div>`
                        users.push(user);
                    });
                    $result[0].innerHTML = users.join("");
                });
        }
    </script>
@endpush

ログアウトの通知もやってみるに挑戦してみる
まずは、イベントの登録から

app\Providers\EventServiceProvider.php
    protected $listen = [
        Registered::class => [
            SendEmailVerificationNotification::class,
        ],

        // ログイン成功したら実行
        'Illuminate\Auth\Events\Authenticated' => [
            'App\Listeners\LogAuthenticated',
        ],
//+
            // 👇 ここを追加しました
    'Illuminate\Auth\Events\Logout' => [
        'App\Listeners\LogSuccessfulLogout',
    ],
php artisan event:generate
<?php

namespace App\Listeners;

use App\Events\UserAccessed;
use Illuminate\Auth\Events\Logout;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;

class LogSuccessfulLogout
{

    public function __construct()
    {
        //
    }

    public function handle(Logout $event)
    {
        $user = $event->user;
        $user->last_accessed_at = null;
        $user->save();

        UserAccessed::dispatch(); // Pusherへ通知
    }
}

ログイン時
image.png
ログアウト時
image.png
あとは、15分以上、last_access_atが更新のないユーザーのis_onlineが
他のユーザーが更新する時に、falseに書き換えられることになります。
それによって、ログアウトと判断される仕組みです。

素晴らしい。

えーとこれをプレゼンスチャンネルで書き換えたいんだが、どうしたらいんだ?
サイト全体でプレゼンスチャンネルを使う必要があるよね。
お部屋に入ったときにhere()が発火
leave()が発火する条件は
とにかく、お部屋から出ると発火する

こうやっても、room<=>indexする度に発火する。
//resources\views\chat\room.blade.php
window.Echo.join("chat.1.{{ Auth::user()->team_id }}");

//resources\views\chat\index.blade.php
window.Echo.join("chat.1.{{ Auth::user()->team_id }}");

発火の嵐だよ。
あまり、使わん方がええような気がしてきた。
限定すべきだと思う。

それ以上に protected $appends にもの凄く感動した。

Userモデル
    protected $appends = [
//これは意味がないような気がする・・・。<-後でこの関数の凄さを知りました。
        'is_online' // 👈 ここを追加しました
    ];

    // Accessor
    public function getIsOnlineAttribute()
    {
        $last_accessed_at = $this->last_accessed_at;//$thisはuserのこと
        return (!is_null($last_accessed_at) &&
// 修正
            now()->diffInMinutes($last_accessed_at) <= config('app.is_within')
        );
    }

正直、意味不明だった。
これ知っているか知ってないかでプログラミングの効率がすごく変わるし、
ますます、コードが簡単にかけるようになるよね。

1
5
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?