hoge

いわゆるWeb Developerの備忘録

【Laravel】WebPush通知クラスを実装する

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 の設定は割愛(上記サイト参考)します。

登録の流れ

  1. ログイン済みのユーザがプッシュ機能を有効にするボタンを押す
  2. 有効を許可した場合 token を発行し、サーバーに送る
  3. サーバは送られてきた token を保存する

最近プッシュ通知を追加するサイトが増えてきましたが、サイトにアクセスするだけで通知の許可を求めるサイトが多くて困ります。

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 の通知を使った実装を残していたので、そちらを実装します。

通知 6.x 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

同じカテゴリー(PWA)の記事
上の画像に書かれている文字を入力して下さい
<ご注意>
書き込まれた内容は公開され、ブログの持ち主だけが削除できます。

削除
【Laravel】WebPush通知クラスを実装する
    コメント(0)