PHP Packagist に自作フレームワークを登録してみた!
Rayleigh(レイリー) と呼ばれる PHP 8.1+ のフレームワークをもくもくと作っています。特に理由はありません。なんとなく、 「Laravel/Symfony って PSR-7 基準じゃないし、毎年メジャーアップデートするし、 PSR-7 基準かつ毎年メジャーアップデートしない安定したフレームワークがあればいいのになあ」 と思い、調べる前に 「時間もあるし作ってみよう」 と思って技術検証も兼ねて作っている次第です。
設計思想
SIMPLE, SHORT, SMART
まず、基本的に Proof of Concept なので Symfony や Laravel のような「Easy にするための実装」を豊富に用意することが出来ない前提で、じゃあ 「Simple に使える Short な実装をしよう」 という風にしました。また、 PHPStan を level: max で運用して mixed 含め すべての型が解決出来る Smart な実装 を心がけています。
PSR-20 ClockInterface
最初は PSR-20 ClockInterface の実装から始めました。このインターフェースは最近採用されたものですが、 1 メソッドしか定義されていないので非常に簡単ですね。
interface ClockInterface
{
function now(): \DateTimeImmutable;
}
これだけです。 DateTime
ではなく DateTimeImmutable
なあたりが、多分論争起こって色々あった結果なんだろうなあ、という所がありますね。
PHP では DateTime
は若干使いづらいというか、 API が微妙なので、一般的に Carbon または Chronos が使われていると思います。
Laravel ではデフォルトで Carbon
クラスが使われる仕様となっていますが、 Illuminate\Support\DateFactory で別のクラスを使うように変更することもできます。
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
\Illuminate\Support\DateFactory::use(\Cake\Chronos\Chronos::class);
}
}
蛇足ですが、 Carbon
は DateTime
を継承する mutable なクラスですが、 CarbonImmutable
や Chronos
は DateTimeImmutable
を継承する immutable なクラスなので安全度がアップします。 Laravel を使う際は CarbonImmutable
か Chronos
を使うようにするのがおススメです。
Rayleigh では、成熟した Carbon
や Chronos
を超える実装をするのは難しいと考えたため、 Adapter を用意して基本的にそっちを使ってねーとする形にしてみました。
まだ Documentation は用意していませんが、 SystemClock
と FixedClock
が用意されています。 SystemClock
は常にシステムの現在日時を取得し、 FixedClock
は static に時間を固定して、常に同じ日時を取得できるようにしています。例えば、 HTTP Middleware の最初で Request した時間に固定して、該当のリクエスト処理中は常に同じ日時を指すような使い方を想定しています。
現在仕事では CarbonImmutable::setTestNow
メソッドを利用して固定された日時を取得するようにしているのですが、名前の通り setTestNow
メソッドはユニットテストでしか使われない想定のものなので微妙な状態ではあります。それを解決するのが FixedClock
の目的です。
PSR-3: Logger Interface
PHP では基本的にログ処理は monolog で行うと思いますので、こちらも今の所アダプタを用意しただけです。 singleton を用意しているので、 DI せず Rayleigh\Log\Logger::info('info log', $context);
を呼ぶ形になります。テストの時は WriterInterface
に stub を渡せばいいんじゃないかと思います。
PSR-7: HTTP Message
次に、 PHP で主な処理となる HTTP Message の実装に乗り出しました。
PSR-7 実装は既にいくつか存在しているので、それらを参考に自前で実装とテストを書いています。
やることは基本的に既存実装と変わらないので、テストである程度問題がないか確認しながら実装しました。一週間くらいで実装できるもんですね。
PSR-11: ContainerInterface
これは去年実装したものですが、 DI コンテナのインターフェースです。
interface ContainerInterface
{
function has(string $id): bool;
function get(string $id): mixed;
}
PSR では、 DI コンテナから取得する側の API のみ定義されています。 DI コンテナに値を設定する側は API が結構揺れているので、定義されませんでした。
Rayleigh ではシンプルなクラスリゾルバを実装していて、クラス名を指定したら、依存しているクラスやインターフェースの実装バインドをチェックしてインスタンス化します。
Laravel や他の実装ではクラス名ではなく文字列 'log'
とか 'app_path'
とかを登録して参照している部分が多いですが、分かりづらいので非推奨にしています。
値を設定する側のインターフェースは下記になります。
interface ContainerInterface extends PsrContainerInterface
{
public function bind(string $id, mixed $resolver): void;
public function forceBind(string $id, mixed $resolver): void;
public function unbind(string $id): void;
public function call(callable $func, array $args = []): mixed;
}
基本的に bind
で interface と concrete を紐づけるくらいのことしかしない認識です。また、任意の callable を DI 解決しながら呼べる call
メソッドも用意しています。この時は任意の値を引数として渡せます。 Controller の呼び出しなどで、パス変数を渡したりする想定です。
PSR-15: HTTP Handler
Middleware と一緒に、 HTTP ServerRequest クラスを渡して、 ResponseInterface を生成するインターフェースです。まだここは手を付け始めたばかりなので、スーパーグローバル変数から ServerRequest を生成するあたりしか実装していません。テストもしていないので脆弱性の塊だと思います。
その他の PSR
その他、基本的に採択された PSR に準拠した実装を最初に行う予定です。
- PSR-1: Basic Coding Standard
- PSR-3: Logger Interface
- PSR-4: Autoloading Standard
- PSR-6: Caching Interface: WIP
- PSR-7: HTTP Message Interface
- PSR-11: Container Interface: WIP
- PSR-12: Extended Coding Style Guide: WIP
- PSR-14: Event Dispatcher: WIP
- PSR-15: HTTP Handlers: WIP
- PSR-16: Simple Cache: WIP
- PSR-17: HTTP Factories: WIP
- PSR-18: HTTP Client: WIP
- PSR-20: Clock
- PER Coding Style 2.0
Packagist に登録してみた
composer で利用するためには、 packagist への登録が必要です。
GitHub Actions でリリース機能を作ればいいのかなと思いましたが、 GitHub の権限を渡せば自動的に webhook を登録して向こう側でトラックしてくれるみたいです。こちらとしては、登録する git リポジトリ(GitHub の URL)を Submit するだけで、後は勝手にやってくれました。便利~。
と思ったのですが、 Git リポジトリ 1 つにつき 1 つのパッケージしか登録出来ない仕様らしく、 Symfony のようにコンポーネントごとに 1 パッケージとしてリリースしたい場合は GitHub リポジトリもコンポーネント分用意しなければならないみたいです。つらい。なので symfony や laravel は read-only のコンポーネントリポジトリが存在しているんですね。今度 GitHub Actions でサブディレクトリを mirroring するものがないか探してみます。
今後
まずはフレームワークとして機能させるため、 ServerRequest を生成して ResposneInterface を emit する部分を作ります。 emit は既に作っていたのですが、スーパーグローバル変数から ServerRequest クラスを生成するのが意外と大変でした。 $uri がまず分からない。
そのあとはルーティングを作るのですが、これはちょっと面白いことを考えていて、 Attributes で定義したり routes.php で手動で定義するようなものではなく、 Next.js のように「ファイルパスから URL を推定する」仕組みを取ろうかなと考えています。そうすればいちいち Attributes の定義をすることも、 routes.php のメンテナンスをすることも必要なくなるので便利じゃないかなと。
または、 OpenAPI 3.0 の yaml ファイルをパースしてそのあたりの Stub ファイルを自動生成するかもしれません。
return static function (UsersListRequest $request, UserListTransaction $tx): UsersListResposne {
return new UsersListResponse($tx($request));
};
こんな感じで、そもそも Controller をクラスにせず状態を持たせないで DI だけで完結させ、ミニマムな Controller を実現させる仕組みも良さそうだなと思っています。
ここで UsersListRequest
UsersListResponse
クラスは openapi.yaml から自動生成され、バリデータも作られ、型もしっかり付くので、ユーザーはドメインロジックである UserListTransaction
クラスの実装に集中することが出来ます。便利~。
残りは、ちまちま PSR で不足している部分を実装していく予定です。 Cache とか Event Dispatcher とかは optional なものなので、暇を見て、という感じですね。
それと、最近の流行に乗って Apache や nginx によるサーブではなく、 RoadRunner や FrankenPHP を使ってサーブするのも簡単に出来るようにする予定です。その辺まで出来たらかなりプロダクションユースに近いものも出来るかもしれません。
Database については一応場所は用意していますが、どのくらい実装するか完全に未知数です。 ORM は正直 Doctrine なり Eloquent なり使えばいいし、今から実装するコストに見合うか微妙なので、別のルートを開拓しようかなとも考えています。
Discussion