タグ「PHP」の 記事 9 件中 1 ~ 9 件を表示しています。

FCM (Firebase Cloud Messaging) は Android / iOS でプッシュ通知を実装する際に利用するサービスであるが、これが 2024 年 6 月 20 日から従来のレガシーな実装は廃止されるとメールで通知が来た。 なので FCM HTTP v1 API への移行を余儀なくされたわけだ。 移行に関しては公式ドキュメントに記載があるのでこの通りやればいいのだが、残念ながら PHP で書いた時の例がない。 Firebase Admin SDK を利用すれば比較的簡単に実装できるようだが、これも PHP 非対応である。 これだけメジャーで使われているのに何故こんなに PHP に対して塩対応なのか。 まあ言語仕様がアレだから Google 的には使わせたくないというのはわからないでもないが……。 ともかくこれをやる上で必要だったこと、ハマったことをここにメモっておく。

OAuth 2.0 token には Google APIs Client

前述の公式移行ドキュメントに記載のある通り HTTP ヘッダに OAuth 2.0 のアクセストークンを含める必要がある。 これを自前で発行・管理するのは面倒なので Google APIs Client Library for PHP を使ったらうまくいった。 尚、PHP が古い場合 composer require するライブラリバージョンを下げる必要がある。

// PHP 7.2 でも動くように古いバージョンを指定する
composer require google/apiclient:2.12.1

実装は以下のような感じ:

// Firebase Console から持ってきた秘密鍵 JSON を指定する. 本来は環境変数に書くべきところ
putenv('GOOGLE_APPLICATION_CREDENTIALS=hoge/fuga.json');

// まず Google API Client で OAuth 2.0 トークンを取得する
$client = new Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope('https://www.googleapis.com/auth/firebase.messaging');
$httpClient = $client->authorize();

// push する JSON を組み立てる
$json = [
    'message' => [
        'token' => $token,
        'notification' => [
            'title' => 'FCM message',
        ],
        'data' => $data,
    ],
];
$url = 'https://fcm.googleapis.com/v1/projects/${project-id}/messages:send';
$result = $httpClient->post($url, ['json' => $json]);

data には key-value pair として双方とも string を指定する

上記のような実装で何度か試してみると 400: Bad Request が返却されて正しく通らない。 何故なのかずっと試行錯誤していたところ $data に含める PHP 連想配列は key, value 共に string でなければならないことがわかった。 つまり、

$data = ['key' => '1'];  // これは送信できる
$data = ['key' => 1];  // これは送信できない (値が int)
$data = ['key' => ['key2' => '1']];  // このようにネストした連想配列も送信不可

ちなみに legacy FCM API では前述のすべての例が送信できたので、全然気づかずハマってしまった。 ということで旧 API との互換性がなく、もしクライアント側でこのようなネストした値を parse している場合移行する時に工夫する必要があるかもしれない。

まさか IntelliJ IDEA の正式な日本語化パックプラグインが提供される日が来るとは思わなかった。 最近になって Pleiades が適用できなくなっていたので仕方なく英語のまま使っていたのだが JetBrains チームからお使いの IDE が日本語に対応しましたというメールが来たのでそれを知った。 PhpStorm のような IntelliJ IDEA の派生 IDE であればプラグインから「日本語」で検索して日本語化パックをインストールすることができるようになっていた。 これは楽だ。 Android Studio に関してはこれができなかったが、これに関しても Android Studioを日本語化する(トラブルシューティング付き) という題でやり方をまとめてくださっている方がいたので、その通りにやったらできた。 感謝。

この Blog のバックアップデータを Google Drive に定期的に保存しようと思ってスクリプトを書いたのだが、バックアップデータが 1 GB 以上あるので転送時にメモリが足りなくて強制終了してしまう。 php.ini のメモリの設定も確認したがちゃんと -1 (制限なし) になっている。 これを改善するには VPS をもっと良いプランにしなければならないが、そこにお金を使う気にはなれないので断念。 クライアント側で定期的に cron 的な処理を叩いて転送するのが良さそうだが、時間がある時にまたトライすることにする。

カレンダー UI を実装している時にその日が祝日かどうかまたはその日の祝日名が欲しいことがあると思う。 これが結構大変なのだが、私の場合はコジごみカレンダーというアプリで祝日判定に関して実装していたので、それを移植して祝日名を返すようにしたらできた。 2021 年は山の日、スポーツの日、海の日が特別ルールになっていたりといろいろと面倒だ。 あと振替休日判定が面倒。 ただ単に前の日が日曜日だったら、という判定では足りない。 尚 Laravel での実装前提なので日付を格納するインスタンスのクラスとして Carbon を使用している。 例えば素 PHP の場合は適当に $year $month $day などの int 型変数に書き換えればいいだろう。 また、今どきはこういう実装は JavaScript 側にすることが多いと思う。 そちらへの移植も難しくないだろう。

    /**
     * その日が祝日であれば祝日名を返す.
     * 祝日でなければ null を返す.
     *
     * @param Carbon $carbon 対象日付
     * @return string|null 祝日名 (祝日でない場合 null)
     */
    private function getHolidayName(Carbon $carbon): ?string
    {
        list($y, $m, $d, $w) = [$carbon->year, $carbon->month, $carbon->day, $carbon->dayOfWeek];
        if ($m === 1 && $d === 1) {
            return '元旦';
        } elseif (($y < 2000 && $m === 1 && $d === 15) || ($y > 1999 && $m === 1 && $d >= 8 && $d <= 14 && $w === Carbon::MONDAY)) {
            return '成人の日';
        } elseif ($m === 2 && $d === 11) {
            return '建国記念の日';
        } elseif (($y > 2018 && $m === 2 && $d === 23) || ($y > 1988 && $y < 2019 && $m === 12 && $d === 23)) {
            return '天皇誕生日';
        } elseif ($this->isShunbun($y, $m, $d)) {
            return '春分の日';
        } elseif ($m === 4 && $d == 29) {
            return '昭和の日';
        } elseif ($m === 5 && $d === 3) {
            return '憲法記念日';
        } elseif ($m === 5 && $d === 4) {
            return 'みどりの日';
        } elseif ($m === 5 && $d === 5) {
            return 'こどもの日';
        } elseif (($y > 1995 && $y < 2003 && $m === 7 && $d === 20) || ($y > 2002 && $y !== 2021 && $m === 7 && $d >= 15 && $d <= 21 && $w === Carbon::MONDAY) || ($y === 2021 && $m === 7 && $d === 22)) {
            return '海の日';
        } elseif (($y > 2015 && $y !== 2021 && $m === 8 && $d === 11) || ($y === 2021 && $m === 8 && $d === 8)) {
            return '山の日';
        } elseif (($y < 2003 && $m === 9 && $d === 15) || ($y > 2002 && $m == 9 && $d >= 15 && $d <= 21 && $w === Carbon::MONDAY)) {
            return '敬老の日';
        } elseif ($this->isShubun($y, $m, $d)) {
            return '秋分の日';
        } elseif (($y < 2000 && $m === 10 && $d === 10) || ($y > 1999 && $y !== 2021 && $m === 10 && $d >= 8 && $d <= 14 && $w === Carbon::MONDAY) || ($y === 2021 && $m === 7 && $d === 23)) {
            return $y > 2019 ? 'スポーツの日' : '体育の日';
        } elseif ($m === 11 && $d === 3) {
            return '文化の日';
        } elseif ($m === 11 && $d === 23) {
            return '勤労感謝の日';
        } else {
            return null;
        }
    }

    /**
     * 春分の日かどうかを判定して返す.
     *
     * @param int $y 年
     * @param int $m 月
     * @param int $d 日
     * @return bool 春分の日かどうか
     */
    private function isShunbun(int $y, int $m, int $d): bool
    {
        $arg1 = $y < 1980 ? 20.8357 : 20.8431;
        $arg2 = $y < 1980 ? 1983 : 1980;
        return $m === 3 && floatval($d) === floor($arg1 + 0.242194 * ($y - 1980) - floor(($y - $arg2) / 4.0));
    }

    /**
     * 秋分の日かどうかを判定して返す.
     *
     * @param int $y 年
     * @param int $m 月
     * @param int $d 日
     * @return bool 秋分の日かどうか
     */
    private function isShubun(int $y, int $m, int $d): bool
    {
        $arg1 = $y < 1980 ? 23.2588 : 23.2488;
        $arg2 = $y < 1980 ? 1983 : 1980;
        return $m === 9 && floatval($d) === floor($arg1 + 0.242194 * ($y - 1980) - floor(($y - $arg2) / 4.0));
    }

    /**
     * 対象日が振替休日かを判定して返す.
     *
     * @param Carbon $target 対象日
     * @return bool 振替休日かどうか
     */
    private function isExtra(Carbon $target): bool
    {
        // 前日が日曜かつ祝日であるならば振替休日
        $carbon = $target->copy();
        $carbon->subDay();
        if ($this->getHolidayName($carbon) !== null) {
            if ($carbon->dayOfWeek === Carbon::SUNDAY) {
                return true;
            }

            // 2007 年以後では日曜の祝日が発生した場合次の平日が振替休日となる: GW の振替休日判定が増える
            if ($carbon->year > 2006) {
                // 前日と後日が祝日であったならば挟まれた日も祝日
                $carbon->addDays(2);  // 対象日の後日に移動
                if ($this->getHolidayName($carbon) !== null) {
                    return true;
                }

                // 前々日から日曜の祝日を順に探索. 日曜でなく祝日だけ満たしていたらその前の日を探索し続ける. 祝日でなくなったら終了
                $carbon->subDays(3);  // 後日から前々日へシフト
                while ($this->getHolidayName($carbon) !== null) {
                    if ($carbon->dayOfWeek === Carbon::SUNDAY) {
                        return true;
                    }
                    $carbon->subDay();
                }
            }
        }
        return false;
    }

これを以下のように使用できる:

$carbon = Carbon::now();
$holidayName = $this->getHolidayName($carbon) ?? ($this->isExtra($carbon) ? '振替休日' : '');

$holidayName には祝日名もしくは振替休日という文字列、対象でない場合は null が格納される。

Laravel 学習中だがモデルに関して感じたことを書く。

xxxRaw メソッド

O/R マッパーは確かに便利なのだがちょっと凝ったことをしようとすると SQL だと簡単に書けるのに O/R マッパーでどういう風に表現するのか分からずに実装できないという問題がある。 CakePHP などは顕著な問題で特にアソシエーションなどを使用すると正しいはずなのに何故か動かないというコードになりがちだ。 しかも COUNT(*)SUM(*) を書きたいだけでも $query->func()->xxx() などという訳のわからない記法を要求される。 これが覚えられない。

Laravel だと DB::raw(), selectRaw()whereRaw(), orderByRaw() などの低レベルに書けるタイプのメソッドが用意されており、これを使うと対象部分だけ SQL 直で書くことができる。 単純な機構だがとても便利だ。 例えば以下のような感じだ:

/**
 * 年月ごとの記事数を返却する.
 *
 * @return \Illuminate\Http\JsonResponse
 */
public function countYearMonth(): JsonResponse
{
    $posts = Post::query()
        ->groupBy(['year', 'month'])
        ->orderByDesc('year')
        ->orderByDesc('month')
        ->get(['year', 'month', DB::raw('COUNT(id) AS count')]); // DB:raw() で SQL 直書き
    return Response::json($posts);
}

これは覚えやすい。 「何故既存の O/R マッパーは使いにくいのか」がよく分かっている人が設計した感じがする。

何もしないと取得結果がすべて string になってしまう

クエリを投げて DB からモデルに取得した結果が INTEGER カラムであったとしてもすべて string になってしまう。 これだと毎回変換しなければならず使いにくい。 これに関しては【Laravel】DBから取得した値を$castsで型変換する【Attribute Casting】に記載がある通り、モデルに型変換したいカラムを明示的に指定する。

class Post extends Model
{
    use SoftDeletes;

    // このように定義しておけば year, month, day, count は取得時に integer にキャストされる
    protected $casts = ['year' => 'integer', 'month' => 'integer', 'day' => 'integer', 'count' => 'integer'];
}

上記の例で、例えば countposts テーブル内に存在しないカラムだったとしても問題ないし、DB::raw('COUNT(id) AS count') で取った結果がちゃんと integer で返ってくる。 この辺りは便利なのか不便なのかちょっとよく分からなかった。

Composer の設定を同期してしまう

タイトルに PhpStorm と書いたが私は今 IntelliJ IDEA Ultimate を使用しているのでそちらで再現した問題である。 だが検索すると PhpStorm でも同様の問題に遭ったという投稿が見受けられるので同様と思われる。

PhpStorm (IntelliJ IDEA Ultimate) では PHP language level (PHP バージョン) を設定することができる。 これによって例えば PHP 7.1 などに設定して PHP 7.1 の文法 (nullable なタイプヒンティングなど) を使ってもエディタ上でエラーにならない。 だが、一旦 IDE を再起動してしまうと何故か元に戻ってしまい文法エラー表示されてしまう。 これは何なのだろう、と調べてみたがデフォルトで composer.json の設定と同期されてしまうからだった。

対処

以下のどちらかを実施する:

1. composer.json の言語レベルを見直す

composer.json の以下の箇所を使用したい PHP バージョンに書き換えればよい:

"require": {
    "php": ">=7.1",
}

2. composer.json と IDE 設定の同期を止める

これに関しては IntelliJ のサポートフォーラムに投稿があった のでこの通り実施する。

That looks like IDE is synchronising these settings with your composer.json. This happens on project opening or when that file change is detected. "Settings/Preferences | Languages & Frameworks | PHP | Composer" -- uncheck appropriate option there ("Synchronize IDE Settings with composer.json").

ということで Settings -> Preference -> Languages & Frameworks -> PHP -> Composer の「Synchronize IDE Settings with composer.json」という設定を外せばよい。

CakePHP 3 ではアソシエーションを設定し関連データを取得することが出来る。 CakePHP 2 以前もあった仕組みであるが hasMany 若しくは belongsToMany で関連付けたデータを contain にして where で絞り込もうとすると失敗する:

$this->find()
    ->contain(['Comments'])  // Post has many comments とする
    ->where(['Comments.type' => 1]);  // 指定できそうに見えるが失敗!!

この PostbelongsTo の場合は内部的に INNER JOIN 若しくは OUTER JOIN された SQL が発行されるので大丈夫なのだが hasMany 若しくは belongsToMany複数の SQL に分割して発行されるので、最初の SQL に Comments が存在せず、条件を指定してもそれが最初の SQL に対し指定されてしまい「存在しないカラムに対し条件を指定している」ことになり失敗してしまう:

# 内部的にこのように分けて発行されるので最初の SQL には Comments は存在しない!!
SELECT * FROM articles;
SELECT * FROM comments WHERE article_id IN (1, 2, 3, 4, 5);

これを避けるために contain に対し以下のようにクロージャを渡すことにより 2 つ目の SQL に対し条件を渡すことが出来る:

$query = $articles->find()->contain([
    'Comments' => function ($q) {
       return $q->where(['Comments.type' => 1]);
    }
]);

つまり contain している Entity のアソシエーションがどの関係であるかを思い浮かべて適切なコードを書かなければならない。 この手の O/R マッパーを使用する際はしばしば内部で吐かれる SQL を dump しながら挙動を確認しなければならないのが辛い所ではある。

普段は Vagrant 上に Composer をインストールしていた

昨今の PHP 開発では Composer は必須であるが私はいつも Vagrant 上の VM に Composer をインストールしており vagrant ssh した上で composer install / update するなどしていた。 それでも勿論良いのだが PhpStorm 上に Vagrant や Composer のメニューが有り PhpStorm から出ずに vagrant up から vagrant ssh していろいろやったり composer update したりが可能になっている。 このメニューを使用するには PhpStorm 側にローカルの Composer の実行パスを教える必要がある。 つまりローカルに Composer をインストールする必要があった。 そして Composer を使うには PHP が必要となるので PHP をインストールして Composer をインストールする手順を記す。

macOS の場合はほぼ Linux の場合と同じなのでやりやすいが Windows の場合は意外と困る。 あと PHP をインストールするのは Xampp を使うと楽なのだが余計な Apache や MariaDB はインストールしたくないので素の PHP をインストールすることとする。

PHP インストール

Windows 用の PHP バイナリをダウンロードするサイトがあるのでここからダウンロードする。 バージョンは使いたいものでいいと思うが、現時点での最新は PHP 7.1 なのでそれをダウンロード。 Non Thread Safe か Thread Safe のどちらがいいのかというところだが Apache を使う場合は Thread Safe にせよとの事らしい。 コマンドラインから使うだけの分にはどちらでも良さそうだ。

ダウンロードした zip を適当な位置に解凍する。 自分は C:\php71 にした。

その後コマンドラインから実行する為に PATH を通す。 よくある手順だがコントロールパネルの「システムの詳細設定」からの「環境変数」を押下。 自分だけに適用したい場合は「ユーザー環境変数」、ユーザ全員に適用したい場合は「システム環境変数」の PATH を選択する。 そこに先程 PHP をコピーしたディレクトリ C:\php71 を追記する。

php.ini

さて、用意した PHP にはまだ php.ini が無い。 だが元となる php.ini-development が置いてあるので、それをコピーして php.ini にリネームする。 ちなみに本番環境では php.ini-production を使うが、今回の場合は実行時にエラーメッセージが表示される development の方がいい。

そして php.ini の以下の場所のコメントアウトを外す (; を削除する):

; Windows だと ext ディレクトリに DLL が全て入っている
extension_dir = "ext"

; CakePHP 3 インストールに必要
extension=php_intl.dll
extension=php_mbstring.dll

; Composer インストールに必要
extension=php_openssl.dll

コマンドプロンプトを開き php -v などと叩き、正しく PHP バージョンが表示されることを確認する。

Composer

Composer のダウンロードページComposer-Setup.exe をダウンロードして実行する。 ここでローカル PHP のパスを聞かれるので、先程インストールしたものを教える。

コマンドプロンプトを開き composer と叩き正しく Composer のコマンドリストが表示されるのを確認する。

インストールが完了すると Composer が C:\ProgramData\ComposerSetup\bin\composer に入る (ProgramData は隠しフォルダなのでエクスプローラで普通にたどると見えないので注意)。 後は PhpStorm 側でこのパスを教えるなどすれば良い。

Vagrant

Vagrant が何かに関しては検索すれば幾らでも情報が出てくるのでここでは述べない。

まずは Vagrant をインストールする。 公式からダウンロードしてインストールすれば良い。 VirtualBox も入っていなければインストールしておく。

Ubuntu 16.04 LTS に関しては公式に box が提供されているのでそれを使用する:

vagrant init ubuntu/xenial64

これでカレントディレクトリに VagrantFile が出来るので、この中の以下の行のコメントアウトを外す:

" VM 上の 80 番ポートへのアクセスをホスト側の 8080 番ポートに変換する設定. 8080 が使用済なら適宜変更する
config.vm.network "forwarded_port", guest: 80, host: 8080

その後以下のコマンドを叩いて VM を起動する:

vagrant up --provider virtualbox

以下のコマンドで VM に SSH 接続する:

vagrant ssh

試しに Apache を導入し動作確認を行う:

sudo apt install apache2

http://localhost:8080/ にアクセスし、正しく Ubuntu の Apache テストページが表示されるのを確認する。

PHP 7.0

PHP 7.0 の環境を構築する。 Ubuntu 16.04 における PHP 7.0 関連の項目を検索するには以下のコマンドを叩く:

apt search php7.0

まとめて必要そうなのを入れてしまう:

sudo apt install libapache2-mod-php7.0 php7.0 php7.0-cli php7.0-intl php7.0-json php7.0-mbstring php7.0-sqlite3
/* sudo apt install php7.0-mysql */
/* sudo apt install php7.0-pgsql */

デフォルトの DocumentRoot が /var/www/html なのでそこに試しに PHP ファイルを置いてみる:

cd /var/www/html
sudo mv index.html index.html.old
sudo vi index.php

index.php の内容は以下とする:

<?php
phpinfo();

http://localhost:8080/ にアクセスし、正しく phpinfo が表示されるのを確認する。

DocumentRoot を /vagrant/xxx にする

デフォルトの DocumentRoot のままだとホスト側に /vagrant がマウントされる仕組みを活かすことが出来ないので変更する。 /vagrant/xxx だが xxx の部分は各自適当なプロジェクト名とする:

 sudo vi /etc/apache2/sites-available/000-default.conf

以下 DocumentRoot を編集しアクセス許可を与える:

DocumentRoot /vagrant/xxx
<Directory /vagrant/xxx>
        Options Indexes FollowSymLinks
        AllowOverride All
        Require all granted
</Directory>

テスト用のページを用意しておく。 先程の phpinfo 再利用でよいだろう:

mkdir /vagrant/xxx
sudo mv /var/www/html/index.php /vagrant/xxx
sudo service apache2 restart

http://localhost:8080/ にアクセスし、正しく phpinfo が表示されるのを確認した上で DOCUMENT_ROOT/vagrant/xxx になっているのを確認する。

mod_rewrite 有効化

CakePHP 3 では mod_rewrite を使用しているが Ubuntu の Apache のデフォルトでは有効になっていないので以下で有効にしておく:

sudo a2enmod rewrite
sudo service apache2 restart

CakePHP 3 インストール

先程の index.php は不要なので消しておく:

rm index.php

CakePHP 3 のインストールに関しては公式のドキュメントが素晴らしいのでこれに従っておけば問題ない。 ただ composer create-projectzipunzip が必要なようなので以下で入れておく:

sudo apt install zip unzip

あと composer create-project で出来るプロジェクトがディレクトリに含まれているので (この例だと /vagrant/xxx のこと) /vagrant 直下で composer create-project するのがいい。

尚 DB ばデフォルトで MySQL を使用するように config/app.php に書かれているので例えば SQLite を使うつもりでそのまま http://localhost:8080 にアクセスしても「MySQL のドライバーが見つからない」といったエラーになってしまう。 これに関しては config/app.php を以下のように SQLite 用に直せば良い:

'Datasources' => [
    'default' => [
        'className' => 'Cake\Database\Connection',
        'driver' => 'Cake\Database\Driver\Sqlite',  // Sqlite にする
        'persistent' => false,
        'host' => 'localhost',
        'username' => '',  // 空にする
        'password' => '',  // 空にする
        'database' => 'xxx.sqlite',  // 適当な SQLite ファイル名を書く
    ... (省略) ...
    'test' => [
        'className' => 'Cake\Database\Connection',
        'driver' => 'Cake\Database\Driver\Sqlite',  // こっちも Sqlite にしないと駄目
        'persistent' => false,
        'host' => 'localhost',
        'username' => '',  // 空にする
        'password' => '',  // 空にする
        'database' => 'test.sqlite',  // 適当な SQLite ファイル名を書く
    ... (省略) ...

これで http://localhost:8080 にアクセスしカラフルな Get the Ovens Ready ページが表示されれば開発環境構築完了である。