Rosanna (the PSX engine)#
Relayer is built on top of polidog/rosanna and polidog/rosanna-defer. The app machinery — routing, DI, authentication, caching, forms — is handled by Relayer; the view (rendering) is handled by Rosanna, and deferred fragments by rosanna-defer. This page summarizes the essentials of that rendering engine.
Rendering with Rosanna never starts a session, needs no JavaScript and adds nothing to the HTML. It takes values and returns a string.
In v1.0.0 Relayer moved its foundation from usePHP to Rosanna and dropped support for hooks such as
useState. If you are upgrading from 0.x, see Migrating from usePHP.
What is PSX?#
PSX is "PHP you can write JSX-style syntax in". Rosanna's compiler turns .psx files into H:: calls (hyperscript). Rosanna is to PSX what React is to JSX.
<?php
use Polidog\Rosanna\Html\H;
return fn () => (
<section className="card">
<h1>It works</h1>
<p>This page is from the year {date('Y')}.</p>
</section>
);The above is equivalent to the following (roughly what it compiles to).
return fn () => H::section(className: 'card', children: [
H::h1(children: 'It works'),
H::p(children: ['This page is from the year ', date('Y'), '.']),
]);- Attributes use
className(React style; converted toclass). Embed PHP expressions with{ ... }. - Inside
.psxyou write JSX. Helper classes in.phpcan't use JSX, so they callH::directly. - Attribute names PHP can't express (such as
aria-label) are passed toH::with a spread (H::button(...['aria-label' => 'Close'], children: 'x')).
Text is always escaped (XSS-safe)#
The renderer escapes every text child and attribute value. There is no mechanism for injecting raw HTML (text is always escaped; only the structure becomes real tags). There is no dangerouslySetInnerHTML either.
So when displaying Markdown, convert it not into an "HTML string" but into an H:: Element tree (this site's App\Docs\Markdown does exactly that — structure as real tags, text escaped = safe).
javascript:, vbscript: and data: values in href / src / action and similar attributes drop the attribute, and inline on* attributes or invalid tag/attribute names throw.
Components#
A PascalCase tag is a component. The FQCN is resolved via use. A component is a function that returns an 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="Notice">
<p>Body…</p>
</Card>
);A class implementing Renderable is rendered as-is when placed in 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>;
}
}Loops and conditionals are written as expressions.
<ul>
{array_map(fn ($x) => <li>{$x['name']}</li>, $items)}
</ul>
{$loggedIn ? (<a href="/logout">Logout</a>) : (<a href="/login">Login</a>)}{/* comment */}creates no child.{$cond && <b>y</b>}also works.- Numbers like
{0}or{$count}render as-is;nulland booleans render nothing. - Entities in text and attributes (
©) are decoded at compile time. - The contents of
<style>/<script>are raw strings.{$x}is not expanded, and user input must never go there. A closing tag inside throws. - Attribute spreads (
<div {...$a} />) and namespaced attribute names are not available in PSX.
Props that differ from React#
| prop | How Rosanna handles it |
|---|---|
style | Pass a string (style="color: red"). Arrays are dropped |
key | Not special. <li key="1"> is output as a plain attribute |
dangerouslySetInnerHTML | Doesn't exist. Dropped like any other non-scalar attribute |
Rosanna raises an E_USER_NOTICE for each dropped attribute, and in dev the profiler records it as rosanna.notice.
State and interaction#
Rosanna has no hooks (such as useState) and no event props that round-trip to the server (onClick / onChange / onInput / onSubmit). Rendering is always a pure "props → HTML" transformation. Rendering an event prop in Relayer throws.
When a user action needs to change server state or make the screen respond, pick by purpose:
| You want to | Use |
|---|---|
| Call server-side logic on form submit | Server actions ($ctx->action() / PageComponent::action()) |
| Validate input and show errors | Validation + server actions |
| Run rich client-side behavior | React islands |
| Load heavy or per-visitor parts later | Defer components |
A server-action form is just <form action={$send}>; the framework adds the POST target and the _rosanna_action / _rosanna_csrf hidden fields.
Compiling and distribution#
.psx doesn't run as-is; it must be compiled. Relayer uses the rosanna-defer compiler, which also writes the deferred manifest.
vendor/bin/relayer compile # compile .psx → routes → DI container in order
vendor/bin/rosanna-defer compile src/Pages # .psx only (adds src/Components if present)
vendor/bin/rosanna-defer publish # place public/rosanna-defer.jsDon't use vendor/bin/rosanna compile: it doesn't write the deferred manifest Relayer needs. rosanna-defer.js works in tandem with the server, so republish it after updating (the scaffold's composer script Polidog\Rosanna\Defer\Installer\AssetInstaller::publish does this automatically).
- dev (
APP_ENV=dev) compiles automatically on request. - Production requires precompiling at deploy time (see Deployment).
- Artifacts are written to
var/cache/psx/and the manifest holds absolute paths. The build and the runtime must use the same absolute path.
Editor syntax highlighting#
VS Code and Vim/Neovim don't recognize .psx out of the box, so left alone it gets treated as PHP or opened as plain text. The polidog/rosanna repository ships highlighting definitions for .psx; once installed you get:
- Built-in tags (
<div>) and components (<Card />) - Fragments (
<>…</>) - Attribute names, string values and
{ ... }values - Entities in text and string attribute values (
&) - PHP inside
{ ... }(tags written inside it are highlighted too) - The contents of
styleandscript({}is not treated as a PHP expression)
The definitions live under editors/ in the polidog/rosanna repository. See each editor's README for installation.
Type checking, completion and go-to-definition are not covered.
Whether
<is a comparison or a tag is guessed lexically, so$a <b>gets highlighted as a tag. Put a space after<($a < $b) and it stays a comparison.
Per-page assets#
Instead of adding JS/CSS globally, you can declare them only on the routes that need them.
return function (PageContext $ctx) {
$ctx->js('https://example.com/highlight.min.js', defer: true);
return fn () => (/* ... */);
};This site loads highlight.js for code highlighting via $ctx->js() only on doc pages that contain <pre><code> (see Per-page scripts).
Escape hatches#
Parts that need a rich UI can be handed off to client rendering with React islands (see React islands). Heavy parts can become deferred components (#[Defer] / deferred(fn, new Defer(...)); rosanna-defer.js fetches /_defer/{name} afterwards — see Defer components).
How it relates to Relayer#
Your app
└─ Relayer routing / DI / auth / cache / forms / validation / DB
├─ rosanna-defer deferred fragments / rosanna-defer.js / deferred manifest
└─ Rosanna PSX compilation / Element rendering / escapingRosanna is "the heart of rendering". Think of Relayer as the web-app skeleton added around it, and the big picture becomes easy to grasp. Next, head to Routing and pages.
Migrating from usePHP#
In v1.0.0 Relayer moved its rendering foundation from polidog/use-php to polidog/rosanna and polidog/rosanna-defer. These are the key points when upgrading a 0.x (usePHP-era) app to v1.
useState is no longer supported#
useState and everything built on it has been removed:
useState/useFormAction/useEffect/useRouterandfc()- Event props (
onClick/onChange/onInput/onSubmit). Rendering one throws wire:*attributes, Snapshot storage (StorageType), partial page updates- The
USEPHP_SNAPSHOT_SECRETenvironment variable (no longer read)
These live in a separate package, polidog/rosanna-state, not in Rosanna itself, and Relayer doesn't use it. Use server actions ($ctx->action()) or React islands instead. For example, a useState counter becomes a server action that updates the value and re-renders via Post/Redirect/Get.
Steps#
- Run
vendor/bin/relayer upgrade. It replaces the asset-publish setting incomposer.jsonscripts and updates scaffold files you haven't edited. - Change
Polidog\UsePhp\Html\H,Polidog\UsePhp\Runtime\Elementetc. in.psxand PHP toPolidog\Rosanna\…. - Replace
fc(fn, defer: new Defer(...))withdeferred(fn, new Defer(...)).DeferisPolidog\Rosanna\Defer\Deferand the function isPolidog\Rosanna\Defer\deferred. Remove statelessfc(...)wrappers and return the function directly. - In your own JS and templates, change
window.usePHPtowindow.Rosanna, theX-UsePHP-Deferheader toX-Rosanna-Defer, anddata-usephp-*todata-rosanna-*. If you read form fields directly, also change_usephp_action/_usephp_csrfto_rosanna_action/_rosanna_csrf. - Precompile with
vendor/bin/relayer compile(orvendor/bin/rosanna-defer compile).vendor/bin/usephpis gone, andvendor/bin/rosanna compiledoesn't write the deferred manifest. - Don't put
APP_ENV=devin the committed.env; put it in the gitignored.env.local(a.envwithoutAPP_ENVis production).relayer upgrademoves it out of an unedited.env. See Environment variables and the .env cascade. - The
<div data-rosanna="…">that wrapped pages is gone. Fix CSS or JS that relied on it. - The scaffold
Caddyfileis worker mode (for production) and actually sends requests topublic/worker.php. Under a worker, services live across requests, so clear per-request state withRequestLifecycle::onReset()(see Deployment). - Pass
styleas a string, and fix anything relying onkeyordangerouslySetInnerHTML(see Props that differ from React).
Because field names and session keys change, forms rendered by the old version stop working at deploy time. Purge your CDN cache on deploy so old HTML and usephp.js aren't served (see CDN cache).
Revisions (5)
- v1.1.0: usePHP→Rosanna への移行と useState 非サポートを記載、PSX エンジンを Rosanna として書き直し
- v0.31.0: useFormAction、イベント、PSX の記法、useRouter の注意を追記
- Fix broken use-php GitHub links (the repo is polidog/usePHP)
- Add editor syntax highlighting section (VS Code / Neovim) referencing use-php editors/
- Created