2019年11月20日 18:40
Laravel に WebPush 通知機能を追加するためには FCM(Firebase Cloud Messaging)を使うか、VAPID(Voluntary Application Server Identification)を使うという選択肢があります。
VAPID の場合laravel-notification-channels/webpush: Webpush notifications channel for Laravel.という便利なライブラリがあるのですが、何故かインストール時にエラーが発生してしまい入らないため、今回は FCM を採用します。
VAPID を使った WebPush は以下のサイトを参考にしてください。
Laravel で Web Push 2019 | kawax.biz
[ Laravel ] 自前で用意する、サービスワーカーを使った Notification
のプッシュ通知 - Qiita
ベースとなる大まかな部分は下記のサイトを参考にしています。
Laravel で Firebase Cloud Messaging を使ってブラウザにプッシュ通知する - Qiita
Firebase の登録や manifest.json の設定は割愛(上記サイト参考)します。
登録の流れ
最近プッシュ通知を追加するサイトが増えてきましたが、サイトにアクセスするだけで通知の許可を求めるサイトが多くて困ります。
UX としては最悪なので、会員制サイトであればユーザの通知設定画面から、特定のボタンを押したときにようやくアラートが表示されるのが理想です。
fcm_tokens というテーブルを用意し、そこに user_id と token を登録します。ユーザは複数のデバイスから通知を許可する可能性があるので users テーブルとは別にします。
php artisan make:migration create_fcm_tokens_table -m
class CreateFcmTokensTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('fcm_tokens', function (Blueprint $table) {
$table->bigIncrements('id');
$table->unsignedBigInteger('user_id');
$table->string('token', 255)->unique();
$table->timestamps();
$table->foreign('user_id')->references('id')->on('users');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('fcm_tokens');
}
}
<?php
class FCMToken extends Model
{
protected $table = 'fcm_tokens';
protected $fillable = [
'token',
];
public function user()
{
return $this->belongsTo(User::class);
}
}
class User extends User
{
use Notificable;
/** 省略 */
public function fcmTokens()
{
return $this->hasMany(FCMToken::class);
}
}
フロント側では Vue を使っていますが、ボタンが押された時にアラートを表示し、許可された場合登録を行います。
yarn add -D firebase
メインの処理部分である app.js で呼び出し、Vue コンポーネントで firebase が使えるようにします。
window.firebase = require("firebase/app");
import "firebase/messaging";
/** 省略 */
const firebaseConfig = {
/** apiKeyなど */
};
firebase.initializeApp(firebaseConfig);
allowPush() {
if (("granted" || "denied") === Notification.permission) {
return;
}
const messaging = firebase.messaging();
const token = await messaging
.requestPermission()
.then(() => messaging.getToken());
const res = await axios.post("/api/fcm-tokens", { token });
/** 省略 */
}
これで token を受け取って保存することができました。
続けて通知するための処理を書いていきます。
PHP では Firebase の公式 SDK が配布されていないので、非公式のkreait/firebase-php: Unofficial Firebase Admin SDK for PHPを使います。
Cloud Messaging — Firebase Admin SDK for PHP Documentation
簡単にプッシュ通知のテストができるようにコマンドを追加します。
php artisan make:command PushTest
参考にしたサイトではコマンド内で全ての処理を書いており、課題として Laravel の通知を使った実装を残していたので、そちらを実装します。
カスタムチャンネルの作り方が書いてありますが、生成するコマンドはないようなので(?)、カスタムメッセージとカスタムチャンネルをスクラッチします。
プッシュ通知で必要な情報を WebPushMessage クラスに流し込みます。
REST Resource: projects.messages | Firebase
<?php
namespace App\Channels\Messages;
class WebPushMessage
{
/**
* The title of the notification.
*
* @var string
*/
public $title;
/**
* The body of the notification.
*
* @var $body
*/
public $body;
/**
* The icon of the notification.
*
* @var string
*/
public $icon;
/**
* The color of the notification.
*
* @var string
*/
public $color;
/**
* The click action of the notification.
*
* @var string
*/
public $click_action;
/**
* The image of the notification.
*
* @var string
*/
public $image;
/**
* Set the title of the notification.
*
* @param string $title
* @return $this
*/
public function title($title)
{
$this->title = $title;
return $this;
}
/**
* Set the body of the notification.
*
* @param string $title
* @return $this
*/
public function body($body)
{
$this->body = $body;
return $this;
}
/**
* Set the icon of the notification.
*
* @param string $icon
* @return $this
*/
public function icon($icon)
{
$this->icon = $icon;
return $this;
}
/**
* Set the color of the notification.
*
* @param string $icon
* @return $this
*/
public function color($color)
{
$this->icon = $color;
return $this;
}
/**
* Set the click action of the notification.
*
* @param string $click_action
* @return $this;
*/
public function clickAction($click_action)
{
$this->click_action = $click_action;
return $this;
}
/**
* Set the image of the notification.
*
* @param string $image
* @return $this
*/
public function image($image)
{
$this->image = $image;
return $this;
}
/**
* Get an array representation of the message.
*
* @return array
*/
public function toArray()
{
return [
'title' => $this->title,
'body' => $this->body,
'icon' => $this->icon,
'color' => $this->color,
'click_action' => $this->click_action,
'image' => $this->image,
];
}
}
カスタムメッセージが用意できたので、実際の送信を行うカスタムチャンネルを作ります。
Firebase
との接続はここで行っています。
<?php
namespace App\Channels;
use Kreait\Firebase\Factory;
use Kreait\Firebase\ServiceAccount;
use Kreait\Firebase\Messaging\CloudMessage;
use Kreait\Firebase\Messaging\WebPushConfig;
use Illuminate\Notifications\Notification;
class WebPushChannel
{
public $messaging;
/**
* Create a new notification instance.
*
* @return void
*/
public function __construct()
{
$service_account = ServiceAccount::fromJsonFile(base_path() . '/config/json/firebase_credentials.json');
$firebase = (new Factory)
->withServiceAccount($service_account)
->create();
$this->messaging = $firebase->getMessaging();
}
/**
* Send the given notification.
*
* @param mixed $notifiable
* @param \Illuminate\Notifications\Notification $notification
* @return void
*/
public function send($notifiable, Notification $notification)
{
$message = $notification->toWebPush($notifiable);
$config = WebPushConfig::fromArray([
'notification' => $message,
]);
$fcm_tokens = $notifiable->fcmTokens;
foreach ($fcm_tokens as $fcm_token) {
$message = CloudMessage::withTarget('token', $fcm_token->token)
->withWebPushConfig($config);
try {
$this->messaging->send($message, $fcm_token->token);
} catch (Exception $e) {
$fcm_token->delete();
}
}
}
}
Firebase の管理者 SDK を使う必要があるので、json ファイルを読み込みます。
json ファイルは Firebase のプロジェクトの設定からサービスアカウントタブを選択し、新しい秘密鍵の生成で json ファイルが保存されます。
登録されている token が使われなくなった(新しい token が生成された)場合、送信時にエラーが発生するので try-catch でエラー処理を行い、使われなくなった token を削除するようにしています。
エラーを握りつぶしてしまっているので、要修正。
カスタムメッセージとカスタムチャンネルが用意できたので、Notification クラスを作ります。
php artisan make:notification NewsPush
<?php
namespace App\Notifications;
use App\Channels\Messages\WebPushMessage;
use App\Channels\WebPushChannel;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class News extends Notification
{
use Queueable;
/**
* Create a new notification instance.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Get the notification's delivery channels.
*
* @param mixed $notifiable
* @return array
*/
public function via($notifiable)
{
return [WebPushChannel::class];
}
/**
* Get the mail representation of the notification.
*
* @param mixed $notifiable
* @return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail($notifiable)
{
return (new MailMessage)
->line('The introduction to the notification.')
->action('Notification Action', url('/'))
->line('Thank you for using our application!');
}
/**
* Get the array representation of the notification.
*
* @param mixed $notifiable
* @return array
*/
public function toArray($notifiable)
{
return [
//
];
}
/**
* Get the push representation of the notification.
*
* @param mixed $notifiable
* @return WebPushMessage
*/
public function toWebPush($notifiable)
{
return (new WebPushMessage)
->title('新着情報')
->body('新しいニュースが追加されました')
->clickAction('http://localhost:3000');
}
}
このまま実行するとメールまで送信されてしまうので、via を先ほど作ったカスタムチャンネルに変更し、toWebPush メソッドを用意します。
これでようやく通知クラスができたので、PushTest クラスから呼び出しを行います。
<?php
namespace App\Console\Commands;
use App\User;
use App\Notifications\JobPush;
use App\Notifications\NewsPush;
use Illuminate\Console\Command;
class PushTest extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'webpush:test';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Command description';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$user = User::find(1);
$user->notify(new NewsPush);
}
}
id が 1 のユーザが token を保存しているものとしていますが、適宜変更してください。
サーバ側は完了したので、フロント側で受け取れるようにします。
firebase/messaging
を使うとfirebase-messaging-sw.js
という指定されたファイル名のサービスワーカーが読み込まれるので、用意します。
importScripts("https://www.gstatic.com/firebasejs/7.4.0/firebase-app.js");
importScripts("https://www.gstatic.com/firebasejs/7.4.0/firebase-messaging.js");
firebase.initializeApp({
/** apiKeyなど */
});
const messaging = firebase.messaging();
messaging.setBackgroundMessageHandler(payload => {
const notificationTitle = payload.notification.title;
const notificationOptions = {
body: payload.notification.body,
icon: payload.notification.icon
};
return self.registration.showNotification(
notificationTitle,
notificationOptions
);
});
あとはコマンドラインから実行してプッシュ通知が来れば完成です。
php artisan webpush:test
p {
margin-bottom: 1em;
}
blockquote p {
margin-bottom: initial;
}
#content ul {
padding-left: 40px;
}
.token.operator {
background-color: initial;
}