Technote

by sizuhiko

GAE gen1 で動いている PHP5.5 で作った個人開発サービスを gen2 PHP8.2 へ移行した1年記 〜 その 4

この記事は GAE gen1 で動いている PHP5.5 で作った個人開発サービスを gen2 PHP8.2 へ移行した1年記 〜 その 3 の続編となります。

PHP 8.2 で実行できるように修正していく

ひとまずロジック部分については 8.2 環境で PHPUnit でテストできるようになり、CI も動作するようになったので、アプリケーションを起動して動くのかを試していきたいと思います。

Memcache の残りを移行する

Memcache は Redis に移行したのですが、単体テスト外の部分にも少し残っていたので、こちらのコミット で対応しました。

移行方法は GAE gen1 で動いている PHP5.5 で作った個人開発サービスを gen2 PHP8.2 へ移行した1年記 〜 その 2 でも実施した内容なのですが、DI 部分と設定ファイルのデフォルト値、READMEの説明を修正しました。

GAE のアプリケーションバージョンを新しく設定する

GAE では通常のトラフィックを古いバージョンへ向けて、新しいバージョンもデプロイして別のURLから実行できるようにする機能があります。 dev_appserver.py での実行が困難になっているので、新しいバージョンを GAE にデプロイして動作するのかを検証していくことにしました。

今回はバージョン2として修正しました。

GAE で動かすにあたり php.ini も変更が必要だったので、拡張に redis を追加して修正しました

デプロイして動かしてみる

全然動きませんな (^^;;

まず問題を切り分けるために、まだビジネスロジックに分岐できていなかったデバイス値の取得処理を Google Cloud Storage から取得する処理をテスト可能なサービスとしてリファクタリングしました。

あとセッションの保存場所を設定する必要があったので、いったん php.inisession.save_path=Google\AppEngine\Api\Memcache\Memcache を追加しました。

依存のライブラリもいくつか PHP 8.2 環境だと動かないものがあったのでアップデート。

いったんそれらの修正コミットがこちら

で、いろいろやっていくうちに、そもそも slim3 だと PHP 8系で動かないな…という基本的なところに気づきました(さいしょから考えておけよという話)。

Slim3 から Slim4 に移行していく

幸いなことにこちらに関してはインターネットに移行記事がたくさんあり、とても参考になりました。

今回の移行に関するコミットがこちらです。

依存関係の更新

  • slim 3.1 を 4.12 に
  • monolog のアプデ
  • BASIC認証のライブラリ tuupola/slim-basic-auth のアプデ
  • セッションミドルウェア akrabat/rka-slim-session-middleware のアプデ
  • PSR7実行が外部依存になったので slim/psr7 の追加
  • DI が外部依存になったので php-di/php-di の追加
  • Google Cloud Storage がランタイム外になったので google/cloud-storage の追加

php-di への移行

もともとは Slim3 のアプリケーションコンテナを使っていたので、こんな感じでやっていたのを

// Instantiate the app
$settings = require __DIR__ . '/src/settings.php';
$app = new \Slim\App($settings);

以下のように変更しました。

use DI\Container;

$settings = require __DIR__ . '/src/settings.php';
$container = new Container();
$container->set('settings', $settings['settings']);

続いてDIコンテナの初期化処理化を

require __DIR__ . '/src/dependencies.php';

のように指定していたのを

use Slim\Factory\AppFactory;

$dependencies = require __DIR__ . '/src/dependencies.php';
$container = $dependencies($container);

AppFactory::setContainer($container);
$app = AppFactory::create();

このように変更しました。

  • コンテナを初期化
  • 環境設定の注入やコンテナの初期化を実行
  • AppFactory でアプリケーション初期化

のような実装に変わります。

Slim4 っぽい書き方に変更

もともとは

// Register middleware
require __DIR__ . '/src/middleware.php';

// Register routes
require __DIR__ . '/src/routes.php';

こんな感じでミドルウェアとルーティングの設定を書いていたのですが、

// Register middleware
$middleware = require __DIR__ . '/src/middleware.php';
$middleware($app);

// Register routes
$routes = require __DIR__ . '/src/routes.php';
$routes($app);

Slim4アプリを引数で指定して設定するように変更しました。 $app をグローバル参照から引数参照するコールバック関数に変わったぐらいです。

Basic認証の修正

登録済みのデバイスからデータを受信するAPIでは、Basic認証ミドルウェアを設定していて、 callback という処理が呼び出されることになっていました。

//
// For Registered Devices
//
$app->post('/api/devices/{id}/values', '\ToiletEvolution\Controllers\DeviceValuesController:add')
    ->add(new \Slim\Middleware\HttpBasicAuthentication([
        "authenticator" => new ToiletEvolution\Middlewares\HttpBasicAuthentication\DeviceAuthenticator($app->getContainer()->get('DeviceStore')),
        "callback" => function($request, $response, $arguments) {
          $route = $request->getAttribute('route');
          $id = $route->getArgument('id');
          return $id === $arguments['user'];
        },
        "secure" => false
      ]));

Slim4でのBasic認証では before が呼び出される様に変わっています。

  //
  // For Registered Devices
  //
  $app->post('/api/devices/{id}/values', '\ToiletEvolution\Controllers\DeviceValuesController:add')
      ->add(new \Tuupola\Middleware\HttpBasicAuthentication([
          "authenticator" => new ToiletEvolution\Middlewares\HttpBasicAuthentication\DeviceAuthenticator($app->getContainer()->get('DeviceStore')),
          "before" => function($request, $response, $arguments) {
            $id = $arguments('id');
            return $id === $arguments['user'];
          },
          "secure" => false
        ]));

コントローラーの修正

APIでJSONを返却するときの処理が標準ではなくなったので、JSON文字列を出力する JsonRenderer のようなクラスを作ります。

namespace ToiletEvolution\Renderer;

use Psr\Http\Message\ResponseInterface;

final class JsonRenderer
{
    /**
     * Write JSON to the response body.
     *
     * This method prepares the response object to return an HTTP JSON
     * response to the client.
     *
     * @param ResponseInterface $response The response
     * @param mixed $data The data
     *
     * @return ResponseInterface The response
     */
    public function json(
        ResponseInterface $response,
        mixed $data = null,
    ): ResponseInterface {
        $response = $response->withHeader('Content-Type', 'application/json');

        $response->getBody()->write(
            (string)json_encode(
                $data,
                JSON_UNESCAPED_SLASHES | JSON_PARTIAL_OUTPUT_ON_ERROR
            )
        );

        return $response;
    }
}

で、それをコントローラのコンストラクタで初期化して、APIメソッドで呼び出します。

class DevicesController
{
  protected ContainerInterface $ci;
  private JsonRenderer $renderer;

  public function __construct(ContainerInterface $ci)
  {
    $this->ci = $ci;
    $this->renderer = new JsonRenderer();
  }

  public function index(ServerRequestInterface $request, ResponseInterface $response, array $args)
  {
    $deviceModel = $this->ci->get(Device::class);

    $data = array_map(function($device) {
      return $device->toArrayWithoutSecret();
    }, $deviceModel->all());

    return $this->renderer->json($response, $data)->withStatus(empty($data)?204:200);
  }

Slim3 では以下のようなコードで $response->withJson でよかったところが変更になっています。

public function index($request, $response, $args)
{
    $deviceModel = $this->ci->get(Device::class);

    $data = array_map(function($device) {
      return $device->toArrayWithoutSecret();
    }, $deviceModel->all());

    return $response->withJson($data, empty($data)?204:200);
}

ミドルウェアの修正

PSR の書き方が変わっているので、それに応じて修正しています。

class RequireLoginMiddleware
{
    public function __invoke($request, $response, $next)
  {
    if(empty($this->session->get('current_user')))
    {
      $response = $response
        ->withStatus(302)
        ->withHeader('Location', $this->redirectIfNotLogin);
    }
    else
    {
      $response = $next($request, $response);
    }

    return $response;
  }    

こんな感じだったのが、以下のように変更になっています。

class RequireLoginMiddleware
{
  public function __invoke(Request $request, RequestHandler $handler): ResponseInterface
  {
    $response = new Response();
    if(empty($this->session->get('current_user')))
    {
      $response = $response
        ->withStatus(302)
        ->withHeader('Location', $this->redirectIfNotLogin);
    }
    else
    {
      $response = $handler->handle($request);
    }

    return $response;
  }

引数が変更になっていて、 $response がなくなって、次のミドルウェアへ処理を引き継ぐのが $next から $handler に変わっています。

$response はミドルウェアごとに作成して設定していきます。 次のミドルウェアに引き継ぐときは、 $handler->handle で呼び出してレスポンスを取得します。

さいごに

Slim3 から Slim4 への移行手順がインターネットに多くあって助かりました。 また Slim 関連の OSS パッケージも 3 から 4 への移行をドキュメントにしてくれていたので、比較的スムーズに移行できたと思います。

ここではその一部を紹介する感じになりましたが、同じような対応をする人に役立つ内容になっていれば嬉しいです。

GAE gen1 で動いている PHP5.5 で作った個人開発サービスを gen2 PHP8.2 へ移行した1年記 〜 その 3

この記事は GAE gen1 で動いている PHP5.5 で作った個人開発サービスを gen2 PHP8.2 へ移行した1年記 〜 その 2 の続編となります。

前回は PHPUnit を最新化して通過するところまで実施したので、今回は GitHub Actions で CI できるようにしていきます。

GitHub Actions と Google Cloud を連携する

Authenticate to Google Cloud from GitHub Actions というリポジトリがあって、Actions から gcloud 関係の CLI ツールを動かす前に認証を通過させる方法が書いてあります。

Actionsが通過するようになったPRがこちら

GitHub Actions からのキーなしの認証の有効化 という公式ドキュメントに加え、先ほども紹介したリポジトリのREADMEを併せて読むと簡単に環境構築できるようになります。

環境構築ができたら Actions の yaml ファイルを定義していくだけです。

  • ソースコードをチェックアウト
  • google-github-actions/auth で認証
  • google-github-actions/setup-gcloud で gcloud コマンドをセットアップ
  • PHP8.2環境のセットアップ
  • composer アクションで依存関係を解決
  • gcloud コマンド実行
  • PHPUnit 実行

のような手順にしました。

さいごに

google-github-actions/auth の認証方法が変更になっているので最新に追従しないといけないので、おいおい対応していきます。

GAE gen1 で動いている PHP5.5 で作った個人開発サービスを gen2 PHP8.2 へ移行した1年記 〜 その 2

この記事は GAE gen1 で動いている PHP5.5 で作った個人開発サービスを gen2 PHP8.2 へ移行した1年記 〜 その 1 の続編となります。

単体テストを通過するようにアップグレード

やることは多そうだなというのは想像していたのですが、いざ取り掛かろうとしたとき頼りになるのは単体テストだな、ということでPHPUnitとPHPのバージョンを上げて、テストが通過するように修正していきます。

テストが通過するようになったコミットがこちら

composer の依存バージョンを変更

  • PHPのバージョンを8.2以上へ
  • PHPUnitのバージョンを10.1.3(当時最新)へ

その関連で合わせてあげないといけない依存もバージョンup。

PHPUnit の実行方法を変更

以前は GAE の内部コンポーネントに依存していたので、PHPUnitを実行するのも dev_appserver.py を起動してからそのインスタンス内の PHPUnit 実行スクリプトを経由して実行していたのですが、GAE gen2 ではPHPスクリプトの起動自体が php -S に変わったりしたこともあり、PHPUnitもComposer スクリプトから直接実行するように変更しました。

以前は dev_appserver 経由で起動されていたデータストアとキャッシュについては、docker と gcloud emulator で起動してから PHPUnit を実行するように変更しました。

実行手順をREADMEにも記述。

ここで、レガシーランタイム脱却のため Memcache を Redis に変更する対応も入っています。 当初は Memcache を変更せずに PHPUnit が通過してから移行する予定だったのですが、 GeckoPackages\MemcacheMock が終了していて、最新のPHP/PHPUnitで動作しないことがわかったのでこのタイミングで一緒に移行を決断しました。

簡単なアーキテクチャと既存テストコード

Toilet Evolution は PHP Slim Framework に、Google Datastore を使用(DatastoreモードのFirestoreを使用)しています。 REST API を通じてデバイスからトイレの利用状況をデータストアやキャッシュに格納したり、Webアプリから利用状況を閲覧できるAPI が用意されています。

テストコードは、データストアとキャッシュのI/O、PSRのミドルウェア部分にあって、主なロジックはこのあたりに集中しています。

テストコードの記述を修正

PHPUnit のテストケース(クラス)の宣言を修正

// PHP5.5 / PHPUnit 4.8
class DeviceTest extends \PHPUnit_Framework_TestCase {}

// PHP8.2 / PHPUnit 10.1.3
use PHPUnit\Framework\TestCase;

class DeviceTest extends TestCase {}

setUp を before に変更

// PHP5.5 / PHPUnit 4.8
public function setUp()

// PHP8.2 / PHPUnit 10.1.3
/**
 * @before
 */
public function before()

assert を $this 経由で呼び出すように変更

// PHP5.5 / PHPUnit 4.8
assertEquals('username', $results->name);

// PHP8.2 / PHPUnit 10.1.3
$this->assertEquals('username', $results->name);

Memcache から Redis への記述を修正

インスタンスはDIしているので、変数名を memcache から redis に変更した。 メソッドは get / set で同じになるけど、データがオブジェクト形式から文字列に変わるので以下のような対応となった

// GAE gen1
$this->memcache->set("device:id:{$device->getKeyId()}", $device);

// GAE gen2
$this->redis->set("device:id:{$device->getKeyId()}", serialize($device));

データを set するときは serialize して、 get するときは unserialize するように変更している。

このぐらいで PHPUnit がうごくようになった

Warining は出てるんですが、PHPUnit がエラーで落ちることはなくなりました。 これはバージョンアップ意外と楽チンなのでは?!と勘違いしたのは言うまでもなく、この連載もまだ続きます。

ここで勘違いしたので、心も折れず続けられたというのはあるかもしれないと振り返り思うのでした。

GAE gen1 で動いている PHP5.5 で作った個人開発サービスを gen2 PHP8.2 へ移行した1年記 〜 その 1

2023年2月3日にGoogle Cloudからメールが届きました。

ここで作っていた個人開発のサービスとは Toilet Evolution です。一時期に流行ったトイレ利用状況可視化のIoTサービスです。

PHP + Web Components の SPA 構成でできていて、Google App Engine(以下GAE)の第一世代で動いています。 当時ほぼ無料で動かせるPaaSというのはあまりなくて、GAEばかりを選択していたと記憶しています。 その後 Heroku とか出ましたけど、実質それなりの利用で月100円もかからずに運用しつづけられている状況なのでありがたいです。

GAEのランタイム サポート スケジュール によると PHP5.5 は 2024年1月30日にサポートの終了で、非推奨が2026年1月31日ということ。廃止は書かれていません。

と同時にまだ使っている人もいるので、簡単にメンテ大変なんでクローズしますというのも違いますね。ユーザー大事。ありがたや。

そこで、サポート終了状態でずっと動かすのもなと思い、最新版のPHPへの移行を検討し始めました。

一番最後のメンテナンス

このWebサービスは長いことやっているのですが、直近で記事にしたのは Toilet EvolutionのフロントエンドをPolymer3対応する(4) で、2019年4月。あのあとコロナがあっての5年ぶりのメンテになります。

移行前の composer.json

以下のような依存バージョンでした。

{
    "php": ">=5.5.0",
    "slim/slim": "^3.1",
    "monolog/monolog": "^1.17",
    "tomwalder/php-gds": "^2.1",
    "tuupola/slim-basic-auth": "^2.0",
    "nesbot/carbon": "^1.21",
    "league/oauth2-client": "^1.4",
    "league/oauth2-google": "^1.0",
    "akrabat/rka-slim-session-middleware": "2.0.0-RC1",
    "sizuhiko/slim3-csrf-utilities": "dev-master",
    "justinrainbow/json-schema": "^2.0",
    "phpunit/phpunit": "^4.8",
    "helmich/phpunit-psr7-assert": "^1.1",
    "gecko-packages/gecko-memcache-mock": "^2.1",
    "google/appengine-php-sdk": "^1.9"    
}

歴史を感じますね。

GAE gen1 から gen2 で何が変わるのか

GAEでの移行ガイドが出ていて、PHPだと PHP 5.5 と PHP 7 / 8 の違い というドキュメントがあります。

PHP公式ドキュメントの移行ガイドへのリンクと、GAE特有のアップグレードガイドが書かれています。 タイトルだけ拾うと、以下のような移行が必要になるということでした。

  • app.yaml ファイルを移行する
  • ランタイム制限の緩和
  • App Engine の PHP SDK から移行する(レガシーバンドルサービスAPI利用方法)
  • ローカルでの実行方法

レガシーバンドルサービスとは PHP 7 / 8 用の以前のバンドル サービスにアクセスする で書かれているサービスで、今回のサービスで言うと Memcache とセッションを利用していました。

で、これらは使えるので、使い続けて良いのか?という選択を迫られます。 もう少し関連するドキュメントを読んでみましょう。 以前のバンドル サービスの概要 によると

レガシー ランタイムの一部はオープンソース コミュニティで管理されなくなったため、App Engine デベロッパーは、新しいランタイムに移行すべきかどうか難しい判断に迫られる可能性があります。こうした移行には時間と労力がかかりますが、レガシー ランタイムの使用を継続すれば、アプリの維持コストが増大する結果になります。 こうした課題を踏まえ、Google Cloud では新しいランタイムへの移行パスを段階的に提供していく予定です。Google Cloud では、ランタイム移行の複雑さを軽減するために、Python 3、Java 11、Go 1.12 以降、PHP 7 / 8 などの第 2 世代ランタイムで App Engine の従来のバンドル サービスとその関連 API をサポートしています。

移行大変なんで段階的な移行パスを用意するよってことですね。 Memcache はこの移行バスに入ったようでそのままでも利用できたんですが、あとでまた移行するのが面倒そうなんで、このタイミングでやっておきたいと考えました。

レガシーランタイムからの移行に関してのドキュメントは レガシー バンドル サービスからの移行 にあって、参考になります。

実行目標

  • PHPのバージョンを最新化する
  • GAEのレガシーランタイムからの脱却

というのを掲げてみました。

けど、PHP自体あんまり触ってない(たぶん7を少しやったぐらいで、実質5系の知識で止まっている)ため、長期戦は予想していました。当時はまだ1年ぐらいあるから何とかなるやろ、ぐらいに思っていた自分がいました。

そして、その後の1年ぐらいで移行を完了させて、現在は GAE gen2 の PHP8.2 で稼働しています。👏👏

このあと数回にわたって、どうやって移行してきたかをコミットログと共にふりかえっていきたいと思います。 少しでも PHP のバージョンアップや GAE gen2 へ移行する人の助けになればな、と思っています。

PHPカンファレンス関西2024に参加して、オフラインカンファレンスの盛り上がりを体験してきた

大阪自体はそれほど久しぶりではないけど、東京以外のカンファレンスにリアル参加するのは久しぶりだった。 ブログを振り返ると、PHPカンファレンス沖縄2019で、 標準インターフェースを使ったアプリケーション開発について発表してきましたがコロナ前最後に参加した地域PHP conであったようだ。 大阪へは藤井風のパナスタライブ以来なので、1年半ぶりぐらい。

このところのホテル事情

昨年秋のシルバーウィークに九州まで車でスカイラインを巡る旅をしてきて、ホテルの混雑状況とか把握していたつもりでしたが、今回はやばかった。 ちょうど3連休開催ということで、ホテルの値段がやばい。大阪ではいつも宿泊する本町のホテルが早々に売り切れててどうしよう?となりましたが、 運よくリニューアルしたばかりの東横inn淀屋橋駅南が空いてて、普段の東横価格だったので安心できました。

前夜祭への参加

さらに前日入りをすることにしていたのですが、午前中に東京で用事があったので 【非公式!前夜祭】 PHPカンファレンス関西2024 へも新幹線に乗るタイミングで間に合いそうだったので参加表明するという慌ただしさ。

前夜祭で他の人の発表を聞いていて、自分も何か話したいなーという欲が出てきたので、その場でスライドをさくさくと作ってWebアクセシビリティの話をしました。 いろいろなところで4月から義務化?!とか言われてますが、実際のところどうなのよ?みたいなところを。 最新関係する仕事もしていてかなり調べたので、気になっている人もいるのでは?と思ったのです。

かつてはカンファレンスを開催していた大阪産業創造館という場所も懐かしかった。

前夜祭を楽しんだ後は、最近大阪に来たら必ずいく Bar で飲んでちゃんと終電ではホテルに戻りました。

カンファレンス当日

今回はあまり普段登壇で見かけない人の発表を聞く、久しぶりに会うコミュニティメンバーと会話する、という2つの目的で参加していました。 どちらも当初の見込みは達成できて、たくさんの刺激をもらいました。 午後は主にアンカンファレンスの会場にいて、枠も空いていたので前夜祭で話した内容をまたやってみたりと、聞くだけでなく積極的にイベントにも参加しました。 ちゃんとスポンサークイズにも応募したよ!(半分ぐらいは聴けてなかったので勘で回答しちゃいましたがw)

懇親会 -> 2次会 -> ホテルそばで1人3次会 をやって終了。 翌日の月曜日も3連休ということで休みでしたが、東京で推しのライブがあるのでお昼には新幹線に乗り込みました。 いつものネギ焼きは新幹線のフードコードで

あ、その前にもちろん聖地巡礼(阪急梅田のタイガースショップ)もして、グッズも買いました。

久しぶりの地域カンファレンスに参加してのまとめ

昨年のPHPカンファレンス福岡に始まり、地域カンファレンスの開催も戻ってきて、今回の大阪に久しぶりに参加しました。 大阪のPHPcon自体は PHPカンファレンス関西2018に参加してGAEに継続的デプロイする方法について発表してきました のとき以来で、今回はこのときの登壇者Tシャツを来て参加しました。

つまり関西では 6年ぶりということですね。 オフライン開催のPHP系カンファレンスに関しても昨年の福岡などより前となると、おそらく PHPerKaigi 2020で「E2Eテストに向き合う」ついて発表してきました このときの PHPerKaigi がコロナ前最後であったという記憶があります。

それでも4年。 つまり6年以上前からコミュニティに参加していた関西圏の人以外はほぼ初参加となるわけです。

今年は月刊PHPカンファレンスなんて言葉が出るぐらいですが、それも昨年の福岡に始まった地域カンファレンスの楽しさから触発されたもので、しばらく行われてこなかった反動だったり、この4から6年の間に新たにこの業界で働き始めた人たちの新しい交流の場なんだなというのを実感しました。

今回のPHPCon関西ではいくつもの素晴らしい取り組みがありました。

  • 公式 note で発信
  • PHPerシール
  • お誘いチケット
  • おすすめトーク診断

まず公式 note は X だけで発信していると流れてしまいそうな情報に簡単にアクセスできるのもあり、良いなーと思いました。 困ったら note 見る、みたいな動線がある。もちろん公式HPに載せるのもありなんで、そこは運用の手間がどちらが少ないかという判断もあるでしょう。

続いて PHPerシール。こちらは2020のPHPerKaigiで行われていたトレーディングカードにインスパイアされたものだと思いますが、積極的に交流できるようにするアイテムとしてとても良いと思いました。スピーカーにはレアカードがあったりしたのも良かったと思います。

お誘いチケットは有料イベントで参加を迷っている人を誘いやすい仕組みとして良いなと思いました。 PHPerシールとか当日の交流の仕組みがあっても、まず参加してくれないと届きませんね。

おすすめトーク診断も、誘ってもらったり紹介してもらったので参加するけど、カンファレンスの歩き方ってわからんな?という人も向けて楽しみながら参加できる仕掛けだったと思います。

つまりPHP関西自体に初参加、ましてやコロナで勉強会を含めオフラインのイベントそのものに初参加ということが6年というブランクから容易に想像できる環境での地域コミュニティの盛り上げ方に優れた戦略が練られていたことがわかります。

  1. お誘いチケットで誘って参加してもらい
  2. おすすめトーク診断で当日迷わないようにして
  3. PHPer シールで当日は交流してもらう
  4. なんなら X のアカウント持ってなかったけど、シール経由で作ってもらって今後の交流やPHP関西からの発信を受け取ってもらえるようにする

委員長の閉会の挨拶でもこれらを話されていましたが、関西でのPHPコミュニティを盛り上げたいという熱い想いを聞いて、また来年開催されたら参加したいな、発表もしてみたいなと思いました。

久しぶりということで運営の経験値とかもリセットされた中での開催だったと思うけど、そういうのを感じさせない良いカンファレンスでした。 参加のみなさま、久しぶりに再会したみなさま。そして何より運営のみなさまありがとうございました。