WebSocketを使ってリアルタイムかつ両方向の通信機能をWebサイトやモバイルアプリに組み込むサービス「Pusher」。Laravelと組み合わせて、Webアプリ内にリアルタイムな通知機能を追加する方法を解説します。Eメール、SMS、Slackなどにも通知できます。
ユーザーはどのWebサイトでも同じ機能を提供できると考えています。SNSだけでなくどんなWebサイトにも「通知を受ける」のドロップダウンがあるなかで、自分のサイトが非対応ではいけません。
幸いにもLaravel とPusher で、簡単に実装できます。本記事で紹介するコードはこちら で入手できます。
リアルタイム通知
リアルタイムの通知は優れたユーザーエクスペリエンスの必要条件です。Ajaxリクエストを一定間隔でバックエンドに送信して最新の通知を受け取る実現方法もありますが、より優れたアプローチはWebSockets を利用して通知が送信されると同時に受信する方法です。詳しく紹介します。
Pusher
PusherとはWebサービスの1つで、WebSocketを使ってリアルタイムかつ両方向の通信機能をWebサイトやモバイルアプリに組み込むサービス です。
PusherのAPIはもともとシンプルですが、Laravel Broadcasting とLaravel Echo を組み合わせると極限までシンプルにできます。
この記事では、リアルタイム通知を既存のブログに追加する方法を紹介します。
基本的な機能はStreamで実現するリアルタイムLaravel通知 に似ています。
まずはChristopher Vundi が作ったリポジトリ (少しだけ改変しています)を見てください。Simple Blogは、投稿された記事に対してユーザーがCRUDを実行する機能があります。
プロジェクト
初期化
まずはLaravelのSimple Blogをクローンします。
git clone https://github.com/vickris/simple-blog
次にMySQLデータベースを作成し、データベースにアクセスする環境変数を設定します。
env.example を.env にコピーして、関連する変数を更新します。
cp .env.example .env
.env DB_HOST=localhost
DB_DATABASE=homestead
DB_USERNAME=homestead
DB_PASSWORD=secret
続いて次のコマンドでプロジェクトの依存オブジェクトをインストールします。
composer install
次にマイグレーションとseedコマンドを実行して、データベースになんらかのデータを格納します。
php artisan migrate --seed
アプリケーションを実行して/posts にアクセスすると、自動作成した投稿の一覧が表示されます。
このアプリケーションでユーザー登録や新規記事の投稿をしてください。簡単なアプリですが、デモにはぴったりです。
ユーザー同士のフォロー機能
ユーザー同士でフォローする機能を追加するために、ユーザー間にMany To Many リレーションシップを構築します。
まずはユーザー同士を関連付けるピボットテーブルを作成します。次のコマンドでfollowers のマイグレーションの作成します。
php artisan make:migration create_followers_table --create=followers
マイグレーションにuser_id とfollows_id のフィールドを追加します。user_id はフォロー元のユーザーID、follows_id はフォロー先のユーザーIDです。
マイグレーションを更新は以下の通りです。
public function up()
{
Schema::create('followers', function (Blueprint $table) {
$table->increments('id');
$table->integer('user_id')->index();
$table->integer('follows_id')->index();
$table->timestamps();
});
}
マイグレートしてテーブルを作成します。
php artisan migrate
ここまでの内容はStreamアプローチの記事 とほぼ同じですが、ここからはStreamのアプローチとは異なる方法でフォローする機能を実装します。
User モデルにリレーションシップを追加します。
app/User.php // ...
class extends Authenticatable
{
// ...
public function followers()
{
return $this->belongsToMany(self::class, 'followers', 'follows_id', 'user_id')
->withTimestamps();
}
public function follows()
{
return $this->belongsToMany(self::class, 'followers', 'user_id', 'follows_id')
->withTimestamps();
}
}
ユーザーモデルにリレーションシップを追加しました。followers でユーザーのフォロワーをすべて取得して、follows はユーザーがフォローしているユーザーを取得します。
続いてヘルパー関数follow とisFollowing を用意します。follow はユーザーがほかのユーザーをフォローする関数で、isFollowing はユーザーが特定のユーザーをフォロー中か返す関数です。
app/User.php // ...
class extends Authenticatable
{
// ...
public function follow($userId)
{
$this->follows()->attach($userId);
return $this;
}
public function unfollow($userId)
{
$this->follows()->detach($userId);
return $this;
}
public function isFollowing($userId)
{
return (boolean) $this->follows()->where('follows_id', $userId)->first(['id']);
}
}
モデルを用意したので、次はユーザーをリスト化します。
ユーザーのリスト化
まずは必要なルートを設定します。
routes/web.php //...
Route::group(['middleware' => 'auth'], function () {
Route::get('users', 'UsersController@index')->name('users');
Route::post('users/{user}/follow', 'UsersController@follow')->name('follow');
Route::delete('users/{user}/unfollow', 'UsersController@unfollow')->name('unfollow');
});
続いて、ユーザーのコントローラーを作成します。
php artisan make:controller UsersController
indexメソッドを追加します。
app/Http/Controllers/UsersController.php // ...
use App\User;
class UsersController extends Controller
{
//..
public function index()
{
$users = User::where('id', '!=', auth()->user()->id)->get();
return view('users.index', compact('users'));
}
}
このメソッドはビューが必要です。users.index ビューを作成して、内部に格納します。
resources/views/users/index.blade.php @extends('layouts.app')
@section('content')
<div class="container">
<div class="col-sm-offset-2 col-sm-8">
<!-- Following -->
<div class="panel panel-default">
<div class="panel-heading">
All Users
</div>
<div class="panel-body">
<table class="table table-striped task-table">
<thead>
<th>User</th>
<th> </th>
</thead>
<tbody>
@foreach ($users as $user)
<tr>
<td clphpass="table-text"><div>{{ $user->name }}</div></td>
@if (auth()->user()->isFollowing($user->id))
<td>
<form action="{{route('unfollow', ['id' => $user->id])}}" method="POST">
{{ csrf_field() }}
{{ method_field('DELETE') }}
<button type="submit" id="delete-follow-{{ $user->id }}" class="btn btn-danger">
<i class="fa fa-btn fa-trash"></i>Unfollow
</button>
</form>
</td>
@else
<td>
<form action="{{route('follow', ['id' => $user->id])}}" method="POST">
{{ csrf_field() }}
<button type="submit" id="follow-user-{{ $user->id }}" class="btn btn-success">
<i class="fa fa-btn fa-user"></i>Follow
</button>
</form>
</td>
@endif
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
</div>
@endsection
/users ページにアクセスして、ユーザーのリストが表示されることを確認してください。
フォローとフォロー解除
UsersController にfollow とunfollow メソッドを追加します。
app/Http/Controllers/UsersController.php //...
class UsersController extends Controller
{
//...
public function follow(User $user)
{
$follower = auth()->user();
if ($follower->id == $user->id) {
return back()->withError("You can't follow yourself");
}
if(!$follower->isFollowing($user->id)) {
$follower->follow($user->id);
// sending a notification
$user->notify(new UserFollowed($follower));
return back()->withSuccess("You are now friends with {$user->name}");
}
return back()->withError("You are already following {$user->name}");
}
public function unfollow(User $user)
{
$follower = auth()->user();
if($follower->isFollowing($user->id)) {
$follower->unfollow($user->id);
return back()->withSuccess("You are no longer friends with {$user->name}");
}
return back()->withError("You are not following {$user->name}");
}
}
フォロー関連の機能を実装しました。/users ページからユーザーのフォローとフォロー解除ができます。
通知
LaravelのNotification クラスには、Emails、SMS、Webサイトなどで通知を送るAPIが用意されています。
2種類の通知を実装します。
フォロー通知: ほかのユーザーからフォローされたことを通知
投稿通知: フォロー中のユーザーが新規投稿したことを通知
フォロー通知
artisanコマンドで、通知のマイグレーションを作成します。
php artisan notifications:table
マイグレートして新しいテーブルを作成します。
php artisan migrate
フォロー通知から着手します。次のコマンドを実行してNotification クラスを作成します。
php artisan make:notification UserFollowed
作成したNotification クラスを編集します。
app/Notifications/UserFollowed.php class UserFollowed extends Notification implements ShouldQueue
{
use Queueable;
protected $follower;
public function __construct(User $follower)
{
$this->follower = $follower;
}
public function via($notifiable)
{
return ['database'];
}
public function toDatabase($notifiable)
{
return [
'follower_id' => $this->follower->id,
'follower_name' => $this->follower->name,
];
}
}
コードはこれだけですが、たくさんのことをしています。まず、Notification を作成するときに$follower のインスタンスを挿入します。
via メソッドでLaravelにdatabase チャネルで通知を送るよう指示します。このメソッドを実行すると、LaravelはNotifications テーブルに新しいレコードを作成します。
user_id と通知のtype は自動的に設定されます。さらにtoDatabase を使って、Notification を継承してデータを追加します。戻り値の配列はNotification のdata フィールドに追加されます。
Laraveは自動的にNotification をキューに追加してバックグラウンドで実行するので、レスポンス時間が短くなります。あとでPusherを使うときにHTTPコールを追加するため最後にShouldQueueを実装します。
以下のコードで、ユーザーがフォロ―されたときにNotification を実行できるようにします。
app/Http/Controllers/UsersController.php // ...
use App\Notifications\UserFollowed;
class UsersController extends Controller
{
// ...
public function follow(User $user)
{
$follower = auth()->user();
if ( ! $follower->isFollowing($user->id)) {
$follower->follow($user->id);
// add this to send a notification
$user->notify(new UserFollowed($follower));
return back()->withSuccess("You are now friends with {$user->name}");
}
return back()->withSuccess("You are already following {$user->name}");
}
//...
}
User モデルにはNotifiable トレイトがあるので、notify メソッドを呼び出します。
notify したいモデルは、Notifiable トレイトを使ってnotifyメソッドにアクセスできるようにします。
通知を既読にする
Notification には通知の内容とリソースへのリンクが含まれます。新しい投稿の通知なら説明文と新しい投稿へのリンクがあり、閲覧するとその通知を既読とします。
リクエストに?read=notification_id インプットが含まれているか確認して既読をマークするミドルウェアを次のコマンドで作成します。
php artisan make:middleware MarkNotificationAsRead
次のコードをミドルウェアのhandle メソッドに追加します。
app/Http/Middleware/MarkNotificationAsRead.php class MarkNotificationAsRead
{
public function handle($request, Closure $next)
{
if($request->has('read')) {
$notification = $request->user()->notifications()->where('id', $request->read)->first();
if($notification) {
$notification->markAsRead();
}
}
return $next($request);
}
}
リクエストごとにミドルウェアを実行するように、$middlewareGroups に追加します。
app/Http/Kernel.php //...
class Kernel extends HttpKernel
{
//...
protected $middlewareGroups = [
'web' => [
//...
\App\Http\Middleware\MarkNotificationAsRead::class,
],
// ...
];
//...
}
通知を表示します。
通知の表示
Ajaxを使って通知の一覧を表示し、Pusherでリアルタイムに更新します。まずはnotifications メソッドをコントローラーに追加します。
app/Http/Controllers/UsersController.php // ...
class UsersController extends Controller
{
// ...
public function notifications()
{
return auth()->user()->unreadNotifications()->limit(5)->get()->toArray();
}
}
これで直近5件の未読の通知を取得できます。アクセスするルートを追加します。
routes/web.php //...
Route::group([ 'middleware' => 'auth' ], function () {
// ...
Route::get('/notifications', 'UsersController@notifications');
});
続いてヘッダーに通知のドロップダウンを追加します。
resources/views/layouts/app.blade.php <head>
<!-- // ... // -->
<!-- Scripts -->
<script>
window.Laravel = <?php echo json_encode([
'csrfToken' => csrf_token(),
]); ?>
</script>
<!-- This makes the current user's id available in javascript -->
@if(!auth()->guest())
<script>
window.Laravel.userId = <?php echo auth()->user()->id; ?>
</script>
@endif
</head>
<body>
<!-- // ... // -->
@if (Auth::guest())
<li><a href="{{ url('/login') }}">Login</a></li>
<li><a href="{{ url('/register') }}">Register</a></li>
@else
<!-- // add this dropdown // -->
<li class="dropdown">
<a class="dropdown-toggle" id="notifications" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
<span class="glyphicon glyphicon-user"></span>
</a>
<ul class="dropdown-menu" aria-labelledby="notificationsMenu" id="notificationsMenu">
<li class="dropdown-header">No notifications</li>
</ul>
</li>
<!-- // ... // -->
現在のユーザーIDを取得するために、グローバル変数window.Laravel.userId をスクリプトに追加しています。
JavaScriptとSASS
Laravel Mixを使ってJavaScriptとSASSをコンパイルします。まずはnpmパッケージをインストールします。
npm install
続いて、次のコードをapp.js に追加します。
app/resources/assets/js/app.js window._ = require('lodash');
window.$ = window.jQuery = require('jquery');
require('bootstrap-sass');
var notifications = [];
const NOTIFICATION_TYPES = {
follow: 'App\\Notifications\\UserFollowed'
};
ここでは初期化のみです。AjaxやPusherで取得したNotification オブジェクトを格納するにはnotifications を使います。NOTIFICATION_TYPES にNotification の種類が格納されています。
AjaxでNotificationをGET します。
app/resources/assets/js/app.js //...
$(document).ready(function() {
// check if there's a logged in user
if(Laravel.userId) {
$.get('/notifications', function (data) {
addNotifications(data, "#notifications");
});
}
});
function addNotifications(newNotifications, target) {
notifications = _.concat(notifications, newNotifications);
// show only last 5 notifications
notifications.slice(0, 5);
showNotifications(notifications, target);
}
最新の通知をAPIから受け取りドロップダウンに追加します。
現在のNotification をaddNotifications に集約しています。新しいものにはLodash を使って、最新の5件を取り出します。
完成にはまだいくつかの関数が必要です。
app/resources/assets/js/app.js //...
function showNotifications(notifications, target) {
if(notifications.length) {
var htmlElements = notifications.map(function (notification) {
return makeNotification(notification);
});
$(target + 'Menu').html(htmlElements.join(''));
$(target).addClass('has-notifications')
} else {
$(target + 'Menu').html('<li class="dropdown-header">No notifications</li>');
$(target).removeClass('has-notifications');
}
}
この関数は、すべてのNotification の文字列を構築してドロップダウンに追加します。
Notification がなければ「No notifications」と表示します。
またドロップダウンボタンにクラスを追加し、Notification が存在すればGitHubのように色が変わります。
Notification の文字列を作るヘルパー関数を追加します。
app/resources/assets/js/app.js //...
// Make a single notification string
function makeNotification(notification) {
var to = routeNotification(notification);
var notificationText = makeNotificationText(notification);
return '<li><a href="' + to + '">' + notificationText + '</a></li>';
}
// get the notification route based on it's type
function routeNotification(notification) {
var to = '?read=' + notification.id;
if(notification.type === NOTIFICATION_TYPES.follow) {
to = 'users' + to;
}
return '/' + to;
}
// get the notification text based on it's type
function makeNotificationText(notification) {
var text = '';
if(notification.type === NOTIFICATION_TYPES.follow) {
const name = notification.data.follower_name;
text += '<strong>' + name + '</strong> followed you';
}
return text;
}
app.scss に追加します。
app/resources/assets/sass/app.scss //...
#notifications.has-notifications {
color: #bf5329
}
アセットをコンパイルします。
npm run dev
ユーザーをフォローすると、フォローされたユーザーに通知が届きます。フォローされたユーザーが通知をクリックすると/users にリダイレクトされ、通知が消えます。
新規投稿の通知
ユーザーが投稿したときにフォロワーに通知する機能を実装します。
Notification クラスを作成します。
php artisan make:notification NewPost
自動作成されたクラスを編集します。
app/Notifications/NewArticle.php // ..
use App\Post;
use App\User;
class NewArticle extends Notification implements ShouldQueue
{
// ..
protected $following;
protected $post;
public function __construct(User $following, Post $post)
{
$this->following = $following;
$this->post = $post;
}
public function via($notifiable)
{
return ['database'];
}
public function toDatabase($notifiable)
{
return [
'following_id' => $this->following->id,
'following_name' => $this->following->name,
'post_id' => $this->post->id,
];
}
}
通知の送信を実装します。いくつかの方法があります。
この記事ではEloquent Observer を使います。
Post のオブザーバーを作成してイベントを監視します。app/Observers/PostObserver.php クラスを作成します。
namespace App\Observers;
use App\Notifications\NewPost;
use App\Post;
class PostObserver
{
public function created(Post $post)
{
$user = $post->user;
foreach ($user->followers as $follower) {
$follower->notify(new NewPost($user, $post));
}
}
}
オブザーバーをAppServiceProvider に登録します。
app/Providers/AppServiceProvider.php //...
use App\Observers\PostObserver;
use App\Post;
class AppServiceProvider extends ServiceProvider
{
//...
public function boot()
{
Post::observe(PostObserver::class);
}
//...
}
メッセージをJSで表示するためフォーマットします。
app/resources/assets/js/app.js // ...
const NOTIFICATION_TYPES = {
follow: 'App\\Notifications\\UserFollowed',
newPost: 'App\\Notifications\\NewPost'
};
//...
function routeNotification(notification) {
var to = `?read=${notification.id}`;
if(notification.type === NOTIFICATION_TYPES.follow) {
to = 'users' + to;
} else if(notification.type === NOTIFICATION_TYPES.newPost) {
const postId = notification.data.post_id;
to = `posts/${postId}` + to;
}
return '/' + to;
}
function makeNotificationText(notification) {
var text = '';
if(notification.type === NOTIFICATION_TYPES.follow) {
const name = notification.data.follower_name;
text += `<strong>${name}</strong> followed you`;
} else if(notification.type === NOTIFICATION_TYPES.newPost) {
const name = notification.data.following_name;
text += `<strong>${name}</strong> published a post`;
}
return text;
}
これでユーザーにフォローと新規投稿の通知が送られます。試してください!
Pusherでリアルタイム通知
Pusherを使ってWebsocketで通知をリアルタイムに受け取ります。
pusher.com に無料登録して、新しいアプリを作成します。
...
BROADCAST_DRIVER=pusher
PUSHER_KEY=
PUSHER_SECRET=
PUSHER_APP_ID=
broadcasting 設定ファイルにアカウントのオプションを追加します。
config/broadcasting.php
//...
'connections' => [
'pusher' => [
//...
'options' => [
'cluster' => 'eu',
'encrypted' => true
],
],
//...
providers の配列にApp\Providers\BroadcastServiceProvider を登録します。
config/app.php // ...
'providers' => [
// ...
App\Providers\BroadcastServiceProvider
//...
],
//...
PusherのPHP SDKとLaravelのEchoが必要です。
composer require pusher/pusher-php-server
npm install --save laravel-echo pusher-js
送信する通知のデータが必要なので、UserFollowed Notificationを編集します。
app/Notifications/UserFollowed.php //...
class UserFollowed extends Notification implements ShouldQueue
{
// ..
public function via($notifiable)
{
return ['database', 'broadcast'];
}
//...
public function toArray($notifiable)
{
return [
'id' => $this->id,
'read_at' => null,
'data' => [
'follower_id' => $this->follower->id,
'follower_name' => $this->follower->name,
],
];
}
}
次はNewPostです。
app/Notifications/NewPost.php //...
class NewPost extends Notification implements ShouldQueue
{
//...
public function via($notifiable)
{
return ['database', 'broadcast'];
}
//...
public function toArray($notifiable)
{
return [
'id' => $this->id,
'read_at' => null,
'data' => [
'following_id' => $this->following->id,
'following_name' => $this->following->name,
'post_id' => $this->post->id,
],
];
}
}
JavaScriptを更新します。app.js を開いて次のコードを追加します。
app/resources/assets/js/app.js // ...
window.Pusher = require('pusher-js');
import Echo from "laravel-echo";
window.Echo = new Echo({
broadcaster: 'pusher',
key: 'your-pusher-key',
cluster: 'eu',
encrypted: true
});
var notifications = [];
//...
$(document).ready(function() {
if(Laravel.userId) {
//...
window.Echo.private(`App.User.${Laravel.userId}`)
.notification((notification) => {
addNotifications([notification], '#notifications');
});
}
});
通知がリアルタイムに反映されます。アプリを触って、通知が更新されることを確認してください。
最後に
Pusherにはリアルタイムにイベントを受信するためのシンプルなAPIがあります。これをLaravelのNotificationと組み合わせると、1カ所から通知をいろいろなチャネル(Email、SMS、Slackなど)で送信できます。この記事では、ユーザーをフォローする機能をSimple Blogに追加しました。またリアルタイムの機能を強化するためのツールも紹介しました。
PusherやLaravel Notificationでできることはたくさんあります。この組み合わせなら、Pub/Subメッセージをリアルタイムでブラウザーやモバイル、IoTデバイスに送信するサービスも実現できます。ほかにもユーザーのオンライン/オフライン状態を取得するプレゼンスAPIもあります。
詳しくはドキュメント(Pusher doc 、Pusher tutorial 、Laravel doc )にアクセスして、PusherやLaravelでできることを調べてください。
本記事はRafie Younes 、Wern Ancheta が査読を担当しています。最高のコンテンツに仕上げるために尽力してくれたSitePointの査読担当者のみなさんに感謝します。
(原文:How to Add Real-Time Notifications to Laravel with Pusher )
[翻訳:内藤 夏樹/編集:Livit ]