リファレンスサイト
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をインストールと環境構築
composer require pusher/pusher-php-server
npm install --save-dev laravel-echo pusher-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に入力
#(上の方に外れておいてある忘れずに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個あるから注意、上は外れている。
//こっちは外れている
Illuminate\Broadcasting\BroadcastServiceProvider::class,
/*
* Application Service Providers...
*/
App\Providers\BroadcastServiceProvider::class, // コメントを解除しました
とりあえずイベントをつくる
イベントを作成して、コントローラー等からイベントを発火させる。
イベントに登録されてあるチャンネルが
同じチャンネルのlaravel-echo-pusher-js
を発火させる。
と今は理解している。
php artisan make:event helloWorld
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');
}
}
イベントを発火するページ(ルート)を作る。
Route::get('/', function () {
//イベントを発火
broadcast(new \App\Events\helloWorld());
//welcomeページをリターン
return view('welcome');
});
resources\views\welcome.blade.php
の(/body)の上に追記
{{-- 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には繋がっていた。
npm run devの結果がうまく反映されていないのではないか。
version()を使うため、asset関数からmix関数へ変更する。
mix.js('resources/js/app.js', 'public/js')
.sass('resources/sass/app.scss', 'public/css')
.version() /*追加 */
.sourceMaps();
//asset()からmix()へ変更
<script src="{{ mix('js/app.js') }}"></script>
'mix_url' => env('MIX_ASSET_URL', '/laravel-echo/public/'),
npm run dev
さいど、検証してみる。
あっ・・・console.log()にチラッとでて遷移されて消える。
laravelのキャッシュ対策のためには、mix関数を優先して使うべき。
これはjsの読み込むタイミングが早いためだと思う。しっかりとイベントが発火して、jsを発火させている。のか、それとも、イベントとは関係なくjsが発火しているのか・・
確認するため、
Chrome とedgeで立ち上げて、edgeでアクセスして、Chrome でjsが発火するか見てみる。
イベントがjsを発火させていた。ヤッターマン\(^o^)/
今度は、ajaxでイベントを発火させてみる。
Route::get('/', function () {
// broadcast(new \App\Events\helloWorld());
//welcomeページをリターン
return view('welcome');
});
Route::get('/hello', function () {
//ajaxでイベントを発火させる。
broadcast(new \App\Events\helloWorld());
});
<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>
###broadcastWith()を使用してみる。
broadcastWith()のlaravelの公式サイトの説明
イベントのペイロードとしてpublicプロパティはすべて自動的にシリアライズされます。これによりJavaScriptアプリケーションより、publicデータにアクセスできます。ですから、たとえば、あるイベントにEloquentモデルを含む、publicの$userプロパティがあれば、そのイベントのブロードキャストペイロードは、次のようになります。
{
"user": {
"id": 1,
"name": "Patrick Stewart"
...
}
}
しかしながら、ブロードキャストペイロードをより上手くコントロールしたければ、そのイベントへbroadcastWithメソッドを追加してください。このメソッドから、イベントペイロードとしてブロードキャストしたいデータの配列を返してください。
意味不明。
そもそも論、ペイロードってなにやねん。
それをコントロールするってどういうこと。
けど、使えば何となく分かるはず。
取り敢えず、使えるのか確認する。
そういえば、
console.log(e)がカラの配列を返していた。
公式サイトのプロジェクトページ
なんのための、eやねん。
取り敢えず、broadcastWith()を使ってみる。
###helloWorldイベントにbroadcastWith()を追加
public function broadcastOn()
{
// return new PrivateChannel('channel-name');
return new Channel('hello');
}
/**
*追加
*/
public function broadcastWith()
{
return [
'message' => 'hello world 2',
];
}
●eにdataをペイロードしてしまいましたか?。そういう意味か?。
●ちょっと違うよな、コントロールするってどういう意味やねんってなる。
次に、あるイベントにEloquentモデルを含む、publicの$userプロパティがあれば
、
そのイベントのブロードキャストペイロードは、次のようになります。
とあるので、helloWorldイベントにUserモデルのプロパティを作成する。
データーベースを作成せなあかんめんどくさい。
DB_CONNECTION=sqlite
# DB_HOST=127.0.0.1
# DB_PORT=3306
# DB_DATABASE=laravel_echo2
# DB_USERNAME=root
# DB_PASSWORD=
type nul > database\database.sqlite
touch database/database.sqlite
php artisan migrate
//コメントアウトを解除
\App\Models\User::factory(10)->create();
php artisan db:seed
// +
use App\Models\User;
class helloWorld2
-------
// +
public $users;
public function __construct()
{
// +
$this->users = User::all();
}
//-
/**
*動作確認のためコメントアウト
*/
// public function broadcastWith()
// {
// return [
// 'message' => 'hello world 2',
// ];
// }
あっなんか、デフォルトだと、イベントのプロパティのデータを全部返すみたい。
じゃないわ、ペイロードするみたい。これを、細かくコントロールするのが、
broadcastWith()メソッド。
/**
*先程のコメントアウトを解除
*/
public function broadcastWith()
{
return [
'message' => 'hello world 3',
];
}
はい、無事ペイロードのコントロールを確認できました。
\(^o^)/ヤッターマン
これは、メッセージを送って相手側でリアルタイム発火することができるんじゃねーか。
検証してみる。
###まずはイベントの作成
これがなくては、始まらない。
php artisan make:event MessageSend
作ったら、なにがなんでもこれが最初やろ
------------------------------
class MessageSend
implements ShouldBroadcast // +
{
------------------------------
}
<?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でええやろう。
<!-- 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>
ルートの作成
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を使い回す)
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を作成)
@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
エッジから送信してみる。
無事、Chrome で通信確認。
スマホからも通信してみる。
通信できてるな。\(^o^)/ヤッターマン
うーん・・・・。
通信といえば、通信しているが・・・。
やったことは、イベントのプロパティにデータを持たせただけ、
感覚的には、それを表示しているに過ぎない・・・・。
これでいいのか。
なんか、適当なアプリを作って、確認したい。
九保すこひ様のリアルタイム・チャットをつくるに挑戦してみる
[九保すこひ様のリアルタイム・チャットをつくる]
(https://blog.capilano-fw.com/?p=1418)
↑データーベースとの連携もあって本当にちょうどよい。
本当に九保すこひ様いつもありがとうございます。
九保やすこひ様のサイトは他のどのサイトよりも安心して挑戦できる。
その他にも、laravelのチートシートが沢山あって、公式サイトなんていらないぐらいだ。
form等は先程のformを使い回すとして、保存するテーブルがいる。
###モデルとテーブルを作成する。
php artisan make:model Message -a
protected $fillable = [
'message',
];
public function up()
{
Schema::create('messages', function (Blueprint $table) {
$table->id();
$table->string('message');
$table->timestamps();
});
}
php artisan migrate
###ルートを作成する
これは、先程のルートを使い回す。
###コントローラーを作成する
これも先程のメソッドを使い回して編集する。
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
class MessageCreated
// +
implements ShouldBroadcast
{
もう、ここは、チャンネル名を変えてコピペ。
うん、イベントのプロパティにデータを渡してるだけ。
<?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を書き換える。
@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
無事、通信できている。\(^o^)/ヤッターマン
やったことは、イベントのプロパティにデータをもたせただけ。
なんて、簡単なんや。
認証中ユーザーの回避
broadcast関数には->toOthers()メソッドってがあって、
ブロードキャストの受取人から現在のユーザーを除外できる
らしい。
取り敢えず、できるか試してみる。
今の状態は
エッジから送ってみると、送ったがわにも、ブロードキャストされているのがわかる。
->toOthers()を追記して検証してみる。
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));
}
//コメントアウト
//追加
// getMessage();
では、試してみる。edgeから送信
受け取ったChrome ではjsが発火したが、送ったedgeではjsは発火しなかった。
今度は、Chrome から送信
今度は、逆の関係になった。
これは、\(^o^)/ヤッターマンではないか?
と思ったら。
設定のところに
VueとAxiosを使用しない場合、JavaScriptアプリケーションでX-Socket-IDヘッダを送信するように、設定する必要があります。
とある。
(゚Д゚)ハァ?
僕が、やったのは、$ajax送信
その上に
。VueとAxiosを使用していれば、X-Socket-IDヘッダとして、送信する全リクエストへ自動的に付加されます。そのため、toOthersメソッドを呼び出す場合、LaravelはヘッダからソケットIDを取り除き、そのソケットIDを使い全接続へブロードキャストしないように、ブロードキャスタに対し指示します。
とある。
これは、どういうこと?
今の所、送信時のheadersはX-CSRF-TOKENだけや。
$.ajaxSetup({
timeout: 3000,
headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content'),
}
});
公式サイトの説明では、headersにX-Socket-IDを設定しなければ、
vue axios通信以外では->toOthers()は使えんよとある。
要するに、↓のような設定をする必要があるはずや。
var socket_id = Echo.socketId();
$.ajaxSetup({
timeout: 3000,
headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content'),
'X-Socket-ID': socket_id
}
});
↑こんな設定はしておらん。
どういうことや。
いっかいこれでやってみる。
逆もOKや、動作公式サイトどうりに、機能している。
いや違うねん。
俺は見せたかったんや。
'X-Socket-ID': socket_id
がある場合とない場合で、
うーん普通にformで送信してみる。
あかん、遷移してまう、めんどくせぇ
ajax送信におけるheadersの中身を確認してみる。
普通こっちだわな。
普通に、自動で追記してくれてる・・・・。
結論、ajax送信のときも
X-Socket-IDをheaderに自動で追記してくれる。
(laravel8時)
あっおれ 5.6の公式サイト 見てる・・・・。
改めてlaravel8を見てみる怖い・・・
グローバルAxiosインスタンスを使用してJavaScriptアプリケーションからHTTPリクエストを作成している場合、ソケットIDはすべての送信リクエストにX-Socket-IDヘッダとして自動的に添付されます。
なに?
グローバルAxiosインスタンスを使用して
なに?コレ?
グローバルAxiosインスタンス??
そういえば、resources\js\bootstrap.jsにそれっぽいのがある。
window.axios = require('axios');
いったん、これを外してみた。
npm run dev
あ、あかん。
エラーになって最初から動かん。
ということです。
イベントの方でも->toOthers()メソッドを指定できる。
$this->dontBroadcastToCurrentUser()
試してみる。toOthers()をコメントアウトして、
broadcast(new \App\Events\MessageSend($request->message))/*->toOthers()*/;
イベントに$this->dontBroadcastToCurrentUser();を追記
public function __construct($message)
{
// +
$this->dontBroadcastToCurrentUser();
$this->message = $message;
}
コネクションのカスタマイズ
(ここ読む必要ない)
複数のブロードキャスト接続とやりとりしており
viaメソッドを使ってどの接続にイベントをプッシュするか指定できます
例として、
broadcast(new OrderShipmentStatusUpdated($update))->via('pusher');
とある。
なんか使い方が、想定と違う。
イベントに複数チャンネルがあって、それを選択できるのかと思ったが、違う。
うーん、
とりあえず、もう一個チャンネルを公式サイトでつくってみる。
あかんつまった。
アプリケーションが複数のブロードキャスト接続とやりとりしており、デフォルト以外のブロードキャスタを使いイベントをブロードキャストしたい場合は、viaメソッドを使ってどの接続にイベントをプッシュするか指定できます。
デフォルトのチャンネルがcomposed-flower-493で、other_messageがデフォルト以外のブロードキャスタと仮定して、viaメソッドを使って切り替えるのかと思ったけど。
そもそも論、デフォルト以外のブロードキャスタをどうやって、登録するのか?
サンプルもないし。
ちょっと置いとく、
(ここまで)
プライベートチャンネル
概要
プライベートチャンネルをサブスクライブするには、ユーザーがそのチャンネルでリッスンする認証と認可を持っている必要があります。
(´Д`)ハァ…相変わらず意味不明
なんや、認証と認可ってまぁ、
認証と認可が2ついるんってことだけわかったわ。
まぁ、いつものごとく試してみたらわかるんや。
まずは、プライベートイベントの作成やろ・・・と思ったけど、
ここは、サーバー側から把握するよりもフロント側から把握した方が、
理解が早いんとちゃうか。
取り敢えず、先程のMessageSendイベントのchannelをprivateに書き換えてみるか。
// 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がないとかいってる。
イベントは何も発火させてへんのに、勝手にや、アクセス時や。
これが、認証って意味か?
とりあえず、404って言うことはルートがないってことや、これは、xamppから
アクセスしてるかやろ。
取り敢えず、ルートを修正してやるか。
承認エンドポイントのカスタマイズってのができるらしい。
公式サイトのコードauthEndpoint: '/custom/endpoint/auth'
↑何これ。
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(認可)を区別していないとも思えんし・・
routes\channels.php
403は401(認証)の勘違いだと判断して、
認証するにはサーバーサイドの処理も必要になる。
これは、公式サイトによれば、routes\channels.phpでやるみたい。
Broadcast::channel('message.*'/* *なんでもOK */,function(){
return true;
});
フロント側でもサーバー側に認可コードを送ってやる必要がある。
window.Echo.private("message.1"/*認可コード*/)
.listen('MessageSend', (e) => {
console.log(e)
});
再度アクセスすると、エラーは表示されず、ソースを確認すると200になっている。
認証が成功しているのが確認できる。
でも、これでメッセージをポストしても動作しない。
200okが帰ってきてるのに。なんでや。
あっ認可か、認可がないからか。
Privateの認可判断するためには、PrivateChannel()を使用する必要がある。
よって、Channel()からPrivateChannel()に変更する。
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)
* });
*/
}
再度メッセージを送ってやる。無事発火。
このコードでは、すべてのユーザーを認証しているため、
今度は、ログインユーザーを認証するよう変更する。
とりあえず、ログインしているユーザーだけにメッセージを送ってやる。
サーバー側は
公式サイトでは$userになっているが、わかりにくいので$authに変更した
Broadcast::channel('message.{userId}'/* (*)でもOK */,function($auth /*$userから変更*/,$userId/*ここでは不要*/){
/*ログイン認証してなければnull値となりfalseになる*/
return $auth;
// return (int) $user->id === (int) $userId;
});
public function broadcastOn()
{
// return new Channel('message');
//('チャンネル.引数') 引数は絶対にいる{id}とか*ではあかん'
//プライベートチャンネルでは認証と認可が必ず必要なため。
return new PrivateChannel('message.fugafuga');
}
//"message.fugafuga"これは、認可と同じでないとだめ。
window.Echo.private("message.fugafuga")
.listen('MessageSend', (e) => {
console.log(e)
});
認証時(アクセス時)、チャンネルの引数が認可コードとして届けられている事がわかる。
画像をとるのを忘れたが、しっかりと、送信できた。
今度は特定のユーザーにだけイベントが発火するようにしたい。
public function broadcastOn()
{
//本来ならメッセージを送るときにこの番号も一緒に送ってやる
$userId = 12;
// return new Channel('message');
return new PrivateChannel('message.'.$userId);
}
//本来的にはこれはやめたほうがいいらしい。
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();
//ここのコードが走るのはアクセス時のみ、イベント発火時とうは、関係ない。
Broadcast::channel('message.*',function($user){
return $user;
//$idがね相手に対して入力をもとめるpasswordとかやったらね。この処理はね理解できるねん。
//ログインしているかしていないかの処理でこれは違うと思う。今のところ。
//adminユーザーの場合とかguardsの種類もここで、判定できる。
// return (int) $user->id === (int) $id;
});
これで、ログインIDが12番のユーザーだけのjsが発火する。
認証と認可を分けたことは、本当によく考えられている。
頭のええ人は違う。
うーんしかし、
認証と認可、わかったようなわからないような。
今回以外だったことは、
認証は認可と常にセットであるといえるが、
認可だけでいいなら、認証は必要がなかったことである。
パブリックチャンネルで特定のユーザにだけ、jsを発火したい場合。
//privateからchannelに書き換える。認可番号はいる。
window.Echo.channel("message.{{ Auth::id() }}")
.listen('MessageSend', (e) => {
console.log(e.message)
});
//privateからchannelに書き換える。
public function broadcastOn()
{
$userId = 12;
return new Channel('message.'.$userId);
// return new PrivateChannel('message.'.$userId);
}
必要ないが、とりあえず、コメントアウト
// Broadcast::channel('message.*',function($user){
// return $user;
// // return (int) $user->id === (int) $id;
// });
認証のいらないパブリックチャンネルでも認可による制限が機能している。
なにも認証はbroadcastでやる必要はないような気がしてきた・・・。
なによりも、認証と認可を区別してるくせに、認可コードは認証とセットしてるし、
複数の認可コードを設定することもできないので、複数の認可コードをセットしたい時は下のように2つ書く必要がある。
よって、2回認証作業が走る。大した事ではないが・・・。不満だ。
ただ、pusherとのやり取りはサーバー側で送信している。
auth(front) <->sever<->pusher
vendor\laravel\framework\src\Illuminate\Broadcasting
pusher = window.Echo.private("message.1").listen('MessageSend', (e) => {
console.log(e)
});
pusher = window.Echo.private("message.3").listen('MessageSend', (e) => {
console.log(e)
});
この辺でまとめておいた方がいいコード
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をもった特定のユーザーに送信してみる。
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();
});
protected function create(array $data)
{
return User::create([
'name' => $data['name'],
'email' => $data['email'],
'password' => Hash::make($data['password']),
//+
'team_id' => rand(1,3),
]);
}
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
@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
Broadcast::channel('message.{id}',function($user,$id){
return $user;
// return (int) $user->id === (int) $id;
});
<?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}");
}
}
<?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設定も必要
断り
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テーブルの編集
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モデルの編集は
編集は不要、今回はセルフリレーションを利用して保存するため必要がない。
ユーザーモデルとのリレーションでは利用したいので、モデル自体は必要。
そのまま
ルートの作成
//ユーザー一覧画面
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');
コントローラーの作成
<?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')に変更
<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>
ユーザー一覧画面の作成と編集
@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
@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
<?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
<?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}からメールが届いているで");
}
}
機能を追加してみる。
ユーザー一覧画面に未読数を表示してみる。
chatsテーブルに既読か未読かのカラムを追加するため、
chatsテーブルをリフレッシュする。
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();
});
php artisan migrate:refresh --path=database\migrations\xxx_create_chats_table.php
まず、一覧画面に未読数を表示できるようにする。
あっなんかバグを発見したかもしれん
->withCount('to_me_non_Reading')としているのに、
フロント側ではなぜか to_me_non__Readingになる。
なんでや。ちょっとこのままおいて置く。
//追加
public function to_me_non_Reading()
{
return $this->from_users()->where('to_user_id',Auth::id())->where('is_reading',false);
}
/**
* ユーザー一覧画面を表示
*/
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
@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の未読のメッセージが表示されるはず、
確認してみる。
何通か送ってみる。ちゃんとカウントされているのが確認できる。
このままでは、永遠に未読なので、既読機能をつける。
Userモデルに追加
// 追加
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()を編集
/**
* チャットルームを表示
* 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';
}
機能を確認してみる。
未読数の追加をリアルタイムに変更してみる。
あ~~~。
from_user_idを外したんや。付け加えなあかん。
その修正をまずする。
//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);
});
});
/**
* ブロードキャストするデータを取得
*
* @return array
*/
public function broadcastWith()
{
return [
'message' => $this->request['message'],
//+
'from_user_id' => $this->request['from_user_id'],
];
}
これでOK。
さっきいのチャンネルをindex画面でもブロードキャストされるように
すればいいだけ。
@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
ちゃんと動作しているな。OK。IDを変更しても動作はいけるは。
プレゼンスチャネル
別のユーザーが同じページを表示しているときにユーザーに通知したり、チャットルームの住民を一覧表示したりするなど、強力なコラボレーションアプリケーション機能を簡単に構築できます
ユーザーがチャンネルへの参加を認可されている場合に、trueを返しません。代わりに、ユーザーに関するデータの配列を返す必要があります
ユーザーがプレゼンスチャンネルへの参加を許可されていない場合は、falseまたはnullを返す必要があります。
Broadcast::channel('chat.{roomId}', function ($user, $roomId) {
if ($user->canJoinRoom($roomId)) {
return ['id' => $user->id, 'name' => $user->name];
}
});
ウ~ン
要するに、認証がOKの時はプライベートチャンネルの時とは違い
ユーザーの配列を返さなあかんと
falseのときはええよと。
Broadcast::channel('chat.{roomId}', function ($user, $roomId) {
if ($user->canJoinRoom($roomId)) {
return ['id' => $user->id, 'name' => $user->name];
}
});
↑これこのまま、利用できるよね。
問題はこれやcanJoinRoom($roomId)
<-説明もなしにこんな事するな。
なんにしろ、
ユーザがこの部屋に入る権限があるのかどうかのif文をもうけて、
OKなら、配列を返せばいいのか。
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=相対パス
ひとまずイベントは無視してルームでメッセージを送信して保存、表示できるようにする。
@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
//チャットルーム画面
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');
チャットルーム画面の作成
コントローラーの作成
/**
* ルーム画面を表示
* 自分が所属するチームのメッセージを取得
*/
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の作成と編集
@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
メッセージの保存と、表示ができている。
通信を加えてやればいいだけだが、、取り敢えず公式サイトのコードをコピペやろ。
//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();
}
})
});
確認してみる。
あっ、認証エラーがでた。
チャンネルを登録する。
Broadcast::channel('chat.{room_id}',function($user,$room_id){
if($user->team_id == $room_id){
return $user;
}
// return (int) $user->id === (int) $id;
});
.here((users) => {
console.log(users);
})
公式サイトのそれぞれのメソッドの説明は以下の通り。
- hereコールバックは、チャンネルへ正常に参加するとすぐに実行され、現在チャンネルにサブスクライブしている他のすべてのユーザーのユーザー情報を含む配列を受け取ります。
- joiningメソッドは、新しいユーザーがチャンネルに参加したときに実行され、
- leavingメソッドは、ユーザーがチャンネルを離れたときに実行されます。
- errorメソッドは、認証エンドポイントが200以外のHTTPステータスコードを返した場合や、返されたJSONの解析で問題があった場合に実行されます。
edgeからもアクセスしてみる。
Chromeの方では 入室してきたユーザーの名前が表示された。
.joining((user) => {
console.log(user.name);
})
edgeの方では、ユーザーの全データが返されてきた。
公式サイトの説明通りやね。ただ、ホンマに全データを返してきているので、
多分これでコントロールできるはず。
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()
の戻り値が無事コントロールできました。
leaving()を検証してみる。検証用コードに変更する。
//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();
}
})
});
全部で.leaving()が発火した。
ただ、気になったのが、リロードボタンを押すと。
joining() と leaving()が両方発火した。
以上 leaving()の検証終了
メッセージの保存時に、イベントを発火させる。
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を追加する。
<?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。
別のチームで検証してみる。検証用にコードを変更する。
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()は発火せず。
次は送信 よし、3番の部屋では発火せず。
代わりに1番のへやで発火したwので、コードを変更する。
//func_2 チャンネルの購入とブロードキャストするイベントを設定する
//変更
window.Echo.join("chat.{{ $room_id }}.{{ Auth::user()->team_id }}")
//追加(引数に$room_idを追加している。)
broadcast(new ChatMessageRecieved($request->all(),$room_id,$team_id))->toOthers();
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
カラムを追加
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
次は、
- last_accessed_atをCarbonインスタンスに変換
- is_online() 最終アクセスが15分以内かどうか判断するAccessor関数を作成
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
<?php
return [
//とりあえず一番上に追加した。
'is_within' => '15',
// 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)。
あ~~~。
作成するにはまず、登録する必要があるみたい。
protected $listen = [
Registered::class => [
SendEmailVerificationNotification::class,
],
//+
// ログイン成功したら実行するイベントファイル
'Illuminate\Auth\Events\Authenticated' => [
'App\Listeners\LogAuthenticated',
],
];
php artisan event:generate
無事ファイルが作成されました。
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();
}
}
今度は、ルートを作成すると
Route::get('users', function(){ return \App\Models\User::get(); });
Route::get('online_users', function(){ return view('online_users'); });
今回はテストなので省略して書いていますが、本番環境ではコントローラーを使うことをおすすめします。
とあるので、編集する。
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');
public function online_users(){
return view('online_users');
}
public function online_users_get(){
$users = User::get(['id','name','last_accessed_at']);
return $users;
}
@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
ログアウトの通知もやってみるに挑戦してみる
まずは、イベントの登録から
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へ通知
}
}
ログイン時
ログアウト時
あとは、15分以上、last_access_atが更新のないユーザーのis_onlineが
他のユーザーが更新する時に、falseに書き換えられることになります。
それによって、ログアウトと判断される仕組みです。
素晴らしい。
えーとこれをプレゼンスチャンネルで書き換えたいんだが、どうしたらいんだ?
サイト全体でプレゼンスチャンネルを使う必要があるよね。
お部屋に入ったときにhere()が発火
leave()が発火する条件は
とにかく、お部屋から出ると発火する
//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 にもの凄く感動した。
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')
);
}
正直、意味不明だった。
これ知っているか知ってないかでプログラミングの効率がすごく変わるし、
ますます、コードが簡単にかけるようになるよね。