PHPからiOS/Androidアプリへプッシュ通知 2023(4)PHP編

広告:超オススメUnity Asset
  広告:超オススメUnity Asset

PHPからiOS/Androidアプリへプッシュ通知するための準備MEMOです。このシリーズは、

のように分けて書いてます。iOSアプリへはApnsPHP(あるいはcurl_exec)を介してAPNs(Apple Push Notification Service)、AndroidアプリへはFirebase Admin SDK for PHPを介してFCM(Firebase Cloud Messaging)を使います。

追記:その後、共有サーバー等を使わずにVPSにサーバーを構築して試したらAPNsもうまくいきました。「エックスサーバーVPSにiOSのAPNsプッシュ通知のためのサーバーを構築する

PHP編

まず、今回は特定のデバイスにターゲットを絞って通知を行うことを目指します。デバイスを特定するために、(1)Unity編でiOS端末、Android端末それぞれで実際にアプリを動作させ、そのデバイス特定の文字列「デバイストークン」を取得し、PHP開発環境へその文字列を持ってきておきます。

Android / FCM編

iOSのAPNs編から書こうと思ったのですが、Firebaseに任せてるだけあってAndroidへの通知の方が圧倒的にスムーズに行ったので、Android編から書きます。

PHP側セットアップ(Android / FCM編)

Firebase Admin SDK for PHPを使うのが良さそうなので、composerでインストール。CakePHPなどの場合ルートで以下のコマンドでセットアップすれば各コントローラから利用可能。(Packagist : kreait/firebase-php

composer require kreait/firebase-php

呼び出し部分(Android / FCM編)

(1)Unity編で調べたデバイストークンとメッセージだけ引数にして呼び出す形に。「_cloud_message_android」と名付けた関数を呼ぶだけのことをしてます。

$token_android = "do-05vwoS1We0jSRORik1k:APA91bESUV76o5zv5NbIP2zLFLPN6XcUWluZMvcPPU6eblg6Uwl1lwm2mtAeRhWRhNFVFJvokdoskJlRwIK0oCdJNJGCYUy5alQNBQj28hclfrcPpxuxxxL0NP4tkgY_YM20xxxxe745"; //一部伏せてます
$message = "shiraishi". date("mdHis");
$error_android = $this->_cloud_message_android($token_android,$message);

呼ばれる関数部分(Android / FCM編)

(3)Android編で入手した「quickreplaceapp-xxxxxx.json」をサーバーの隠れた場所に配置して、以下くらいシンプルなコードで送れます。上記のSDKさえ上手くセットアップできればかなり簡単に実装できました。

use Kreait\Firebase\Factory;
use Kreait\Firebase\Messaging\CloudMessage;
use Kreait\Firebase\Messaging\Notification;
(中略、どっかから呼び出す)
private function _cloud_message_android( $token, $msg )
{
    $factory = (new Factory)
        ->withServiceAccount(パス . 'quickreplaceapp-xxxxxx.json');

    $cloudMessaging = $factory->createMessaging();

    $message = CloudMessage::withTarget('token', $token)
        ->withNotification(Notification::create('Title', $msg))
        ->withData(['key' => 'value']); //何かつけるなら

    $error = $cloudMessaging->send($message);

    return $error;
}

iOS / APNsPHP編

さて、問題はAPNsの方。思った以上にサーバー環境がややこしくなってる時期なのかも?環境の問題が大きいですが、環境や証明書さえ整っていれば、コードに関してはAndroidのFCMとさほど変わらない作業です。

サーバー環境について(iOS / APNsPHP編)

(2)iOS編の中盤でも書いたのですが、

アップルのプッシュ通知APNsでは、HTTP/2が必須となっていて、サーバー側のPHPでHTTP/2対応のcURLが使えないと

に加えて、PHP7.4がまだ多く使われている状況でHTTP/2に対応していないcURLのままのレンタルサーバーが多い上に、自力でサーバーを立ち上げると今度はPHP8がセットアップされ、ここで使おうとしているApnsPHPがあまりメンテナンスされておらずPHP8の型の厳しさについていけてないような状況でいろいろドタバタしてる感じです。

まず、XserverとHetemlなどの共有サーバーでは、cURLがHTTP/2に対応できておらず、APNsを叩いても通知が送れません。サポートに連絡しましたが、おそらく対応は難しそうです。VPS(仮想専用サーバー)系で、PHP7.4上でcURLがHTTP/2に対応できる環境を作れれば、現状ではそれが最も良さそうです。この環境は割とすんなりAPNs通知PHPが作れます。また、レンタルサーバーでもそういう環境(PHP7.4+HTTP2)が提供されているならほぼここで書いてるような問題もない事でしょう。

cURLがHTTP/2に対応しているかどうかを確認するには、SSHでサーバーに入り、curl -V と叩いた結果の「Features: 」の一覧に「HTTP2」とあれば良いようです。

追記:ここでHTTP2があったとしても拡張モジュールとしてインストールされてないとダメ。VPSにサーバー構築するMEMO「エックスサーバーVPSにiOSのAPNsプッシュ通知のためのサーバーを構築する」を。

あるいは、

curl -I https://api.push.apple.com

と叩いて、HTTP/2apns-idが返ってくるでも。ダメなところはcurl: (1) Received HTTP/0.9 when not allowedみたいに返ってくるようです。

困った時のDigitalOcean

追記:以下にDigitalOceanのワンクリックWebサーバーで試していますが、Dropletに最初から構築する方がオススメ

VPSはそこそこな値段な上に、「試す」ために導入するには壁が高い。追記;エックスサーバーVPSやDigitalOceanのDropletはオススメ。

そこで安価でルート権限からサーバーを即立ち上げられるDigitalOceanで、色々実験するのがオススメです(最終的にどうするか問題がありますが、まずは動くところまでということで)。

DigitalOcean で CakePHP4をセットアップするまでの流れをMEMOしておきます。DigitalOceanの「Apps」でや...

このMEMOで書いた通り、かかっても月$5程度で実験できるし、失敗したら最初からサーバーを作り直せばいいですね。ただ、Webサーバーを構築するとPHP8からしかセットアップできないのと、色々実験成功した後、ここを公開用まで使うかどうかというところ。問題はデータベースがちょい高い。安価なレンタルサーバーで無料で使えるMySQLなんかがここでは月$15か、と悩みどころですね。 DropletでMySQLを自分でインストールすべし。

今現在、新規にWebサーバーを立ち上げると、ポチッとPHP8+HTTP/2対応のWebサーバー環境が作れるようです。

ちょい脱線しましたが、このMEMOではとりあえずDigitalOceanを使ってPHP8+HTTP2環境をベースに書いていこうと思います。

PHP側セットアップ(iOS / APNsPHP編)

Android編と同様にcomposerAPNsPHPをインストール。と、ここでまず、つまずきます。どれをインストールすればいいのか、です。

HTTP/2 Protocol support.

「HTTP2対応はここからダウンロードしてね」とあるのでダウンロードでもいいですが、composerについては、forkされてるものがかなり多くあり、勝手に3.0を作ってるのとかもあります。とりあえずPackagistは以下とあるので、そこのv2.0.0-alphaブランチのものをセットアップ。(Packagist : duccio/apns-php

composer require duccio/apns-php:v2.0.0-alpha

あ、ちなみに、composerで入れたものを削除して入れ直したい場合などは、 composer remove protonlabs/apns-php --no-update とかしてから上記をまた打てば入れ替えできます。(これは別のapns-phpを入れててそれを消した例)

呼び出し部分(iOS / APNsPHP編)

さて、ここはAndroidとほぼ同じ。(1)Unity編で調べたデバイストークンとメッセージだけ引数にして呼び出す形に。「_push_notification_ios」と名付けた関数を呼ぶだけのことをしてます。

$token_ios = "efd3f66634621c77945e3a9dxxxxx767a94b700b32796dff4a231e7e706dafe"; //一部伏せてます
$message = "shiraishi". date("mdHis");
$error_ios = $this->_push_notification_ios($token_ios, $message);

呼ばれる関数部分(iOS / APNsPHP編)

サーバー環境がPHP7.4+HTTP2であれば、おそらくこの関数だけで通知が送れるようになると思います。PHP8の場合は後述。

(2)iOS編で作成した「server_certificates.pem」をサーバーの隠れた場所に配置。そして、ApnsPHPを使う場合は、

の「Verify peer using Entrust Root Certification Authority」の項にある

から「Entrust.net Certificate Authority (2048)」の「entrust_2048_ca.cer」をダウンロード。ダブルクリックしてキーチェーンアクセスに入れ、システムルートの「Entrust Root Certification Authority」を探して、右クリックから.pem形式のファイルを保存(例「Entrust_Certification_Authority_2048.pem」)コレも上記同様にサーバーの隠れた場所に配置。

以下のコード内でそのパスを指定するようにして、下記関数を呼び出します。

use ApnsPHP_Abstract;
use ApnsPHP_Message;
use ApnsPHP_Push;
use ApnsPHP_Log_Interface;
(中略、どっかから呼び出す)
private function _push_notification_ios( $token, $msg ){

// 定義されてない場合のため
// if (!defined('CURL_VERSION_HTTP2')) {
// 	define('CURL_VERSION_HTTP2', 1<<16);
// }

// HTTP/2に対応してるかコード上で確認したい時
// if (curl_version()['features'] & CURL_VERSION_HTTP2) {
// 	$msg = "HTTP/2 は利用できます。\n";
// } else {
// 	$msg = "HTTP/2 は利用できません。\n";
// }

	$push = new ApnsPHP_Push(
		ApnsPHP_Abstract::ENVIRONMENT_PRODUCTION,
		パス . 'server_certificates.pem',
		ApnsPHP_Abstract::PROTOCOL_HTTP // ココ追加
	);
// $push->setLogger(new ApnsPHP_Log_Custom); //CakePHPで使う場合などでログをさせたくないとき

	$push->setRootCertificationAuthority(パス . 'Entrust_Certification_Authority_2048.pem');
	$push->connect();
	$message = new ApnsPHP_Message($token);

	// Set the topic of the remote notification (the bundle ID for your app)
	$message->setTopic('com.mushikago.QuickReplace'); // これがないと通知こない

	$message->setText('Hello APNs-enabled device! '.$msg);
	/* add */
	$message->setBadge(3);
	$message->setSound();
	/* add end */

	$push->add($message);
	$push->send();
	$push->disconnect();

	$aErrorQueue = $push->getErrors();
	if (!empty($aErrorQueue)) {
		var_dump($aErrorQueue);
	}

	return $aErrorQueue;
}

一緒にインストールされる「sample_push_http.php」を参考に。

setLoggerについては、

のやり方で「ApnsPHP_Log_Custom」クラスを作ってオフに。CakePHPで使うとこのログ出力がコンフリクトしてエラーになるので。(しかし、10年前の記事。。他のやり方あるかも?)

PHP7.4+HTTP2環境であれば、ここまででうまく通知できるはずです。

PHP8+HTTP2の場合だと問題が

なかなかすんなり行かないもので、これはつまりApnsPHPがメンテナンスされてないからですかね。Forkして自分で対応してるのが多いわけです。

DigitalOceanのPHP8環境で、ここまでで通知までは送られるようになるのですが、送った後、今度は、「/vendor/duccio/apns-php/ApnsPHP/Push.php, line 359」で以下のエラーが発生。PHP8で型が厳しくなったことが原因?ですかね。(エラー発生しながらデイバイスに通知は届く)

fread(): Argument #1 ($stream) must be of type resource, CurlHandle given

この対応はされてないので、Packagistで他にPHP8対応してるものを探すか自力で直すか、、

これvendor内なのでgit経由でDigitalOceanには送れないので(vendor以下をいじるのはオススメしません。あくまでも実験ということで)、Macのローカル環境でCakePHP立ち上げて対処法を試すと、ROOT/vendor/duccio/apns-php/ApnsPHP/Push.php の359行目の $sErrorResponse = @fread($this->_hSocket, self::ERROR_RESPONSE_SIZE); 部分を

// $this->_hSocketがCurlHandleであるか確認
if ($this->_hSocket instanceof CurlHandle) {
	// エラー処理
	$sErrorResponse = false;
} else {
	// ファイルハンドルが有効であるか確認
	if (is_resource($this->_hSocket)) {
		// ファイルを読み取る
		$sErrorResponse = fread($this->_hSocket, self::ERROR_RESPONSE_SIZE);
				// // エラーチェック
		// if ($sErrorResponse === false) {
		// 	// エラー処理
		// 	$sErrorResponse = "エラーが発生しました。";
		// }
	} else {
		// // ファイルハンドルが有効でない場合の処理
		// $sErrorResponse = "ファイルハンドルが有効ではありません。";
		$sErrorResponse = false;
	}
}

みたいに修正するとエラー回避できます。型確認するような記述をかますと。

対応済みのPackagist見つけたら追記します。

iOS / curl_exec編

そもそもApnsPHPを使わない?

PHPからAPNsを叩くにはApnsPHPを使うだろう、という固定概念からドツボにハマりましたが、黎明期ということなのかなんなのか、Alphaのまま先に進んでる感がないですね。

HTTP/2環境さえ整っていれば、以下の回答にある直接curl_execでapi.push.apple.comを叩く方が余計なトラブルがないかも。PHP8問題も引っかからないです。

I started creating some code based upon this for sending push notifications from PHP. However now that I have understood there is a new API which utilizes HTTP...

呼び出し部分(iOS / curl_exec編)

$token_ios = "efd3f66634621c77945e3a9dxxxxx767a94b700b32796dff4a231e7e706dafe"; //一部伏せてます
$message = "shiraishi". date("mdHis");
$error_ios = $this->_push_notification_ios_without_apnsphp($token_ios, $message);

呼ばれる関数部分(iOS / curl_exec編)

private function _push_notification_ios_without_apnsphp($token, $msg){
    if(defined('CURL_HTTP_VERSION_2_0')){

        $device_token   = $token;
        $pem_file       = パス . 'server_certificates.pem';;
        //$pem_secret     = 'your pem secret';
        $apns_topic     = 'com.mushikago.QuickReplace';


        $sample_alert = '{"aps":{"alert":"'.$msg.'","sound":"default"}}';
        //$url = "https://api.development.push.apple.com/3/device/$device_token";
        $url = "https://api.push.apple.com/3/device/$device_token";

        $ch = curl_init($url);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $sample_alert);
        curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0);
        curl_setopt($ch, CURLOPT_HTTPHEADER, array("apns-topic: $apns_topic"));
        curl_setopt($ch, CURLOPT_SSLCERT, $pem_file);
        //curl_setopt($ch, CURLOPT_SSLCERTPASSWD, $pem_secret);
        $response = curl_exec($ch);
        $httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);

        //On successful response you should get true in the response and a status code of 200
        //A list of responses and status codes is available at
        //https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/TheNotificationPayload.html#//apple_ref/doc/uid/TP40008194-CH107-SW1

//            var_dump($response);
        return $httpcode;

    }
}

 

以上、HTTP/2部分がサーバー環境的に結構厄介なケースがありそうですが、そこがクリアしていればって感じですね。FirebaseでiOSへの通知も行う、という路線がまだありそうですが、その辺はまた新たにMEMOします。