Rosannaを作った

Rosanna という PHP のライブラリを作った。
React の要素みたいな形で HTML を書くためのもので、H で書くか、PSX という JSX っぽい構文で書く。

名前は TOTO の「Rosanna」から。Relayer(Yes)と同じく曲名から取った。Jeff Porcaro のシャッフルで有名なあの曲。

以前 usePHPというライブラリを作っている という記事を書いて、そのあと Relayer というフレームワークも作った。Rosanna はその usePHP の後継になる。

composer require polidog/rosanna
<?php
use Polidog\Rosanna\Html\H;
use Polidog\Rosanna\Rosanna;

$songs = ['Rosanna', 'Africa'];

echo new Rosanna()->renderElement(H::main(children: [
    H::h1(children: 'Toto IV'),
    H::ul(children: array_map(fn(string $song) => H::li(children: $song), $songs)),
]));

描画するだけならセッションも始まらないし、JavaScript もいらないし、HTML に余計なものも足さない。値を受け取って文字列を返すだけ。

なぜ usePHP から変えたのか

きっかけは usePHP を最新にして実際に実装してみたら、#[Defer] を付けていないクラスコンポーネントを PSX から呼ぶと例外になる、という話が出てきたこと。いままでと互換がなくなる。

そこから「usePHPとRelayerを統合したほうがいいのでは?」とか「psxとhooksでパッケージを分けたほうがいいのでは?」とか考えていくうちに、直すというより作り直したくなった。
なので usePHP のリポジトリは改名せずに残して、polidog/rosanna を新しく作った。コードは usePHP からコピーしていない。usePHP は仕様を確かめるための参考資料として読むだけにして、0 から書いた。

コピーしないということは、usePHP で直してきたセキュリティ修正も自動では付いてこない。スナップショットの HMAC、アクションの署名をページに結びつける、タグ名と属性名の検証、インラインの on* を拒否する、みたいなものを全部 Rosanna の要件として書き出して、テストを書き直した。

ついでに削ったものもある。

  • useRouter はなくした。React も router は別だし、URL からページを選ぶのはホスト(フレームワークや 1 ページ 1 ファイルの PHP)の仕事にした
  • イベントディスパッチも入れなかった。PSX と状態の管理だけに絞った

結果として Rosanna には router も DI もイベントもないし、HTTP のことも知らない。

useState をほぼ使っていなかった

usePHP は「React Hooks 風の書き心地でサーバードリブン UI」というのが売りだった。useState があって、onClick を書くとサーバーで状態が更新される。

ところが、Relayer で作っている自分のサイトを 2 つ確認してみたら、usePHP から import しているのは H、Element、Renderer だけだった。hooks もイベントの prop(onClick など)も 1 件も使っていない。フォームは Relayer のサーバーアクションを使っていて、usePHP の ActionHandler を通っていない。

いや、作っていた時点から分かってはいた。PHP なら別にサーバー側にステートを持たせればいいわけだし、useState を使いたいと思う場面はほぼない。
ただ面白かったから usePHP では実装してしまった。(PHP で React できるよ、みたいなノリだったので…)

困るのは、描画層が状態層を直接知っていたこと。状態を使わないサイトにも ComponentState の生成や data-rosanna の出力が付いてきていた。使っていないものを毎回背負っていたことになる。

パッケージを 3 つに分けた

なので最初は 1 パッケージで作った Rosanna を、あとから 3 つに分けた。

パッケージ 中身
polidog/rosanna H、Element、Renderer、PSX のコンパイラ
polidog/rosanna-defer <defer> で後から読み込む部分。セッションも状態も使わない
polidog/rosanna-state useState などの hooks、署名付きのアクション、CSRF、Snapshot、rosanna.js

依存の向きは一方向で、rosanna-state → rosanna-defer → rosanna。本体は他の 2 つのことを知らない。

状態層を切り出した

useState を使わないなら入れなくていい、が素直な形だと思う。React でも状態管理のライブラリは後から足すし。

状態が要るときだけ足す。

composer require polidog/rosanna-state

入口も new Rosanna() から new RosannaState() に変わる。

rosanna-state を入れずに onClick や wire:*、<defer> を書くと、polidog/rosanna-state を案内する例外になる。それっぽく出力されて動いているように見えるけど実は何も起きない、というのが一番困るので、黙って捨てずに止めるようにした。

CSRF も状態層に移した。描画だけで使うときは Relayer 経由を想定していて、フォームの CSRF は Relayer が持っているので。

切り出す前は 1 リポジトリで PHPUnit 1687 件。切り出した後は rosanna が 733 件、rosanna-state が 1030 件。移す前の 1687 件と両方の和を突き合わせて、なくなった 3 件は理由と後継のテストを PR に書いておいた。

defer はさらに別にした

最初は defer も状態層に入れていた。ActionHandler と rosanna.js を共有していたし、2 つのサイトでも使っていなかったので。

ただ、useState は使わないけど defer は Relayer で使いたい。

defer は Relayer の記事でも紹介したやつで、ログイン前と後でヘッダーが変わるような部分だけを後から取りに行くことで、ページ本体を CDN にキャッシュさせられる。これを使うためだけに hooks やセッションまで入れるのは違う。

なので polidog/rosanna-defer を作って、セッションも状態も使わない defer だけのパッケージにした。Renderer の差し替え口も、フォームの束縛(RenderExtension)と defer の描画(DeferredRenderer)で分けている。同じ口に載せたままだと、状態のない実装がタグの検査を複製することになるので。

Relayer の土台も usePHP から polidog/rosanna と polidog/rosanna-defer に移した。
Relayer v1.0.0 からは Rosanna の上で動いている。rosanna-state には依存していないので、Relayer では useState も onClick などのイベント prop も使えなくなった。動きが欲しいところはクライアント側の island でやる。

usePHP 版の Relayer から上げるときは vendor/bin/relayer upgrade を実行して、.psx と PHP の Polidog\UsePhp\… を Polidog\Rosanna\… に書き換える。手順は Relayer の README にまとめてある。

コンパイラは分けなかった

逆に分けなかったものもある。PSX のコンパイラ。

最初は polidog/rosanna と polidog/rosanna-compiler に分ける計画で、途中までその形で作っていた。けど公開の前に 1 パッケージに戻した。そのほうが PHP のライブラリっぽいし、本番のサーバーでも vendor/bin/rosanna compile がそのまま使える。

代わりに nikic/php-parser が必須の依存になる。そこは割り切って、コンパイル済みのファイルで描画するときはコンパイラも php-parser も読み込まれないことをテストで固定している。

usePHP から移るとき

描画だけ使っているサイトなら、だいたい名前の置き換えで済む。

composer remove polidog/use-php
composer require polidog/rosanna

Polidog\UsePhp を Polidog\Rosanna に、new Renderer('...') を new Renderer() に(引数はアクションの送信先で、状態層の話なので消えた)、vendor/bin/usephp を vendor/bin/rosanna にして PSX を再コンパイルする。

hooks を使っているサイトは rosanna-state を足して、use function を Polidog\Rosanna\State\ に替える必要がある。手順は rosanna-state の README に書いた。

いまのバージョンは v1.0.0。PHP 8.5 以上。

カテゴリ