syntaxx / phpx-framework
PHPX Framework - A modern PHP framework with JSX-like syntax support
Requires
- php: >=8.4
Requires (Dev)
- phpunit/phpunit: ^10.0
This package is auto-updated.
Last update: 2026-06-21 18:40:42 UTC
README
React-style components, written in PHP. A small, isomorphic UI runtime: the same component renders on the server (SSR) and runs in the browser as PHP compiled to WebAssembly, then hydrates to interactive — keeping input focus and caret across re-renders.
Status: Technology Preview. The core (reconciler, hooks, SSR + hydration, Suspense, router) is solid and tested, but APIs may change and the edges are sharp. Not production-ready yet.
function Counter($props) { [$count, $setCount] = useState(0); return ( <div className="text-center"> <h1>Count: {$count}</h1> <button onClick={fn() => $setCount($count + 1)}>+</button> </div> ); }
The same Counter is server-rendered to HTML and then hydrated in the browser —
no separate JavaScript implementation, no hydration-mismatch class of bugs.
What it is (and isn't)
- A persistent-instance virtual-DOM reconciler (Preact-class): keyed diffing, surgical DOM patching, focus/caret preservation. It is not React Fiber — rendering is synchronous; there is no scheduler, no time-slicing, no concurrent mode.
- Isomorphic by construction: one tree renders through a pluggable host config to real DOM (browser), an HTML string (server), or an in-memory fake (tests). Server and client seed the same hydration state.
- Pure PHP. The browser runtime is PHP-in-WASM via the VRZNO DOM bridge; on the server it's ordinary PHP.
Installation
composer require syntaxx/phpx-framework
Requires PHP 8.4+ (developed and tested on PHP 8.4). The hook functions are
registered globally via Composer's files autoload, so they're available
everywhere once the autoloader is loaded.
Writing JSX-in-PHP (
<div>…</div>) requires the PHPX compiler, which transforms it into the plainComponent::create(...)calls shown below. For a full project (compiler + WASM build + dev server) start from the PHPX starter kit rather than wiring this package up by hand.
Components
A component is a plain function that takes $props and returns an element tree.
function Greeting($props) { return <p>Hello, {$props['name']}!</p>; }
- Capitalized names are components (resolved from global functions);
lowercase names are host elements (
div,button, …). - Without the compiler, elements are created directly:
use Syntaxx\PHPX\Framework\Component; Component::create('div', ['className' => 'card'], [ Component::create('Greeting', ['name' => 'world'], []), ]);
- Events use
on*props with PHP callables:onClick,onInput,onKeyPress, … (onDoubleClick→dblclick). Handlers are delegated — one root listener per event type — so they survive re-renders untouched.
<input value={$text} onInput={fn($e) => $setText($e->target->value)} />
Hooks
All hooks are global functions (no use needed):
| Hook | Signature | Purpose |
|---|---|---|
useState |
useState($initial, ?string $hydrationKey = null): [$value, $setValue] |
Local state; supports functional updates and === bail-out |
useEffect |
useEffect(callable $fn, ?array $deps = null): void |
Side effects after commit (never runs on the server) |
useRef |
useRef($initial = null): Ref |
Stable mutable container ($ref->current); auto-binds to the host node when passed as a ref prop |
useMemo |
useMemo(callable $factory, array $deps): mixed |
Memoized value |
useCallback |
useCallback(callable $cb, array $deps): callable |
Memoized callback |
useData |
useData(string $key, callable $fetcher): [$data, $loading, $error] |
Isomorphic data: server fetches + seeds, client reads the seed (fetches once if unseeded) |
useSuspenseData |
useSuspenseData(string $key, callable $fetcher): mixed |
Returns the value or throws a Suspension caught by the nearest <Suspense> |
Standard rules of hooks apply: call them unconditionally and in the same order every render.
Server-side rendering + hydration
Render to HTML on the server:
use Syntaxx\PHPX\Framework\{Component, ServerRenderer}; $result = ServerRenderer::render( Component::create('App', [], []), [], // explicit initial state (optional) ['pathname' => $path, 'search' => $search] ); echo "<div id=\"root\">{$result['html']}</div>"; echo ServerRenderer::stateScript($result['state']); // <script id="__phpx_state__">…</script>
Hydrate the same tree in the browser (PHP-in-WASM entry point):
use Syntaxx\PHPX\Framework\{Component, Runtime, Router}; $root = Runtime::hydrateRoot($document->getElementById('root')); $render = fn() => $root->render(Component::create('App', [], [])); $render(); Router::start($render); // client-side navigation re-renders without rebooting WASM
Streaming SSR
StreamRenderer::stream($component, $state, $location) yields shell,
boundary, and close chunks so the shell (with <Suspense> fallbacks) flushes
first and boundary content streams in as data settles.
Routing
A minimal History-API router:
Router::start(callable $onChange)— intercept internal link clicks + popstate.Router::navigate(string $href)— programmatic navigation.Router::current(): array—['pathname' => …, 'search' => …].Environment::location()/Environment::isServer()— read location isomorphically.
Routing is flat pathname matching today (no route params or nested routes yet).
Architecture
Component tree ─▶ Reconciler ─▶ HostConfig backend ─▶ output
(keyed diff, ├─ VrznoBackend → real DOM (browser)
surgical ├─ SsrBackend → HTML string (server)
patching) └─ FakeDomBackend→ in-memory (tests)
The reconciler keeps persistent nodes across renders and applies only the minimal
set of mutations, which is what preserves focus, caret, scroll, and media state.
Hooks are bound to those persistent instances. The HostConfig interface lets you
render to any target.
What's implemented
- ✅ Components, props, children, fragments
- ✅ Hooks:
useState,useEffect,useRef,useMemo,useCallback,useData,useSuspenseData - ✅ Delegated events with a synthetic-event object
- ✅ Keyed reconciliation + surgical DOM patching (focus-preserving)
- ✅
<Suspense>and streaming SSR - ✅ SSR + hydration (isomorphic, state-seeded)
- ✅ Client-side router
- ⬜ Context API,
useReducer - ⬜ Route params / nested routes / route data loaders
- ⬜ Concurrent / interruptible rendering
Testing
composer install vendor/bin/phpunit
Tests run headlessly against an in-memory FakeDomBackend (no browser needed) and
cover the reconciler, hooks, hydration, SSR, and Suspense.
Where this fits
PHPX Framework is one module of the Syntaxx / PHPX ecosystem:
- PHP-X-Parser — JSX grammar + AST for PHP
- PHPX-Compiler — transforms JSX-in-PHP → plain PHP
- PHPX-BuildTools — build/pack/export/serve for WebAssembly projects
- PHPX-WasmRuntimeVrzno — the PHP-WASM browser runtime (VRZNO)
- PHPX Framework — this package: the component runtime
License
MIT