🐈

PHPフレームワーク:BEAR.Sunday でTwigとPHPセッション認証を使ったシンプルなPHPアプリを書く準備

2022/09/19に公開

前置き

  • 掲題の実装を行ったコードのコミット履歴を下記GitHubリポジトリで公開しています。

https://github.com/clap-and-whistle/learn-bearsunday

  • 上記を動作させてみたい場合、ログインに必要な情報は このクラス に直書きしてあります。

「さっさと実際のコードの解説をくれ」という向きの方は、当記事目次の「本題」からお読み頂いていいと思います。

掲題の件のモチベーション

筆者は、次のような動機があって当記事を書いています。

  • BEAR.Sundayは「API中心のRESTfulアプリケーション」として使用されることを主眼に置いているのだけど、この記事の筆者自身はあまりRESTを意識したことがなくフワッとしか理解していない。そこで、BEAR.Sundayの作法に触れるうちにRESTfulアプリケーションならではの考え方の勘所を得られるかも、という期待がある。
  • BEAR.Sunday は skeletonプロジェクトで PHPStan / PHP_CodeSniffer / Psalm といった静的解析ツールを標準装備しているので、これらに怒られるような手癖のコードは最初っから潰しながら開発を進めていけて良さそう。
  • 最近のWEB系MVCフレームワーク普及以前に出回っていた「PerlやPHPによるベタなアプリ」を、現代のフレームワークに載せ直してみたい。
  • その際、BEAR.SundayでPageリソースにHTML出力(よくあるWEB系MVCフレームワークで言うところのView)を担わせて、Appリソースを「RESTfulを強く意識した実装」のように作り進めると、HTML側をReactなどのSPAに置き換えたいような時に分離しやすいのではないか?という妄想の実験をしたい。

BEAR.Sundayを使って最近よくあるWEB系MVCフレームワークとさほど変わらないやり方でアプリを作ることはどうやらダサいのか、ググってもそんな情報を見つけられない。(筆者の検索力不足?)
よくある「PHPセッションを使った昔ながらの気軽な画面遷移」のサンプルも見つけられない。(筆者の検索力不足?)
このダサいであろう部分をPageリソースへ閉じ込めつつ、AppリソースでRESTfulに努めているサンプルがあれば、RESTに理解の薄い筆者のようなタイプのPHPerも体感を伴ってRESTと仲良くなっていけるのでは、という予感がしたのでとりあえず自分でやって公開してみるか、と思った次第。
(「こんなダサいこと、真似するんじゃないよ!」という指摘や風聞は歓迎します。)

記事の概略

以後の 「本題」セクション では、
サブセクションがそれぞれ次のような内容になっています。

サブセクション1.~ 3.まで

当記事の主題である「PHPセッションによる認証機能の準備」部分について、
GitHubリポジトリのコミット履歴を追いながら簡易に解説を試みています。

サブセクション4

前のサブセクションまでで準備した「認証を扱える環境」の上にアプリケーションコードを足して動かす例を示します。
今のところ、BEAR.Sunday公式のサンプルの元になっていると思われる Polidogさんのブログ記事「BEAR Sundayを学習してみた」 の内容(認証機能を持っていないアプリケーション)を上に「被せて認証制御のもとで動かした例」のみ、提示しています。

サブセクション5

当記事で扱っているGitリポジトリは、今後も改修が入ることがあります。
当記事公開後に改修があった場合、関連するコミットについての補足をサブセクション5へ追記して行きます。

アプリケーションの動作を確認するための作業環境の準備

この記事に書いてあることを手元で試してみる場合、まずはご使用のPC上でbash等のUNIX系シェルを実行できる環境を用意してphp8.1系が動作していることを確認してください。

確認例
$ php -v
PHP 8.1.9 (cli) (built: Aug 23 2022 14:17:26) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.1.9, Copyright (c) Zend Technologies
    with Zend OPcache v8.1.9, Copyright (c), by Zend Technologies

php8.1系を直接動かせる環境ではない場合、phpenvなりdockerなりを適宜検討して導入してください。
参考までに、dockerを使った例を示します。

Docker環境準備の例

docker環境で動作確認するなら、任意の作業ディレクトリを作って php環境用のDockerfile置き場とBEAR.Sundayのソースコード置き場を作ってそれぞれ置くといいです。
例えばこんな感じ。

docker環境構築コマンド手順例
mkdir learn    # 任意の作業ディレクトリを作って
cd learn
mkdir php       # php環境用のDockerfile置き場
git clone https://github.com/clap-and-whistle/learn-bearsunday.git    # learn-bearsunday というBEAR.Sundayのソースコード置き場(= gitのローカルリポジトリそのもの)ができる
vi php/Dockerfile        # Dockerfile を作成 (サンプルは後述)
vi docker-compose.yml    # docker-compose up -d で起動できるようにする (サンプルは後述)

Dockerfile サンプル

Dockerfile サンプル
FROM php:8.1.9-apache-buster
SHELL ["/bin/bash", "-oeux", "pipefail", "-c"]

RUN apt-get update && \
  apt-get -y install git unzip libzip-dev libicu-dev zlib1g-dev sqlite3 libsqlite3-dev && \
  apt-get clean && \
  rm -rf /var/lib/apt/lists/* && \
  docker-php-ext-install pdo_sqlite && \
  docker-php-ext-enable pdo_sqlite && \
  docker-php-ext-install opcache && \
  pecl install apcu && \
  echo "extension=apcu.so" > $PHP_INI_DIR/conf.d/apcu.ini

COPY --from=composer /usr/bin/composer /usr/bin/composer

WORKDIR /work

docker-compose.yml サンプル

docker-compose.yml サンプル
version: "3.8"
services:
  app:
    build: ./php
    volumes:
      - ./learn-bearsunday:/work
    ports:
      - 8080:8080

Dockerコンテナ上で実行

上記の準備が出来たら、次の例のような感じで実行できると思います。

実行コマンド手順例
docker-compose up -d
docker exec -it learn_app_1 bash
# 以下はdockerコンテナ内で実行するやつ
composer install
mkdir var/db
sqlite3 var/db/todo.sqlite3
## 行頭が「sqlite>」となってるのはsqliteのプロンプト
sqlite> create table todo(id integer primary key, title, status, created, updated);
sqlite> .exit
composer build         # BEAR.Sundayプロジェクトをbuild
composer page get /    # アプリを実行

最後の「アプリを実行」で出力されるテキスト内容がHTMLによるログインFormの体を成していれば、BEAR.Sundayアプリケーションが動作できています。
なお、ログインに必要な情報はこのクラスに直書きしてあります。
https://github.com/clap-and-whistle/learn-bearsunday/blob/01a4135e1f77856da220d135ad0a00bcf5b743cc/src/Infrastructure/Authentication/IdentityRepository.php

本題

以降では、この 「セッション認証サンプルアプリケーション」 を実装する過程を、gitリポジトリのコミットツリーひとつひとつを追いながら簡単に解説していきます。

最初に、当記事における記述の前提事項を書いておきますのでご承知ください。

【前提事項】

  • 当記事で扱う learn-bearsundayというgitリポジトリ では、 Cw\LearnBear という namespace をベースにアプリケーションを実装しています。
  • コミット履歴をひとつずつ拾い上げながら簡単な解説を加えていきます。(つまり、bear/skeletonから新規プロジェクトを作って、下記1-3.以降で解説していることを実施していけば、learn-bearsundayリポジトリで積んでいっているコミットを後追い体験できるはず、ということです)
  • 「1-1. ~~」とか「2-3. ~~」といった始まり方をする小見出しは、すべて 短縮形コミットID: コミットタイトル という書式にしています。コミットID部分はGitHub上のコミット差分の表示画面へリンクしています。

1. まずは、公式チュートリアルのHTMLセクションを実践する

1-1. b6f6638: initial commit.

この時点では、このリポジトリに次のことだけ規定していて、 まだBEAR.Sundayの使い方と直接の関係はありません
(このGitHubリポジトリ作った当初は筆者の手元のPHPが8.0.17だった)

  • PHP 8.0.17 環境を期待している。
  • phpunit を実行できる開発環境だけあれば良い。
  • このアプリケーションでは次の2つの namespace を扱える
    • Cw\LearnBear
    • Cw\Package

1-2. f8d4638: php8.1 init.

規定する内容を見直しただけのコミットで、 まだBEAR.Sundayの使い方と直接の関係はありません

  • phpのバージョン決め打ちをやめて、 composer.json で 8.1以上 を指定するのみとした。
  • 扱う namespace は、ひとまず Cw\\LearnBear だけにすることにした。

1-3. 7695f87: $ composer create-project bear/skeleton Cw.LearnBear

【コミットメッセージ本文】

  • 上記コマンドを別ディレクトリで実行して、生成された中身をこのリポジトリへ被せている。

このコミットから、BEAR.Sunday を使い始めていて、 BEAR.Sundayのプロジェクトを始めている だけです。

このコミットメッセージで書いてある事の再現は、php8.1を実行できる環境上の任意の空ディレクトリにて例えば次のようにすることで出来ると思いますので参考までに。

「php8.1 init.」の環境へ bear/skeleton を被せる例
# 1. まずは「php8.1 init.」の環境を再現する。
mkdir learn    # 空のディレクトリを作る
cd learn
git init
git remote add origin https://github.com/clap-and-whistle/learn-bearsunday.git
git fetch origin f8d4638c88f30319722b0a316eab7076b51d4b59    # コミットタイトル「php8.1 init.」の内容を fetch する
git switch -c for_experience FETCH_HEAD    # 先程の FETCH_HEAD へ for_experienceというブランチを作成してチェックアウトする。※記載のブランチ名は「体験用」ぐらいの意味で、実際はなんでも良い
composer install    # phpunit実行用のパッケージだけ取得
php vendor/bin/phpunit --version
### ここまでで「php8.1 init.」の環境が再現できたことになる。
rm -rf vendor    # いったん片付け(後で被せるため)

# 2. 続けて、別ディレクトリで bear/skeleton を新規composerプロジェクトとして作成する
cd ../
composer create-project bear/skeleton Cw.LearnBear
##### ...
##### 出力内容を省略して、下記のみ抜粋 #####
> BEAR\Skeleton\Composer::install

What is the vendor name ?

(MyVendor):Cw    # ←ここはプロンプトなので入力する

What is the project name ?

(MyProject):LearnBear    # ←ここはプロンプトなので入力する
composer.json for cw/learn-bear is created.
##### 以降の出力内容省略 #####
##### ...

# 3. 最後に、生成したskeletonプロジェクトを先程fetchした learn/ 配下へ被せる
cd Cw.LearnBear
mv * .[^\.]* ../learn/
cd ..
rmdir Cw.LearnBear    # 後片付け

### このような手順により被せた内容を
### コミットしたのが 7695f87 です。

# 4. 確認
cd learn
git status -sb    # 被せた分が差分として表示されるはず
php bin/page.php get /    # 動作することを確認
composer build            # buildが通ることを確認

1-4. 1b1e89f: 「良くあるWebフレームワーク」として動作するのに使うライブラリを一通りインストール

【コミットメッセージ本文】

やったこととしては、次の通り。

実施したコマンドの例
composer require ray/aura-session-module
composer require ray/aura-web-module
composer require ray/web-form-module
composer require madapaja/twig-module
cp -r vendor/madapaja/twig-module/var/templates var
composer build

1-5. f1c212d: Twigテンプレートのキャッシュもクリアできるようにcompserスクリプトを調整

前コミットで twig-module を install しましたが、htmlテンプレートを弄った時にはtwigのキャッシュをクリアしてあげないといけません。
このコミットでは、気軽にクリアできるよう composer twig-clean で呼べるようにしています。

composer.json
@@ -46,10 +46,14 @@
         "cs": "./vendor/bin/phpcs",
         "cs-fix": "./vendor/bin/phpcbf src tests",
         "metrics": "./vendor/bin/phpmetrics --report-html=build/metrics --exclude=Exception src",
+        "twig-clean": [
+            "rm -rf ./var/tmp/*/twig/*"
+        ],
         "clean": [
             "./vendor/bin/phpstan clear-result-cache",
             "./vendor/bin/psalm --clear-cache",
-            "rm -rf ./var/tmp/*.php"
+            "rm -rf ./var/tmp/*.php",
+            "rm -rf ./var/tmp/*/*"
         ],
         "sa": [
             "./vendor/bin/psalm --show-info=true",

1-6. 21c8ea4: 公式チュートリアルにある HTMLセクションを実践

まず、公式のチュートリアルにある Weekday のアプリケーションリソースファイルを作成 して、appコンテキストでアクセスできることを確認します。

### それぞれviで編集しているファイルの内容はgitのコミット差分表示を御覧ください
vi src/Resource/App/Weekday.php
php bin/app.php get '/weekday?year=2001&month=1&day=1vi tests/Resource/App/WeekdayTest.php
composer test    # $ ./vendor/bin/phpunit でも良い
composer build

次に、公式チュートリアルにある HTMLセクション を実践します。

### それぞれviで編集しているファイルの内容はgitのコミット差分表示を御覧ください
vi src/Resource/Page/Index.php
vi tests/Resource/Page/IndexTest.php   # Page/Index.php の更新に合致するようにクエリストリングを追加
composer test
php bin/page.php get '/?year=2000&month=1&day=1'
vi src/Module/HtmlModule.php
cp -r vendor/madapaja/twig-module/var/templates var
vi bin/page.php    # コンテキストを 'cli-html-app' あるいは 'html-app' となる ように変更
vi var/templates/Page/Index.html.twig    #適宜編集
php bin/page.php get '/?year=1991&month=8&day=1'
vi public/index.php  #'html-app' あるいは 'prod-html-app' として動作するように変更

# ワークフローテストを修正
vi tests/Hypermedia/WorkflowTest.php    # Page/Index.php の更新に合致するようにクエリストリングを追加
vi tests/Http/index.php
vi tests/Http/WorkflowTest.php

composer build
# 動作確認
### 「$ php -S 127.0.0.1:8080 public/page.php」でビルトインサーバを起動して
### ブラウザで http://127.0.0.1:8080/?year=1991&month=8&day=1

1-7. cb40aaa: 簡易の画面遷移を用意しWorkflowTest

このコミットでやっているのは、「画面を1つ追加してリンクで繋ぐ」ことだけです。

まず、テスト時のDOM解析のために ext-dom をインストールします。

composer requrire ext-dom

※以下でそれぞれ編集しているファイルの内容は、gitのコミット差分表示等を御覧ください

次に、Resource\Pageネームスペースへサンプルの画面遷移を用意します。

  • src/Resource/Page/Index.php を編集
  • src/Resource/Page/Next.php を新規作成

次に、上記それぞれのPageリソースに対応するtwigテンプレートを用意します。

  • var/templates/Page/Next.html.twig を編集
  • var/templates/Page/Next.html.twig を新規作成

最後に、各種編集したファイルのテストや画面遷移(Workflow)のテストを書きます。

  • tests/Http/WorkflowTest.php
  • tests/Hypermedia/WorkflowTest.php
  • tests/Resource/Page/IndexTest.php
  • tests/Resource/Page/NextTest.php

動作確認

composer build
# コケる場合のトラブルシュートは、
# @clean や @cs や @sa や @test を状況に応じて使い分けるべし

2. 次に、ベタ書きでいいから認証処理を追加する

ひとまず目指す形として、次の画面遷移(正常系)を想定します。

  1. /index へのアクセスでログインフォームを表示
  2. /login へ「ログインフォームの入力値」をPOSTする
  3. POST内容が認証処理を通ればセッションを開始して、/next へリダイレクト
  4. /next 画面では、「認証済みかどうか」をセッションに問い合わせてOKであれば表示
  5. /logout へのアクセスで認証済みセッションをクリア

このあとの最初の3つのコミットでは「認証を扱える実装をうまくテストするための準備」を行い、
続く以降のコミットで上述の画面遷移を実装していきます。

2-1. 15c3205: テストやデバッグの環境準備(まずはLoggerを用意)

デバッグをしやすくするために入れたコミット。
ここでやっていることは、公式チュートリアルの DI セクションに書いてあることほぼそのまま。

2-2. 3093ec7: テストやデバッグの環境準備(PHPセッションの扱いをInterface化)

ホントは、ベタ書きで認証処理を書きながら「Sessionハンドラクラスの形(どんなメソッドを持ったインターフェースなのか)」が見えていったのだけど、この次に「DIで一時的な束縛差し替え」の準備コミットを持って来たかったので、ここで先に「PHPセッションの扱いをInterface化」のコミットを入れておいた。

2-3. 2f448ed: テストやデバッグの環境準備(DIで一時的な束縛差し替えを繰り返し可能にするための調整)

このコミットはけっこうイレギュラーな対処を行っているので、事情について踏み込んだ補記をしておきたい。

当セクションの当初記載内容を確認するには、このトグルを開いてください

何が起きたか

このコミットより後の方での認証処理実装において、phpunitによるテストの際に複数のリソースオブジェクトへSessionハンドラクラスのInterfaceをスタブ化して注入していたところ、異なる設定で当該スタブインスタンスを繰り返し生成して差し替えようとしてもうまくできず、テストが完了できない問題に遭遇しました。

原因と思われるところ

BEAR.Sundayで BEAR\Package\Injector\PackageInjector というクラスが「依存オブジェクトの出し入れ」を担っているようなのですが、これはシングルトンパターンで設計されています。
先述の「問題に遭遇」のケースでは、この PackageInjector が管理している依存オブジェクトの差し替え(Orverride)が 行われない条件 を満たしてしまっているようでした。(完全な原因究明をするには筆者のおつむが足りてないのでそこまで出来ずにいる)

暫定回避策(このコミットで入れた実装)の課題

PackageInjectorがシングルトンパターンで依存オブジェクトを管理しているのはインスタンスの再利用効率を高めるためであろうと思っていますが、今回のこのコミット内容は、筆者がテストのために「スタブ化したインスタンスを繰り返し差し替えたい」という目的のためにひねり出した暫定的な回避策であり、次の2つの犠牲と引き換えになっていると認識しています。

  1. メンテナンス性を犠牲にしている
    • Cw\LearnBear\Injector::getOverrideInstance() をテスト用に作り変えた「別の同様なクラス」を新たに用意しているため。
  2. Ray\Di\InjectorInterfaceインスタンスの再利用効率を犠牲にしている
    • Injector::getOverrideInstance()を呼んでスタブで差し替えを行いたいようなケースで、BEAR\Package\Injector\PackageInjector::factory() のやっているインスタンス再利用の仕組みを使わずに、DIにまつわるインスタンスを再構築して返しているため。

ここのまっとうな回避方法については、できればBEAR.Sundayの作者さんに「妥当なやり方」を伺ってみたいお気持ちでいます(´・ω・`)

2-4. d5ae6da: wip: ひとまず認証処理をベタ書きで実装(PageリソースへLoginを追加&テストを追加)

ここではひとまず、「Page\IndexPage\Next の間に挟まる Page\Login というリソースを作る」だけの実装とテストコード作成を行っていて、 認証にまつわる処理は未着手

2-5. 358de2b: wip: ひとまず認証処理をベタ書きで実装(Loginページを挟んだワークフローが動くことを確認)

【コミットメッセージ本文】

  • Madapaja\TwigModule\TwigRenderer::renderRedirectView() を見ると、twigテンプレートへ url しか渡していないことがわかる。
  • また、上記から ResourceObject側で bodyの配列へ url というキーをセットする必要が無いこともわかる。

このコミットも、 認証にまつわる処理は未着手

WorkflowTest が用意されている意義(関心事の分離)

ここで初めて「WorkflowTestというクラスが別途用意されていることの何が良いのか」を体感できた。
プロダクトコードの実装でやっていることとしては、「認証処理は置いといて、各Pageリソース同士がちゃんと繋がって画面遷移が行われる」というだけ。
このとき、テストを次のような関心事単位で分けるのが理想的なのだと、WorkflowTestを書く段に至って気が付いた。

  • 関心事1: 個々のPageリソースが担っている責務を単体で果たせているか?
  • 関心事2: 繋がって遷移できてるか?

個々のPageリソースは、「来たリクエストがどこからのものか」なんて知らないけど、次の飛び先は知ってなきゃいけない。
「ちゃんと繋がって遷移できるか?」は別でテストすべきだが、そのワークフローテストは個々のPageリソースが何をしてるのか詳細を知る必要なんて無い。

というわけなんだね、と。

コミットメッセージについて(madapaja/twig-moduleの仕様)

なお、ここのコミットメッセージ本文で触れている内容は、「redirect処理の場合はResoucrObjectクラスの bodyプロパティのキーに何をセットしてもTwigテンプレート(redirect.html.twig)に伝わらないけどなぜ?」という疑問にぶちあたり、調べてわかったことを書いたものです。

2-6. 73d851c: wip: ひとまず認証処理をベタ書きで実装(Logoutページを作成し、それを含んだワークフローが動くことを確認)

前回および前々回コミットと同様、ここでもやっていることは次の2点だけであり、 認証にまつわる処理は未着手

  • 関心事1: 画面遷移のステップを一つ追加作成し、個々の責務をテストする。
  • 関心事2: 作成したステップがちゃんと繋がってるかテストする。

2-7. 01a4135: wip: ひとまず認証処理をベタ書きで実装(「ダミーのオンメモリストレージで単にログイン/ログアウトできる」というだけのセッション実装)

ここでいよいよ認証処理に着手。
このコミットではテストコードは扱わず、ひたすら「認証処理をベタ書きで実装」を行っていて、これにより出来上がるのは「単にログイン/ログアウトできる」というだけのセッション実装です。

SessionHandlerInterfaceから考える責務分割

Sessionハンドラクラスは 3093ec7 のコミットで定義した次のメソッドしか持たない。
https://github.com/clap-and-whistle/learn-bearsunday/blob/3093ec702dc7808c54bca70a547f79a8aac24276/src/AppSpi/SessionHandlerInterface.php

認証処理でやることととSessionハンドラの各メソッドとPageリソースとの関係は次の通り。

メソッド 対応するPageリソース 認証処理でやること
setAuth() Login 認証処理をパスしたユーザーのIdentityをセッションへセットする
isNotAuthorized() Next アクセスユーザのIdentityがセッションにあるかどうか回答する
clearAuth() Logout セッションが保持するユーザーIdentityを消去する
setFlashMessage() Login 認証処理にパスできなかった旨を次の画面へ引き継ぐ
getFlashMessage() Index 認証処理にパスできなかった旨を表示する
Identity照合の責務を担うオブジェクト

Sessionハンドラの仕事は「セッションをハンドリングすること」であり、「"ユーザー名/パスワード"の組み合わせが正しいか?を照合する仕事」をSessionハンドラに持たせるのは、責務過多となりそう。
この「照合する仕事」は setAuth() の前段にあり、これは多くの場合DBなどのなんらか永続化されたデータと照合することになるでしょう。
ここではひとまず「Identity照合の責務を担うオブジェクト」として IdentityRepositoryInterface (実装クラスとしては暫定的にオンメモリの配列をストレージ代わりにしている)を設け、Page\Loginにインジェクションすることにしています。

SessionHandlerInterface の実装クラス

ここでいよいよ ray/aura-session-module の出番。
使い方の詳細は BEAR.Sunday公式の パッケージ ページから辿れる下記リンクたちを一読すべし。

src/Infrastructure/Authentication/CwSession.php にて、Aura.Sessionを使って上述の表で示した各種メソッドの「やること」を実現しています。

関わりの深いオブジェクト同士を「一つのモジュールに属する」と見る

BEAR.Sundayでは app や page といったアプリケーションのコンテキストと関連する構造を src/Module/ の配下に持っているようなんだけど、これらModuleクラスは他Moduleクラスとの依存関係も持てるようだ。

SessionHandlerInterface, IdentityRepositoryInterface は「認証処理の実装」に伴って考案されたオブジェクトであり関わりが深いと見て良さそうなので、これらを CwAuthModule という一つのモジュールとして扱い、アプリケーションのコンテキスト側で適宜installする/しないを選択できるようにしておいた。(とは言え、現時点ではコンテキストを区別する必要がなさそうなのでAppModuleに読み込ませてある)

各種PageリソースへのSessionハンドラクラス注入のやり方

このコミットでは、各種PageリソースのコンストラクタでSessionハンドラクラスをインジェクションしているますが、 Page\Next リソースについては後の「AOPへ置き換える」のセクションでこれ(注入の仕方)を変えることになります。

なお、Page\Next のコンストラクタで一緒に注入されてるのはTwigのエラーレンダラで、ray/di の 束縛アトリビュート を使っています。

2-8. 76998a6: wip: ひとまず認証処理をベタ書きで実装(前回コミットによるセッション実装にテストコードを追随)

【コミットメッセージ本文】

  • Injector::getInstance() を TestInjector::getOverrideInstance() へ変更
  • 各ResouceObjectが依存しているセッションハンドラをスタブへ差し替え

「ベタ書きセッション認証の実装」の最後のコミットとして、
ここでは前コミットで行った実装のテストコードを書いています。

当セクションの当初記載内容を確認するには、このトグルを開いてください

Sessionハンドラをスタブ化して繰り返し差し替えるための工夫

前に 2-3. 2f448edでコミットした「テストやデバッグの環境準備」のイレギュラーな対処は、ここで利用しています。

【BEAR\Package\Injector\PackageInjectorをテスト用に独自実装したもの】

素のInjectorでは(筆者が)上手く出来なかったことについて

このコミットにおけるいくつかのPageリソースオブジェクトやワークフローTestでは、
次のようにして SessionHandlerInterface をスタブ化してDI管理へ差し替える処理を行っていて、テスト要件ごとにstubインスタンスの設定を変えて繰り返し差し替える、という状況が発生しています。

Sessionハンドラのスタブ化例
        $stubSessionn = $this->createStub(SessionHandlerInterface::class);
	$stubSession->method('isNotAuthorized')->willReturn(false);
	// テストケースによってはこれが true を返してほしい場合もある

BEAR.Sundayで用意されている素の Injedtor::getOverrideInstance() で「束縛をスタブインスタンスへ差し替える」ということができることはわかったのですが、次のことがうまく出来ませんでした。

  • あるテストで差し替えてテストを終えた後、別のテストでは「差し替え前の本来のインスタンス」を使う
  • 2回目以降の同スタブインスタンス差し替え

この問題を乗り越えるために上述の「テスト用に独自実装したもの」を用意して使っているわけですが、この対処が妥当なのかどうか、ホントはどうするのがいいのか、はまだわかっていません(´・ω・`)

懸念点: Hypermedia\WorkflowTest の意義が薄れてない?

各種Pageリソースオブジェクトは、現状では html-app というアプリケーションコンテキストが前提となった実装になっています。

Hypermedia\WorkflowTest は、当初halアプリケーションとしての動作をテストする記述内容だったはずのところ、この「TwigとPHPセッション認証」の実装により Pageリソースオブジェクトが Content-Type: application/hal+json を返せなくなっているので掲題の懸念を感じていますが、どうするのが良いのかはまだわかっていません(´・ω・`)

3. それを、AOPへ置き換える

上記セクションまでで書いてきた「ベタ書き認証処理」を、以降のコミットではAOPへ置き換えています。
ここからの実装については、BEAR.Sunday公式におけるAOPに関する次の記述を予め理解しておく必要があると思います。

3-1. 6f22c29: AOPで認証を扱うためのインターセプターを仮作成

  • アノテーションやアトリビュートに使う名前を CheckAuth にしてあるのは、「認証済みかどうかを確認」するシーンでの使用を想定していることを表現しています。
  • 対応する「MethodInterceptorの実装クラス」は、AuthBaseResourceObject を継承したPageリソースオブジェクトに利用されることを前提とした実装にしています。
  • AuthBaseResourceObject は src/Resouce/配下へ置いていますが、これは後の composer build 時に問題となることが発覚します。(後続コミットの fe06835 で対応を行うことになります)
  • Module\CwAuthModule で束縛設定を行っています。

懸念している点

MethodInterceptorの実装クラスである AuthCheckInterceptor が、コンストラクタで twig-module に依存していることが、パッと見でわかりません。

現状は AppModule で CwAuthModule と HtmlModule の2つを一緒にinstallしていますが、CwAuthModule を「HTML向け用とAPI向け用で分けたい」ような場合には「CwAuthModule が HtmlModule に依存してる!」と気づくことになりそうです。
(※実は、今回の勉強中の実装過程では先にこれによる問題に遭遇し、「しょうがないから全部html-appコンテキスト前提のコードにしちゃおう」と判断して今のような実装に至っています)

3-2. 8cc9871: 認証チェックをAOPに置き換え

このコミットでは、前コミットで用意したAOPアドバイスを実際の使用箇所(今のところ Page\Next だけ)で使うように改修しています。
期待どおりに動作することを確認するためにデバッグ用の出力も twigテンプレートへ一時的に埋め込んでいます。

3-3. 150614d: デバッグ用出力の除去、インターセプター束縛設定のmatcherを最適化

前述したように、
今回の「認証処理のAOP化」に対応するMethodInterceptorの実装クラスはAuthBaseResourceObject を継承したPageリソースオブジェクトに利用されることを前提とした実装にしています。

このコミットでは、上記のことをインターセプター束縛設定で明示するようにmatcherを改修しています。

3-4. fe06835: composer buildを阻害する要素を修正

【コミットメッセージ本文】

  • Resource配下へ abstract なクラスを置くのはNGのようだ。composer compile は「Resource配下にはインスタンス化可能なものがある」という前提で動くのかもしれない。
  • pageリソースへTwigの error_page をねじ込むのに、appコンテキストで既にTwig系モジュールをinstall済ませてる状態でないといけないようだ。compile時に Name(’error_page ‘) を解決できなくてコケる。

前コミットまでの実装では、毎回 composer tests が通ることを確認してコミットを積んで行っていたのだけど、「よし、じゃぁbuild通ったらmasterブランチへマージしよう」という段に至って build が途中でコケることに気づきました。

あれこれトラブルシュートしてみて、コミットメッセージ本文に書いた事項に気がついたので、その対処をこのコミットへ積んでいます。

4. 発展:サンプルTodoアプリをそれに被せてみる

上記までのセクションで、「Twigとセッション認証による昔ながらのPHPアプリケーション」を、AOP機能を使いつつBEAR.Sunday上で再現する下準備ができました(筆者としてはそのつもり)。

このセクションでは、「AOPで簡単に認証チェックを差し込めるようになった」ことを体感できるサンプルを提示しようとしています。

BEAR.Sundayでは公式サンプルが用意されていますが、このサンプル「Polidog.Todo」アプリケーションは当初 Polidogさん がBEAR.Sundayの勉強のために書かれた ソースコードブログ を元にしているようですね。

下準備してきた当記事の環境にこれを被せるなら、Polidogさんのブログ記事に書かれている内容を後追いしながら実装するのが良さそうだ、と考えてそうすることにしました。

4-1. af99fb8: 必要なcomposerパッケージを追加インストール

【コミットメッセージ本文】
$ composer require ray/aura-sql-module:*
$ composer update
$ composer require --dev bear/devtools:*

composerの細かい使い方を把握してなくて少し手間取ったコミット。
最初、 composer require ray/aura-sql-module でやろうとしたら依存関係の解決でハマった気がする(細かいことは忘れてしまった)。

4-2. cf4957d: composer update の影響と思われる挙動の変化に対応

【コミットメッセージ本文】

  • Code::SEE_OTHER と headers['Location'] をセットした場合リダイレクトまで済んだ結果が返っていたところ、composer update後はリダイレクト実施前のリソースオブジェクトが返ってくるようになったので、継承元の実装のままで良くなった。

コミットメッセージの通りではあるものの、「なぜ composer update の前後で挙動が変わったのか」までは究明していません。

4-3. 28f617c: Polidog.Todo: データベースの準備

【コミットメッセージ本文】
加えて、下記を実行しておく。

$ sqlite3 var/db/todo.sqlite3
sqlite> create table todo(id integer primary key, title, status, created, updated);
sqlite> .exit

Polidogさんの ブログ の「データベースの準備」セクションに記載された内容を実践しているコミットです。

コミットメッセージの通りなのですが、 sqlite3 var/db/todo.sqlite3 を実行する前に
mkdir var/db
を実行しておく必要があることを書き漏らしてしまいました。
これをしておかないと、sqliteのプロンプト内で create table 文を投入したタイミングで

Error: unable to open database "var/db/todo.sqlite3": unable to open database file

のように怒られますのでご注意を。

4-4. 90e5064: Todoリソースの作成

Polidogさんの ブログ の「Todoリソースの作成」セクションに記載された内容を実践しているコミットです。

4-5. d4e4ca3: 入力フォームの作成

【コミットメッセージ本文】

  1. フォームクラスの作成
    • 直近のBEAR.Sundayパッケージの composer tests を暫定的にパスさせるために、いくつか psalm や phpstan を抑止している。
    • 併せて作成している QueryForNext は Polidog.Todo と無関係だが、画面遷移時に標準チュートリアルに関連するコード部分に対処するために追加している。
  2. AppModuleにFormクラスを記述する
  3. Nextリソース変更
    • 継承元(認証Baseクラス)でコンストラクタインジェクションを定義済みなので、ここでは別の手段でDIする必要がある。
    • Ray.Di(https://ray-di.github.io/manuals/1.0/ja/injections.html)のセッターインジェクションで代替している。
  4. URIの指定
  5. データの渡し方

ここで補記することとしては次のようなところでしょうか。

  • Polidogさんの ブログ の「Todoリソースの作成」セクションでは Page\Index をフォーム入力画面としていますが、当記事の環境では認証下である Page\Next にしています。
  • onGet, onPost とも、 #[CheckAuth] アトリビュートを入れて認証チェックの対象としています。

それ以外はコミットメッセージの通りです。

4-6. 8de3341: Twigファイルの準備

Polidogさんの ブログ の「Twigファイルの準備」セクションに記載された内容を実践しているコミットです。

4-7. 9af981e: 一覧の作成

Polidogさんの ブログ の「一覧の作成」セクションに記載された内容を実践しているコミットです。
クラス名はちょっと変えています。

4-8. dfd8fec: Done処理の実装

Polidogさんの ブログ の「Done処理の実装」セクションに記載された内容を実践しているコミットです。

onGet に #[CheckAuth] アトリビュートを入れて認証チェックの対象としています。

このコミットで Page\Login にちょっと手を加えていますが、Todoアプリ部分の挙動とは無関係で、筆者がやり忘れていたことに気が付いてついでに混ぜたものです。

Polidog.Todo被せのまとめ

以上のように、Polidogさんのブログ記事の内容を当記事環境に被せるのに、認証機能に関連して変える必要があったのは次の2点に絞られていたかと思います。

  • 「認証済みかチェックしたいPageリソース」の継承元
  • 認証済みかチェックする対象メソッドにアノテーションを追記

各クラスに対してテストコードは書いていませんが、Todo関連で追加した各種クラスをテストするコードを書く場合に、「認証関連の処理」が影響するかどうかを意識する必要は無さそうです。

システムの機能が増えるほど、AOPアドバイスで認証を扱うようにしたことのメリットは大きくなりそうですね!

5. その後の追加コミットなど

このセクションでは、当記事を公開した後に追加したコミット等について補足をします。

5-1. 810720c: Merge branch 'dev/infra'

BEAR.Sundayの作者様から このプルリク をいただき、それを機に下記事項の見直しを行いmasterブランチへ反映しています。

独自実装した TestInjector の「良くないところ」が理解できたので、これを除去

  • TestInjector で意図したことを、標準の Injector::getOverrideInstance で置き換えた。
  • TestInjector に依存していた各種テストについて、 「どの束縛を差し替えているか」の可読性を落とさない よう、コード内で明示するようにした。

AppModule から CwAuthModule と HtmlModule への依存を除去

  • appコンテキストでの各種Pageリソース動作を、html-appコンテキストでしか必要とされない束縛が阻害していたので、ひとまず 「appコンテキストではFakeを注入しておいて、html-appコンテキストで Override する」 というやり方で回避することにした。

sqlite3のファイルを自動作成

  • BEAR.Sunday公式サンプルのやり方を参考に、 bin/setup.php へ TODOアプリで扱う sqlite3データベースのファイル作成を行うように改修した。
  • ただ、当該コミットにおける bin/setup.php の差分は、本題である「sqlite3データベースのファイル作成」以外の部分への暫定対処に関する記述がけっこうなボリュームを占めてしまっている。

README更新

  • composer create-project 時から何も手を加えていなかったので、最低限必要と思われる記述を足した。

5-2. d9b4d91: Merge branch 'dev/infra'

BEAR.Sundayの作者様が このissue を立ててくださり、BEAR.Package側でバグの改修が行われました。当gitリポジトリでそのBEAR.Package更新内容を取り込むに際して、下記事項の改修を行いmasterブランチへ反映しています。

bear/package をはじめとするいくつかのComposerパッケージを更新

当Mergeコミットの一つ前の環境で composer update を実行すると、2022-10-02 時点では次のような更新結果が得られます。

Lock file operations: 0 installs, 11 updates, 0 removals
  - Upgrading bear/package (1.14.2 => 1.14.3)
  - Upgrading phpunit/phpunit (9.5.24 => 9.5.25)
  - Upgrading ray/aop (2.12.3 => 2.12.4)
  - Upgrading ray/aura-session-module (1.1.0 => 1.2.0)
  - Upgrading ray/aura-sql-module (1.12.0 => 1.13.0)
  - Upgrading roave/security-advisories (dev-latest 6d26039 => dev-latest 51ff217)
  - Upgrading sebastian/comparator (4.0.6 => 4.0.8)
  - Upgrading sebastian/exporter (4.0.4 => 4.0.5)
  - Upgrading symfony/cache (v5.4.11 => v5.4.13)
  - Upgrading symfony/http-client (v5.4.12 => v5.4.13)
  - Upgrading twig/twig (v2.15.2 => v2.15.3)

BEAR関連では、上述した BEAR.Packageのバグ改修以外にも並行して次のようなリリースが行われており、これらの更新を取り込んでいます。

また、GitHubのDependabotからのTwigの脆弱性報告 CVE-2022-39261 が来ていましたが、それの対応と思われる Upgrade もされています。

bear/package の更新により不要となったコードを除去

暫定回避のためにそこかしこに埋めていたコードを除去し、READMEに記載していた open issue の記述を削除しました。

Discussion