usePHP (the PSX engine)#
Relayer is built on top of polidog/use-php. The app machinery — routing, DI, authentication, caching — is handled by Relayer, while the view (rendering) is handled by usePHP. This page summarizes the essentials of usePHP, that rendering engine.
What is PSX#
PSX is "PHP that lets you write JSX-style syntax." usePHP compiles .psx files and converts them into H:: calls (hyperscript).
<?php
use Polidog\UsePhp\Html\H;
return fn () => (
<section className="card">
<h1>It works</h1>
<p>This is a page from {date('Y')}.</p>
</section>
);The above is equivalent to the following (a picture of the compiled result).
return fn () => H::section(className: 'card', children: [
H::h1(children: 'It works'),
H::p(children: ['This is a page from ', date('Y'), '.']),
]);- The attribute is
className(React style; converted toclass).
Embed PHP expressions with { ... }.
- Write JSX inside
.psx. Helper classes in.phpcannot use JSX, so
use H:: directly there.
Text is always escaped (XSS-safe)#
The Renderer escapes every text child with htmlspecialchars. There is no mechanism to inject raw HTML as-is (text is always escaped, and only the structure becomes real tags).
For that reason, when displaying Markdown you convert it not to an "HTML string" but to an H:: Element tree (this site's App\Docs\Markdown is exactly that — the structure is real tags, the text is escaped = safe).
Components#
PascalCase tags are components. use resolves the FQCN.
<?php
namespace App\Components;
use Polidog\UsePhp\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="Announcement">
<p>Body…</p>
</Card>
);Write loops and conditions as expressions.
<ul>
{array_map(fn ($x) => <li key={$x['id']}>{$x['name']}</li>, $items)}
</ul>
{$loggedIn ? (<a href="/logout">Logout</a>) : (<a href="/login">Login</a>)}Hooks (useState / useEffect / useRouter)#
Hooks work only inside a function component wrapped with fc() (a component context is required). State is held server-side, and you choose how it is persisted with StorageType. The hooks provided are useState / useEffect / useRouter (and fc).
use Polidog\UsePhp\Storage\StorageType;
use function Polidog\UsePhp\Runtime\{fc, useState, useEffect, useRouter};fc()#
fc(callable $component, ?string $key = null,
StorageType $storageType = StorageType::Session, ?Defer $defer = null): FunctionComponent$key is the identifier of the component instance, $storageType is how state is persisted, and $defer is for deferred rendering (described later). Components that use hooks must always be wrapped with fc().
useState#
useState($initial) returns [value, setter]. The setter returns an Action, so you bind it to events (such as onClick). usephp.js progressively enhances it, and falls back to a form POST even without JS.
return fc(function (array $props) {
[$count, $setCount] = useState($props['initial'] ?? 0);
return (
<div>
<span>Count: {$count}</span>
<button onClick={fn () => $setCount($count + 1)}>+</button>
<button onClick={fn () => $setCount(0)}>reset</button>
</div>
);
}, 'counter', StorageType::Snapshot);useEffect#
useEffect(callable $callback, ?array $deps = null): void- Runs on the first time (mount), and when a dependency value changes.
$deps = null… runs on every render /[]… only on mount /
[$a, $b] … only when those values change.
- If
$callbackreturns a cleanup function, it is called before the
next run (and on cleanup).
return fc(function (array $props) {
[$tab, $setTab] = useState('home');
useEffect(function () use ($tab) {
\error_log("tab changed: {$tab}");
return function () {
\error_log('cleanup before next effect');
};
}, [$tab]); // only when $tab changes
return (
<div>
<button onClick={fn () => $setTab('home')}>Home</button>
<button onClick={fn () => $setTab('about')}>About</button>
<p>active: {$tab}</p>
</div>
);
}, 'tabs', StorageType::Session);useRouter#
Gets the current URL and route parameters.
$router = useRouter();
$router['currentUrl']; // the current URL
$router['params']; // e.g. ['id' => '42'] (dynamic segments)StorageType#
| Kind | Behavior |
|---|---|
Session (default) | Held in the server session. Persists across page transitions |
Memory | Reset on every page load |
Snapshot | State is round-tripped embedded in the HTML (signed). The server is stateless. In production USEPHP_SNAPSHOT_SECRET is required |
There is no
useMemo/useRefand so on. Only the hooks above are provided. For validating form submissions and the like, see Validation / Server Actions.
Compilation and distribution#
.psx does not run as-is; it needs compilation.
vendor/bin/usephp compile src/Components # compile .psx + generate manifest
vendor/bin/usephp publish # place public/usephp.js- dev (
APP_ENV=dev) compiles automatically on request. - Production requires precompilation at deploy time (see
- The cache key is the sha1 of the source's realpath. The absolute
path must be the same at build time and at run time.
Editor syntax highlighting#
.psx is not recognized by VS Code or Vim/Neovim out of the box — left alone, it ends up either treated as PHP or opened as plain text. The polidog/use-php repository ships highlighting definitions for .psx. Installing one of them turns on:
- File-type detection for
*.psx - PHP code highlighted via the editor's built-in PHP grammar
- JSX-style rules layered on top:
- HTML elements (
<div>,<span>, …) and PascalCase component tags (<Counter />) - Self-closing tags and fragments (
<>…</>) - Attributes (literal strings and
{ ... }PHP expressions) { ... }blocks highlighted as PHP
- HTML elements (
The definitions live under editors/ in the polidog/use-php repo. Follow each editor's README to install:
- VS Code —
editors/vscode/(README).
The recommended path is to vsce package a .vsix and install it. Not yet published to the Marketplace.
- Neovim / Vim —
editors/nvim/(README).
Works with lazy.nvim, packer, or a manual copy. Pure Vim-script — no Lua dependency.
Not included today (out of scope): an LSP (type-checking, completion), a tree-sitter grammar, a PSX-aware PHPStan extension, and formatter integration. These are likely to land in dedicated repositories later.
Both definitions are regex-based, so a comparison like
$a < $bcan occasionally be mis-highlighted as a tag. File an issue on use-php with a minimal reproducer if you hit one.
Per-page assets#
Instead of adding JS/CSS globally, you can declare it only for the routes that need it.
return function (PageContext $ctx) {
$ctx->js('https://example.com/highlight.min.js', defer: true);
return fn () => (/* ... */);
};This site too loads highlight.js for code highlighting with $ctx->js() only on documentation pages that have <pre><code> (see Per-Page Scripts).
Escape hatches#
For places that need rich UI, you can offload to client-side rendering with React islands (see React Islands). Heavy parts can be made into deferred components (#[Defer] / fc(..., defer: ...), with usephp.js fetching /_defer/{name} afterward).
Relationship to Relayer#
Your app
└─ Relayer routing / DI / authentication / caching / validation / DB
└─ usePHP PSX compilation / Element rendering / state / islands / deferusePHP is "the heart of rendering." It is easier to grasp the big picture if you think of Relayer as adding the skeleton of a web app around it. Next, head to Routing & Pages.
Change history (2)
- Add editor syntax highlighting section (VS Code / Neovim) referencing use-php editors/
- Created