Rosanna(PSX エンジン)#
Relayer は polidog/rosanna と polidog/rosanna-defer の上に構築されています。 ルーティング・DI・認証・キャッシュ・フォームなどのアプリ機構は Relayer が担い、 ビュー(描画)は Rosanna が、遅延描画の断片は rosanna-defer が担当します。このページは、その描画エンジンの要点をまとめます。
Rosanna の描画はセッションを始めず、JavaScript を必要とせず、HTML に何も足しません。値を受け取って文字列を返すだけです。
Relayer は v1.0.0 で土台を usePHP から Rosanna に移し、
useStateなどのフックをサポートしなくなりました。0.x から上げる場合は usePHP からの移行 を参照してください。
PSX とは#
PSX は「JSX 風の構文を書ける PHP」です。.psx ファイルを Rosanna のコンパイラが H:: 呼び出し(ハイパースクリプト)に変換します。Rosanna と PSX の関係は React と JSX の関係と同じです。
<?php
use Polidog\Rosanna\Html\H;
return fn () => (
<section className="card">
<h1>It works</h1>
<p>これは {date('Y')} 年のページです。</p>
</section>
);上は次と等価です(コンパイル結果のイメージ)。
return fn () => H::section(className: 'card', children: [
H::h1(children: 'It works'),
H::p(children: ['これは ', date('Y'), ' 年のページです。']),
]);- 属性は
className(React 流。classに変換)。{ ... }で PHP 式を埋め込み。 .psx内では JSX を書く。.phpのヘルパクラスは JSX 不可なのでH::を直接使う。- PHP で書けない属性名(
aria-labelなど)はH::ではスプレッドで渡します(H::button(...['aria-label' => 'Close'], children: 'x'))。
テキストは必ずエスケープされる(XSS 安全)#
Renderer はすべてのテキスト子要素と属性値をエスケープします。 生 HTML をそのまま注入する仕組みはありません(テキストは常にエスケープされ、構造だけが本物のタグになる)。dangerouslySetInnerHTML もありません。
そのため Markdown を表示するときは「HTML 文字列」ではなく H:: の Element ツリーへ変換します(このサイトの App\Docs\Markdown がまさにそれ — 構造は本物のタグ、テキストはエスケープ=安全)。
href / src / action などの javascript:・vbscript:・data: は属性ごと落とし、インラインの on* 属性や不正なタグ名・属性名は例外になります。
コンポーネント#
PascalCase のタグはコンポーネント。use で FQCN を解決します。コンポーネントは Element を返す関数です。
<?php
namespace App\Components;
use Polidog\Rosanna\Html\H;
return fn (array $props) => (
<div className="card">
<h3>{$props['title']}</h3>
<div>{$props['children'] ?? null}</div>
</div>
);use App\Components\Card;
return fn () => (
<Card title="お知らせ">
<p>本文…</p>
</Card>
);Renderable を実装したクラスも、children に置けばそのまま描画されます。
use Polidog\Rosanna\Runtime\Element;
use Polidog\Rosanna\Runtime\Renderable;
final readonly class SongRow implements Renderable
{
public function __construct(private string $title) {}
public function render(): Element
{
return <li>{$this->title}</li>;
}
}ループや条件は式で書きます。
<ul>
{array_map(fn ($x) => <li>{$x['name']}</li>, $items)}
</ul>
{$loggedIn ? (<a href="/logout">Logout</a>) : (<a href="/login">Login</a>)}{/* コメント */}は子要素を作りません。{$cond && <b>y</b>}も使えます。{0}や{$count}の数値はそのまま描画され、nullと真偽値は何も描画しません。- テキストと属性のエンティティ(
©)はコンパイル時にデコードされます。 <style>/<script>の中身は生の文字列です。{$x}は展開されず、利用者の入力を入れてはいけません。閉じタグが含まれると例外になります。- 属性のスプレッド(
<div {...$a} />)と名前空間付きの属性名は PSX では使えません。
React と違う prop#
| prop | Rosanna での扱い |
|---|---|
style | 文字列で渡す(style="color: red")。配列は捨てられる |
key | 特別扱いしない。<li key="1"> はそのまま属性として出力される |
dangerouslySetInnerHTML | 無い。スカラーでないほかの属性と同じく捨てられる |
Rosanna は捨てた属性ごとに E_USER_NOTICE を出し、dev では プロファイラ が rosanna.notice として記録します。
状態とインタラクション#
Rosanna には フック(useState など)も、サーバーを往復するイベント prop (onClick / onChange / onInput / onSubmit)もありません。描画は常に「props → HTML」の純粋な変換です。Relayer でイベント prop を描画すると例外になります。
ユーザーの操作でサーバーの状態を変えたい・画面を動かしたいときは、目的に応じて次を使います。
| やりたいこと | 使うもの |
|---|---|
| フォーム送信でサーバー側の処理を呼ぶ | サーバーアクション($ctx->action() / PageComponent::action()) |
| 入力の検証とエラー表示 | バリデーション + サーバーアクション |
| クライアント側でリッチに動かす | React アイランド |
| 重い部分・訪問者ごとの部分を後から読み込む | Defer コンポーネント |
サーバーアクションのフォームは <form action={$send}> と書くだけで、POST 先と _rosanna_action / _rosanna_csrf の hidden フィールドをフレームワークが足します。
コンパイルと配布#
.psx はそのままでは動かず、コンパイルが必要です。Relayer では deferred manifest も一緒に書き出す rosanna-defer のコンパイラを使います。
vendor/bin/relayer compile # .psx → routes → DI コンテナを順にコンパイル
vendor/bin/rosanna-defer compile src/Pages # .psx だけ(src/Components があれば追加)
vendor/bin/rosanna-defer publish # public/rosanna-defer.js を配置vendor/bin/rosanna compile は Relayer に必要な deferred manifest を書き出さないので使いません。rosanna-defer.js はサーバーと対で動くので、更新したら配置し直します(雛形の composer スクリプト Polidog\Rosanna\Defer\Installer\AssetInstaller::publish が自動で行います)。
- dev(
APP_ENV=dev)はリクエスト時に自動コンパイル。 - 本番はデプロイ時に事前コンパイル必須(デプロイ 参照)。
- 成果物は
var/cache/psx/に書かれ、manifest は絶対パスを持ちます。ビルドと実行時で同じ絶対パスである必要があります。
エディタのシンタックスハイライト#
.psx は VS Code や Vim/Neovim が標準では認識しないので、放っておくと PHP 扱いになるか、プレーンテキストとして開かれます。polidog/rosanna リポジトリには .psx 用のハイライト定義が同梱されていて、入れると以下が効きます。
- 組み込みのタグ(
<div>)とコンポーネント(<Card />) - フラグメント(
<>…</>) - 属性の名前、文字列の値、
{ ... }の値 - テキストと文字列の属性値の中のエンティティ(
&) { ... }の中の PHP(その中にさらに書いたタグも色分け)styleとscriptの中身({}を PHP の式として扱わない)
定義は polidog/rosanna リポジトリの editors/ に入っています。インストール手順は各エディタの README を参照してください。
型の検査、補完、定義へのジャンプは扱いません。
<が比較かタグかを字面で推測するので、$a <b>のように書くとタグとして色が付きます。$a < $bのように<の後に空白を入れれば比較のままです。
ページ単位のアセット#
グローバルに JS/CSS を足さず、必要なルートだけで宣言できます。
return function (PageContext $ctx) {
$ctx->js('https://example.com/highlight.min.js', defer: true);
return fn () => (/* ... */);
};このサイトでもコードハイライト用 highlight.js を、<pre><code> を持つドキュメントページだけで $ctx->js() 読み込みしています(ページ単位のスクリプト 参照)。
エスケープハッチ#
リッチな UI が必要な箇所は React アイランドでクライアント描画に逃がせます(React アイランド 参照)。重い部分は 遅延コンポーネント(#[Defer] / deferred(fn, new Defer(...))、/_defer/{name} を rosanna-defer.js が後追い取得)にできます(Defer コンポーネント 参照)。
Relayer との関係#
あなたのアプリ
└─ Relayer ルーティング / DI / 認証 / キャッシュ / フォーム / バリデーション / DB
├─ rosanna-defer 遅延描画の断片 / rosanna-defer.js / deferred manifest
└─ Rosanna PSX コンパイル / Element 描画 / エスケープRosanna は「描画の心臓部」。Relayer はその周りに Web アプリの骨格を足したもの、と捉えると全体像が掴みやすいです。次は ルーティングとページ へ。
usePHP からの移行#
Relayer は v1.0.0 で描画の土台を polidog/use-php から polidog/rosanna と polidog/rosanna-defer に移しました。0.x(usePHP 時代)のアプリを v1 系に上げるときの要点です。
useState はサポートしなくなった#
useState とその上に成り立つ機能はすべて廃止しました。
useState/useFormAction/useEffect/useRouterとfc()- イベント prop(
onClick/onChange/onInput/onSubmit)。描画すると例外になります wire:*属性、Snapshot ストレージ(StorageType)、ページの部分更新- 環境変数
USEPHP_SNAPSHOT_SECRET(読まれません)
これらは Rosanna 本体ではなく別パッケージ polidog/rosanna-state 側にありますが、 Relayer では使いません。代わりに サーバーアクション ($ctx->action())か React アイランド を使ってください。たとえば useState のカウンターは、サーバーアクションで値を更新して Post/Redirect/Get で再表示する形に置き換えます。
手順#
vendor/bin/relayer upgradeを実行します。composer.jsonの scripts にあるアセット publish の設定を置き換え、編集していないスキャフォールドのファイルを更新します。.psxと PHP のPolidog\UsePhp\Html\H、Polidog\UsePhp\Runtime\ElementなどをPolidog\Rosanna\…に変更します。fc(fn, defer: new Defer(...))をdeferred(fn, new Defer(...))に置き換えます。DeferはPolidog\Rosanna\Defer\Defer、関数はPolidog\Rosanna\Defer\deferredです。状態を持たないfc(...)のラッパーは外して、関数をそのまま返します。- 自前の JS とテンプレートの
window.usePHPをwindow.Rosannaに、X-UsePHP-DeferヘッダーをX-Rosanna-Deferに、data-usephp-*をdata-rosanna-*に変更します。フォームのフィールドを直接読んでいる場合は、_usephp_action/_usephp_csrfも_rosanna_action/_rosanna_csrfに変更します。 - プリコンパイルは
vendor/bin/relayer compile(またはvendor/bin/rosanna-defer compile)で行います。vendor/bin/usephpはなくなり、vendor/bin/rosanna compileは deferred manifest を書き出しません。 - コミットする
.envにAPP_ENV=devを書かず、gitignore 対象の.env.localに置きます(APP_ENVのない.envは本番扱い)。relayer upgradeは手を入れていない.envからこれを移します。詳しくは 環境変数と .env カスケード。 - ページを包んでいた
<div data-rosanna="…">はなくなりました。この要素に頼る CSS や JS を直します。 - 雛形の
Caddyfileはpublic/worker.phpに実際にリクエストを送る worker モード(本番用)です。worker ではサービスがリクエストをまたいで生きるので、リクエストごとの状態はRequestLifecycle::onReset()で消します(デプロイ)。 styleは文字列で渡し、keyやdangerouslySetInnerHTMLに頼る箇所を直します(React と違う prop)。
フィールド名とセッションのキーが変わるので、旧バージョンが描画したフォームはデプロイの時点で動かなくなります。古い HTML や usephp.js が配信されないよう、デプロイ時に CDN のキャッシュを消してください(CDN キャッシュ)。
改訂履歴 (6)
- v1.1.0: usePHP→Rosanna への移行と useState 非サポートを記載、PSX エンジンを Rosanna として書き直し
- v0.31.0: useFormAction、イベント、PSX の記法、useRouter の注意を追記
- use-php の GitHub リンク切れを修正(リポジトリ名は polidog/usePHP)
- Add editor syntax highlighting section (VS Code / Neovim) referencing use-php editors/
- バージョン差分表記(vX.Y.Z 追加/破壊的変更/依存バージョン注記)を削除し現在形に整理
- 新規作成