A proof-of-concept exploring what happens when you compile PHP with PHX syntax directly to WebAssembly.
HypeScript is a proof-of-concept framework that attempts to answer the question: "Can we compile PHP code with PHX syntax to WebAssembly and make it reactive?"
The answer turns out to be "sort of, yes" - but with significant caveats and complexity.
Terminology:
- HypeScript = The framework name
- PHX = The templating syntax used in
.phxfiles (angle bracket syntax in PHP)
Write PHP code that looks like this:
<?php
namespace MyApp;
use function PHX\createSignal;
use function PHX\on;
function Counter($props) {
list($count, $setCount) = createSignal($props['initial'] ?? 0);
$increment = function() use ($count, $setCount) {
$setCount($count->value + 1);
};
$decrement = function() use ($count, $setCount) {
$setCount($count->value - 1);
};
return <div class="counter">
<div class="display">Count: {$count}</div>
<button onclick={on($decrement, $count)}>-</button>
<button onclick={on($increment, $count)}>+</button>
</div>;
}And have it compile to WebAssembly that runs in the browser with reactive updates.
.phx files (PHP with PHX syntax)
↓
PHX Parser (nikic/php-parser) - converts PHX to createElement() calls
↓
.php files (Pure PHP)
↓
PeachPie - compiles PHP to .NET IL
↓
.NET IL (Intermediate Language)
↓
Blazor WebAssembly - compiles IL to WASM
↓
WebAssembly + JavaScript runtime
The reactivity system works by:
- Registering PHP closures during render
- Serializing signal state to JavaScript
- When events fire, calling back into WASM via JSInterop
- Executing the PHP closure in WASM
- Returning updated signal values to JavaScript
- Patching the DOM
- PHP 8.0+
- .NET 8.0 SDK
- A browser that supports WebAssembly
# Clone the repository
git clone https://github.com/lucasacoutinho/HypeScript
cd hypescript
# Build the project
./phx build
# Serve the app (builds and starts dev server on port 8080)
./phx serve
# Or serve on a custom port
./phx serve 3000
# Show help
./phx helphypescript/
├── public/ # Your HTML template
│ └── index.html
├── src/ # Your .phx files
│ └── App.phx
├── .phx/ # Build tooling (don't touch)
│ ├── runtime/ # PHX runtime (signals, createElement, etc)
│ ├── parser/ # PHX → PHP parser
│ ├── compiler/ # PeachPie + Blazor
│ └── build.sh
└── dist/ # Build output
The idea came from observing that:
- PeachPie can compile PHP to .NET IL
- Blazor can compile .NET to WebAssembly
- SolidJS has a nice reactive model using signals
- Angle bracket syntax makes UI composition easier to read
Could these pieces fit together? Turns out they can, but it's complicated.
- PHX syntax in PHP files (parsed to function calls)
- PHP closures executing as compiled WASM
- Basic reactivity via signals
- Function-based components
- Bundle size - WASM output is huge compared to equivalent JS
- Reflection overhead - Calling PHP closures from JS requires reflection
- No automatic dependency tracking - You must explicitly pass signals to event handlers
- Complex build pipeline - Five stages, multiple compilers
- Debugging - Stack traces cross PHP → .NET → WASM → JS
- Dev experience - No hot reload, slow builds
<?php
function Greeting($props) {
return <div>
<h1>Hello, {$props['name']}!</h1>
</div>;
}
function App() {
return <div>
<Greeting name="World" />
</div>;
}<?php
use function PHX\createSignal;
use function PHX\on;
function Toggle($props) {
list($isOn, $setIsOn) = createSignal(false);
$toggle = function() use ($isOn, $setIsOn) {
$setIsOn(!$isOn->value);
};
return <div>
<button onclick={on($toggle, $isOn)}>
Status: {$isOn->value ? 'ON' : 'OFF'}
</button>
</div>;
}<?php
function Card($props) {
return <div class="card">
<h2>{$props['title']}</h2>
<div class="content">
{$props['children']}
</div>
</div>;
}
function App() {
return <div>
<Card title="Example">
<p>This is inside the card</p>
</Card>
</div>;
}Signals are tracked globally during render and assigned unique IDs. When the PHP code executes, it builds a JavaScript initialization script that sets up signal → DOM element bindings.
PHP closures passed to on() are registered with IDs. When a DOM event fires, JavaScript calls back into WASM with the closure ID and affected signal IDs. The WASM runtime uses reflection to invoke the closure, then returns updated signal values.
The abstraction layers are deep:
- PHP → .NET IL → WASM → JavaScript
- Each layer adds overhead and complexity
- Debugging requires understanding all layers
- Error messages can be cryptic
- Size - Sending megabytes of WASM for a simple counter
- Performance - Slower initial load than vanilla JS
- Complexity - Too many moving parts
- Ecosystem - No libraries, no tooling, no community
- Maintenance - Depends on PeachPie + Blazor staying compatible
- It shows that PHP can target WebAssembly
- It demonstrates JSInterop between WASM and JavaScript
- It proves you can execute PHP closures in the browser
- It's a fun exploration of compiler pipelines
This is a proof-of-concept. If you want to experiment with it, go ahead! But don't expect production-ready code.
MIT - Use at your own risk