syntaxx/phpx-framework

PHPX Framework - A modern PHP framework with JSX-like syntax support

Maintainers

Package info

github.com/Syntaxx-HQ/PHPX-Framework

pkg:composer/syntaxx/phpx-framework

Statistics

Installs: 88

Dependents: 1

Suggesters: 0

Stars: 0

Open Issues: 0

v0.3.0 2026-06-21 09:32 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 plain Component::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, … (onDoubleClickdblclick). 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