PHPとJavaScriptだけでPush通知を送信できるようにする

Push通知ってハードルが高いイメージの方、多いと思います。

僕もその認識でした。Firebaseとかなんとか使うのかなーって。

実際はものすごく簡単です。PHPとJavaScriptだけで完結します。
(理由は後に分かります)

ということで、やっていきましょう。

記事の内容

この記事では、Webサイト上でのPush通知をservice worker、PHPのライブラリを用いて作成していきます。

あくまでPush通知を行うことにフォーカスを向けるため、解説を省略する部分もありますが、ご了承ください。

※Service Worker、PWAの知識とPHPのComposerの知識(composerでライブラリをインストールできる程度)がないと厳しいかもしれません。読み進めて分からないところはどんどん調べてください。

フォルダ構成

icon.pngは正方形のアイコン画像を用意しておいてください。
サイズは128×128、またはそれ以上です。

ファイルの内容を記載していくので、同じように作成していきましょう。

index.html

<!DOCTYPE html>
<html lang="ja">
<head>
  <title>WebPushテスト</title>
  <meta charset='utf-8'>
  <meta name='viewport' content='width=device-width,initial-scale=1'>

  <!-- アドレスバー等のブラウザのUIを非表示 -->
  <meta name="apple-mobile-web-app-capable" content="yes">
  <!-- default(Safariと同じ) / black(黒) / black-translucent(ステータスバーをコンテンツに含める) -->
  <meta name="apple-mobile-web-app-status-bar-style" content="black">
  <!-- ホーム画面に表示されるアプリ名 -->
  <meta name="apple-mobile-web-app-title" content="WebPush Test">
  <!-- ホーム画面に表示されるアプリアイコン -->
  <link rel="apple-touch-icon" href="icon.png">
  <!-- ウェブアプリマニフェスト -->
  <link rel="manifest" href="manifest.json">

  <script defer src='service-worker.js'></script>
  <script src='webpush.js'></script>
</head>

<body>
  <a href="javascript:allowWebPush()">WebPushを許可する</a>
</body>
</html>

manifest.json

scope、starturlに関しては各自で指定してください。

{
    "name": "WebPush Test",
    "short_name": "WebPush Test",
    "theme_color": "#3c76ff",
    "background_color": "#0011d3",
    "display": "standalone",
    "scope": "/pushtest",
    "start_url": "/pushtest/"
  }

service-worker.js

function base64Decode(text,charset){
  return fetch(`data:text/plain;charset=${charset};base64,`+text).then(response=>response.text());
}
// プッシュ通知を受け取ったときのイベント
self.addEventListener('push', async function (event) {
    //serverからのメッセージ
    var msg=event.data.text();
    msg=await base64Decode(msg);
    msg=msg.split('!|!');
    const title = msg[0];
    const options = {
        body: msg[1], // メッセージ
        tag: msg[2], // 通知固有のタグ(このプログラムではURLの伝達に使用)
        icon: 'icon.png', // アイコン
        badge: 'icon.png' // アイコン(縮小版)
    };
    event.waitUntil(self.registration.showNotification(title, options));
});

// プッシュ通知のクリックイベント
self.addEventListener('notificationclick', function (event) {
    var notification_url=event.notification.tag;//通知に関連付けられているURL
    event.notification.close();

    event.waitUntil(
        // プッシュ通知をクリックしたときに開くURL
        clients.openWindow(notification_url)
    );
});


// Service Worker インストール時に実行
self.addEventListener('install', (event) => {
    console.log('service worker install ...');
});

※PHPのSend.php、JSのwebpush.jsについては下記手順の後に作成していきます。

公開鍵/秘密鍵の作成

Push通知を送信するには、公開鍵と秘密鍵が必要です。

自前で用意するのは難しいので、下のサイトで生成してくれます。

サイトを開いたら、そこに記載されている生成されたPublic KeyとPrivate Keyのセットをメモしておきます。

文字が抜けないように気を付けてコピーしてください。

webpush.js

サービスワーカーを登録するパスも、各自で指定してください。

//サービスワーカーを登録
self.addEventListener('load', async () => {
    if ('serviceWorker' in navigator) {
        window.sw = await navigator.serviceWorker.register('/pushtest/service-worker.js', {scope: '/pushtest/'});
    }
});


//Push通知を許可する仕組み
async function allowWebPush() {
    if ('Notification' in window) {
        let permission = Notification.permission;

        if (permission === 'denied') {
            alert('Push通知が拒否されているようです。ブラウザの設定からPush通知を有効化してください');
            return false;
        } else if (permission === 'granted') {
            alert('すでにWebPushを許可済みです');
            return false;
        }
    }
    // 取得したPublicKey
    const appServerKey = 'ここにPublicKeyを入力';
    const applicationServerKey = urlB64ToUint8Array(appServerKey);

    // push managerにサーバーキーを渡し、トークンを取得
    let subscription = undefined;
    try {
        subscription = await window.sw.pushManager.subscribe({
            userVisibleOnly: true,
            applicationServerKey
        });
    } catch (e) {
        alert('Push通知機能が拒否されたか、エラーが発生しましたので、Push通知は送信されません。');
        return false;
    }


    // 必要なトークンを変換して取得
    const key = subscription.getKey('p256dh');
    const token = subscription.getKey('auth');
    const request = {
        endpoint: subscription.endpoint,
        userPublicKey: btoa(String.fromCharCode.apply(null, new Uint8Array(key))),
        userAuthToken: btoa(String.fromCharCode.apply(null, new Uint8Array(token)))
    };

    console.log(request);
}



//トークンを変換するプログラム
function urlB64ToUint8Array (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;
}

中央付近にあるconst appServerKey = 'ここにPublicKeyを入力';の部分を先ほど取得したPublic Keyに置き換えてください。

このような感じです。

const appServerKey = 'BBB8jP1FFFbeg56CDpQQQXKKKQnhwwrh8OwDuY2mkQtDDDxv8QC91Ozpn8gPPPDFXPYMpH71HYnAAA';

ユーザーの認証情報を取得

先ほど作成したファイルをすべてサーバーにアップロードできたら、index.htmlを開きます。

「WebPushを許可する」のリンクをクリックすると、以下画像のように許可が求められるのので、「許可」をクリックします。

ここでデベロッパーツールを開いてください。Ctrl+Shift+Iで開くことができます。

コンソールにendpoint、userAuthToken、userPublicKeyが表示されているので、コピーします。

実はここに簡単な理由が隠されています!

endpointのURLを良く見てみてください。

https://fcm.googleapis.com/fcm/send/...

そう、実はここでFirebaseが利用されています。

このようにブラウザ側の通知APIがエンドポイントを生成してくれるため、このURLにPHPで通知を送信することで、登録されたサービスワーカーに通知を送ることができるようになります。

ライブラリのインストール

web-pushというライブラリを使用します。

Composerからインストールしてください。

composer require minishlink/web-push

インストールする上での注意

注意ですが、PHPバージョンが古い場合、ライブラリ最新のリリースがインストールできません。
場合によっては通知が送信できない可能性もありますので、PHPのバージョンはできるだけ最新に近いものを利用しましょう。

php -v

とコマンドを打って、バージョンが古い場合は更新しておいてください。

送信するPHPファイルの作成

Send.phpを作成します。

先ほどよりもPublic Keyなどの置き換えするところが多いので、入力するところ、順番等ミスがないように注意をして作成してください。

Send.php

<?php
require_once 'vendor/autoload.php';

use Minishlink\WebPush\WebPush;
use Minishlink\WebPush\Subscription;

if(!isset($_POST["n_title"]) || !isset($_POST["n_body"]) || !isset($_POST["n_url"])){
  exit("情報が入力されていません");
}


const VAPID_SUBJECT = 'サイトのドメイン(例:https://example.org)';
const PUBLIC_KEY = 'サイトで取得したPublic Key(公開鍵)';
const PRIVATE_KEY = 'サイトで取得したPrivate Key(秘密鍵)';

// push通知認証用のデータ
$subscription = Subscription::create([
    'endpoint' => 'consoleに表示されたendpoint',
    'publicKey' => 'consoleに表示されたpublickey',
    'authToken' => 'consoleに表示されたauthToken',
]);

// ブラウザに認証させる
$auth = [
    'VAPID' => [
        'subject' => VAPID_SUBJECT,
        'publicKey' => PUBLIC_KEY,
        'privateKey' => PRIVATE_KEY,
    ]
];

$webPush = new WebPush($auth);

$body_msg=base64_encode($_POST["n_title"].'!|!'.$_POST["n_body"].'!|!'.$_POST["n_url"]);

$report = $webPush->sendOneNotification(
    $subscription,
    $body_msg
);

$endpoint = $report->getRequest()->getUri()->__toString();

if ($report->isSuccess()) {
    echo '送信成功しました';
} else {
    echo '送信失敗です';
}

これで完成です!Send.phpにPOSTでアクセスしてみてください。

パラーメーターは以下の通りです。

n_title通知のタイトル
n_body通知のコンテンツ(テキスト)
n_url通知をクリックした際のURL

Jqueryでの実装例

テストのためSend.phpを作成し、クライアントから送信できるようにしていますが、実際はcronなどで自動的にSend.phpを実行する実装になります。

$.ajax("https://example.org/pushtest/Send.php",{
  type:"POST",
  data:{
    n_title:"タイトル",
    n_body:"コンテンツです。",
    n_url:"https://example.org/notifaction_page"
  }
}).done((res)=>{
  if(res==="送信成功しました"){
    alert("送信成功!");
  }else{
    alert("送信失敗!");
  }
});

実際にユーザーに配信できるようにするために

あくまで今回はサンプルとして、データベースを用いない形で動作させました。

途中console.log()を通してブラウザのデベロッパーツールから情報を取り出したと思います。

この情報を、クライアントからサーバーに送信し、データベースに保存します。
その後、送信するときにデータベースから先ほど置き換えた箇所に変数で代入することで、ユーザーに個別に配信することができます。

また、カスタマイズ次第では通知に表示されるアイコンを変えることなども可能です。

通知が送信できない・エラーが出る場合

「送信失敗です」と表示される場合

ユーザーの認証データが古いもの(期限切れ)である可能性が高いです。

認証データを再生成してそのデータで試してみてください。

ブラウザのサイトデータを削除した後などに期限切れになる可能性があるので、期限切れを検知して更新できるプログラムも作るべきといえます。今回の記事では触れませんが。

PHPのエラーが発生する場合

  1. Public Key等の入力位置を間違えている・文字が抜けている
    正しくない値を入れるだけでエラーになってしまうので注意してください。
  2. ライブラリ・PHPのバージョンが古い
    ライブラリやPHPのバージョンは新しいものでないと動かない場合があります。バージョンの確認を行ってください。
  3. Composerのパスを正しく指定していない
    Send.phpの一行目にcomposerを読み込んでいますが、実際にサーバーにインストールされているものを読み込むようにファイル指定を行っているか確認してください。
  4. 「情報が入力されていません」のレスポンスが返ってくる場合
    POSTで、n_title,n_body,n_urlをすべて送信していないとエラーになります。

参考

以下のサイトを参考にさせていただきました。