Relayer
Docsはじめにusephp

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 と真偽値は何も描画しません。
  • テキストと属性のエンティティ(&copy;)はコンパイル時にデコードされます。
  • <style> / <script> の中身は生の文字列です。{$x} は展開されず、利用者の入力を入れてはいけません。閉じタグが含まれると例外になります。
  • 属性のスプレッド(<div {...$a} />)と名前空間付きの属性名は PSX では使えません。

React と違う prop#

propRosanna での扱い
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 />)
  • フラグメント(<>…</>)
  • 属性の名前、文字列の値、{ ... } の値
  • テキストと文字列の属性値の中のエンティティ(&amp;)
  • { ... } の中の PHP(その中にさらに書いたタグも色分け)
  • style と script の中身({} を PHP の式として扱わない)

定義は polidog/rosanna リポジトリの editors/ に入っています。インストール手順は各エディタの README を参照してください。

  • VS Code — editors/vscode/(README)。
  • Neovim / Vim — editors/nvim/(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 で再表示する形に置き換えます。

手順#

  1. vendor/bin/relayer upgrade を実行します。composer.json の scripts にあるアセット publish の設定を置き換え、編集していないスキャフォールドのファイルを更新します。
  2. .psx と PHP の Polidog\UsePhp\Html\H、Polidog\UsePhp\Runtime\Element などを Polidog\Rosanna\… に変更します。
  3. fc(fn, defer: new Defer(...)) を deferred(fn, new Defer(...)) に置き換えます。 Defer は Polidog\Rosanna\Defer\Defer、関数は Polidog\Rosanna\Defer\deferred です。状態を持たない fc(...) のラッパーは外して、関数をそのまま返します。
  4. 自前の JS とテンプレートの window.usePHP を window.Rosanna に、 X-UsePHP-Defer ヘッダーを X-Rosanna-Defer に、data-usephp-* を data-rosanna-* に変更します。フォームのフィールドを直接読んでいる場合は、 _usephp_action / _usephp_csrf も _rosanna_action / _rosanna_csrf に変更します。
  5. プリコンパイルは vendor/bin/relayer compile(または vendor/bin/rosanna-defer compile)で行います。vendor/bin/usephp はなくなり、 vendor/bin/rosanna compile は deferred manifest を書き出しません。
  6. コミットする .env に APP_ENV=dev を書かず、gitignore 対象の .env.local に置きます(APP_ENV のない .env は本番扱い)。relayer upgrade は手を入れていない .env からこれを移します。詳しくは 環境変数と .env カスケード。
  7. ページを包んでいた <div data-rosanna="…"> はなくなりました。この要素に頼る CSS や JS を直します。
  8. 雛形の Caddyfile は public/worker.php に実際にリクエストを送る worker モード(本番用)です。worker ではサービスがリクエストをまたいで生きるので、リクエストごとの状態は RequestLifecycle::onReset() で消します(デプロイ)。
  9. style は文字列で渡し、key や dangerouslySetInnerHTML に頼る箇所を直します(React と違う prop)。

フィールド名とセッションのキーが変わるので、旧バージョンが描画したフォームはデプロイの時点で動かなくなります。古い HTML や usephp.js が配信されないよう、デプロイ時に CDN のキャッシュを消してください(CDN キャッシュ)。

最終更新 — 2026-11-04
改訂履歴 (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 追加/破壊的変更/依存バージョン注記)を削除し現在形に整理
  • 新規作成