RRelayer
Home/Getting Started

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 to class).

Embed PHP expressions with { ... }.

  • Write JSX inside .psx. Helper classes in .php cannot 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 $callback returns 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#

KindBehavior
Session (default)Held in the server session. Persists across page transitions
MemoryReset on every page load
SnapshotState is round-tripped embedded in the HTML (signed). The server is stateless. In production USEPHP_SNAPSHOT_SECRET is required

There is no useMemo / useRef and 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

Deployment).

  • 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

The definitions live under editors/ in the polidog/use-php repo. Follow each editor's README to install:

  • VS Codeeditors/vscode/ (README).

The recommended path is to vsce package a .vsix and install it. Not yet published to the Marketplace.

  • Neovim / Vimeditors/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 < $b can 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 / defer

usePHP 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.

Last updated: 2026-05-20
Change history (2)
  • Add editor syntax highlighting section (VS Code / Neovim) referencing use-php editors/
  • Created