概要
Laravel11 と laravel-notification-channels/webpush を利用してWebブラウザのプッシュ通知の動作確認を行う。
参考資料
環境
Laravel Sailを利用した。
MacOS:14.5 Sonoma
PHP:8.3.10
Laravel:11.20.0
動作確認の準備
Laravel Sail の基本セットアップ
下記コマンドを実行する。
// 作業フォルダへ移動。
% cd 作業フォルダ
// Laravelアプリケーションの作成。
% curl -s "https://laravel.build/webpush-test" | bash
// Sailを起動。
% cd webpush-test
% ./vendor/bin/sail up -d
// DBのマイグレーション実行。
% ./vendor/bin/sail artisan migrate
// laravel-notification-channels/webpush をインストール。
% ./vendor/bin/sail composer require laravel-notification-channels/webpush
app/Models/User.php 編集
次に app/Models/User.php を次のように編集する。(2行だけ編集)
<?php
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
// ※ HasPushSusbscriptions を追加。
use NotificationChannels\WebPush\HasPushSubscriptions;
class User extends Authenticatable
{
// ※ HasPushSusbscriptions を追加。
use HasFactory, Notifiable, HasPushSubscriptions;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'name',
'email',
'password',
];
WebPush用マイグレート等実行
次に下記コマンドを実行する。
// マイグレーションファイル生成。
% ./vendor/bin/sail artisan vendor:publish --provider="NotificationChannels\WebPush\WebPushServiceProvider" --tag="migrations"
// マイグレート実行。
% ./vendor/bin/sail artisan migrate
// VAPIDで利用する公開鍵と秘密鍵を生成して .env へ追加。
% ./vendor/bin/sail artisan webpush:vapid
.env 編集
次に .envファイルにて、
MacのSafariでもプッシュ通知を表示させるために、subjectの設定と、
xDebugを動かすための設定を行う。(合計2行追記)
xDebugを利用するにはIDEとphp.iniの編集も必要になる。
php.ini については、Dockerコンテナ内に入り、テキストエディタをインストールし、/etc/php/8.3/cli/conf.d/20-xdebug.ini を編集すれば良い。
IDEはPHPStormやVSCodeで手順が変わるので、適宜調べると良い。
(この動作確認ではPHPStormを利用している)
※ ./vendor/bin/sail up コマンドを利用しないと、xDebugを利用できないので注意。
(Dockerからコンテナ起動すると、xDebug利用できない)
APP_NAME=Laravel
APP_ENV=local
APP_KEY=base64:Y0EgytJvxhFQn7RpK0y36Y3K09nYR+LSf//QICzIq0Q=
APP_DEBUG=true
APP_TIMEZONE=UTC
APP_URL=http://localhost
APP_LOCALE=en
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US
APP_MAINTENANCE_DRIVER=file
# APP_MAINTENANCE_STORE=database
BCRYPT_ROUNDS=12
LOG_CHANNEL=stack
LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=sail
DB_PASSWORD=password
SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null
BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local
QUEUE_CONNECTION=database
CACHE_STORE=database
CACHE_PREFIX=
MEMCACHED_HOST=127.0.0.1
REDIS_CLIENT=phpredis
REDIS_HOST=redis
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=smtp
MAIL_HOST=mailpit
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}"
SCOUT_DRIVER=meilisearch
MEILISEARCH_HOST=http://meilisearch:7700
MEILISEARCH_NO_ANALYTICS=false
// ※ 下記の1行を追記。
VAPID_SUBJECT="mailto:メールアドレス"
VAPID_PUBLIC_KEY=BCe3IPJTeTMABsuWjtq3i-uxAgWXtfZ93m_nm3TU7YiIhqmKZMt5hHi5lJD3fdaq5Awif3m9Y-mEdeUvmC1WqhU
VAPID_PRIVATE_KEY=wYKHvNOQ469B75RXHwnXVKvYFA7Dq7Q6B-FIUdDYbkw
// ※ 下記の1行を追記。
SAIL_XDEBUG_MODE=develop,debug
app/Notifications/TestPush.php 作成と編集
次に下記コマンドを実行する。
% ./vendor/bin/sail artisan make:notification TestPush
次に app/Notifications/TestPush.php を次のように編集する。
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
use Minishlink\WebPush\WebPush;
use NotificationChannels\WebPush\WebPushChannel;
use NotificationChannels\WebPush\WebPushMessage;
class TestPush extends Notification
{
use Queueable;
private $title;
private $body;
private $url;
/**
* Create a new notification instance.
*/
public function __construct(string $title = "", string $body = "", string $url = "")
{
$this->title = $title;
$this->body = $body;
$this->url = $url;
}
/**
* Get the notification's delivery channels.
*
* @return array<int, string>
*/
public function via(object $notifiable): array
{
// return ['mail'];
return [WebPushChannel::class];
}
public function toWebPush($notifiable, $notification)
{
return (new WebPushMessage())
->title($this->title)
->body($this->body)
->data(['url' => $this->url]);
}
/**
* Get the array representation of the notification.
*
* @return array<string, mixed>
*/
public function toArray(object $notifiable): array
{
return [
//
];
}
}
Breeze セットアップ
次のコマンドを実行する。
// Breeze をインストール。
% ./vendor/bin/sail composer require laravel/breeze
% ./vendor/bin/sail artisan breeze:install
// 色々聞かれるが、とりあえず「Blade with Alpine」を選択する。
> Blade with Alpine
// Would you like dark mode support ? と聞かれるが、どちらを選択しても良い。
> No
// Which testing framework do you prefer ? と聞かれるが、どちらを選択しても良い。
> PHPUnit
アカウント作成
次に最初から存在するWelcomeページから、アカウントを1個作成する。
(今回はこのアカウントに紐付けてプッシュ通知を行う)
Webブラウザでクリックと入力してRegister押すだけなので、手順は省略する。
名前やメールアドレスやパスワードを控える必要は無い。
app/Http/Controllers/WebPushController.php 作成と編集
次にコントローラを作成する。
% ./vendor/bin/sail artisan make:controller WebPushController
次に app/Http/Controllers/WebPushController.php を次のように編集する。
<?php
namespace App\Http\Controllers;
use App\Models\User;
use App\Notifications\TestPush;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class WebPushController extends Controller
{
public function setSubscription(Request $request): JsonResponse
{
$user = User::find(1);
$tempData = $request->json()->all();
$user->updatePushSubscription(
$tempData['endpoint'],
$tempData['keys']['p256dh'],
$tempData['keys']['auth'],
$tempData['contentEncoding']
);
return response()->json();
}
public function send(Request $request)
{
$user = User::find(1);
$user->notify(new TestPush('タイトル', '内容', 'https://google.co.jp'));
}
}
routes/web.php 編集
次に routes/web.php を次のように編集する。
<?php
use App\Http\Controllers\ProfileController;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\WebPushController;
// ここから
Route::view('/webpush', 'webpush');
Route::post('/set-subscription', [WebPushController::class, 'setSubscription'])
->withoutMiddleware(\Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::class);
Route::get('/send', [WebPushController::class, 'send']);
// ここまで追記。
Route::get('/', function () {
return view('welcome');
});
Route::get('/dashboard', function () {
return view('dashboard');
})->middleware(['auth', 'verified'])->name('dashboard');
Route::middleware('auth')->group(function () {
Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
});
require __DIR__.'/auth.php';
ビューの編集
次に resources/views/webpush.blade.php を作成し、下記のように編集する。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>webpush.blade.php</title>
<script src="app.js"></script>
</head>
<body>
webpush.blade.php<br>
<br>
<button
type="button"
onclick="getPermission()"
>
通知の許可を取得
</button>
<br><br>
<button
type="button"
onclick="updateSw()"
>
サービスワーカーを更新
</button>
</body>
</html>
public/app.js 編集
次に public/app.js を作成し、下記のように編集する。
// 公開鍵。
// ※ .envファイルの公開鍵をコピペすれば良い。
const applicationServerKey = 'BCe3IPJTeTMABsuWjtq3i-uxAgWXtfZ93m_nm3TU7YiIhqmKZMt5hHi5lJD3fdaq5Awif3m9Y-mEdeUvmC1WqhU';
// 公開鍵を編集する関数。minishlink/web-pushからコピペした。
function urlBase64ToUint8Array (base64String) {
const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding).replace(/\-/g, '+').
replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
async function getPermission() {
// 通知の許可を取得。
const result = await Notification.requestPermission();
if (result !== 'granted') {
// 許可以外だった場合は何もしない。
return;
}
// サービスワーカーを登録。
await navigator.serviceWorker.register('webpush_sw.js');
// アクティブになったサービスワーカーを取得。
const swr = await navigator.serviceWorker.ready;
// プッシュサーバへ登録。
const subscription = await swr.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(applicationServerKey)
});
// 暗号化方式を確認。
let contentEncoding = 'aesgcm';
if (PushManager.supportedContentEncodings?.includes('aes128gcm')) {
contentEncoding = 'aes128gcm';
}
// アプリケーションサーバへsubscriptionの情報を保存。
await fetch('set-subscription', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(Object.assign(subscription.toJSON(), {contentEncoding}))
});
// 完了した旨表示。
alert('プッシュサーバの登録まで完了');
}
async function updateSw() {
const swr = await navigator.serviceWorker.ready;
swr.update();
}
サービスワーカー(public/webpush_sw.js)の編集
次に public/webpush_sw.js を作成し、下記のように編集する。
// 通知をクリックした時に表示するURL。
let url = null;
self.addEventListener('push', async function (event) {
const jsonData = event.data.json();
url = jsonData.data.url;
event.waitUntil(
self.registration.showNotification(jsonData.title, {
body: jsonData.body,
}),
);
});
// 通知をクリックされた時のイベント。
self.addEventListener('notificationclick', function (event) {
event.notification.close();
clients.openWindow(url);
});
動作確認
- ※ xDebugを利用するなら、WebPushControllerのsetSubscriptionメソッドの最初の行にブレイクポイントを設置する。
- Webブラウザで http://localhost/webpush へアクセスし、「通知の許可を取得」ボタンを押下する。
- Webブラウザで http://localhost/send へアクセスし、通知が表示されることを確認する。
- 通知をクリックすると、Googleの検索ページがWebブラウザで表示されることを確認する。
上記以外にも、
- 途中でapp.js の applicationServerKey を変更するとどうなるのか?
- 複数のユーザで、同一クライアント同一Webブラウザでサブスクリプションを登録するとどうなるのか?
- push_subscriptionsテーブルにどういったデータが入っているのか?
といったことを確認すると良い。