Warning
Experimental project – give it a try
Write XML/HTML-like markup directly in PHP files. PHPX compiles it to plain PHP arrays that a lightweight Renderer turns into HTML strings — no template engine, no magic.
Source (PHPX):
<>
<h1 className="title">Hello, {$name ?? ucfirst($type)}!</h1>
<p>
Welcome to the world of PHPX, where you can write PHP code in a JSX-like syntax.
<img src="about:blank" alt="Happy coding!" /> forever!
</p>
</>Compiled output (PHP):
[
['$', 'h1', ['className'=>"title"], ['Hello, ', ($name ?? ucfirst($type)), '!']],
['$', 'p', null, [
'Welcome to the world of PHPX, where you can write PHP code in a JSX-like syntax.',
['$', 'img', ['src'=>"about:blank", 'alt'=>"Happy coding!"]], ' forever!',
]],
]Each element is a tuple ['$', tag, props|null, children]. PHP expressions inside { } are preserved verbatim, so the compiled output is valid, executable PHP.
Important
This project is not yet published to Packagist. Add the repository manually or include it as a submodule.
git submodule add git@github.com:attitude/phpx.git path/to/phpxAdd to your project's composer.json:
{
"repositories": [
{
"type": "vcs",
"url": "https://github.com/attitude/phpx"
}
],
"require": {
"attitude/phpx": "dev-main"
}
}composer installDownload and extract the repository, then require_once 'path/to/phpx/src/index.php' in your project.
The Compiler transforms PHPX source strings into PHP code strings.
<?php
require_once 'path/to/phpx/src/index.php';
$compiler = new \Attitude\PHPX\Compiler\Compiler();
$phpCode = $compiler->compile(<<<'PHPX'
<>
<h1 className="title">Hello, {$name ?? ucfirst($type)}!</h1>
<p>
Welcome to the world of PHPX, where you can write PHP code in a JSX-like syntax.
<img src="about:blank" alt="Happy coding!" /> forever!
</p>
</>
PHPX);
echo $phpCode;By default the compiler emits ['$', 'tag', ...] arrays. Use PragmaFormatter to emit function-call style output instead:
$compiler = new \Attitude\PHPX\Compiler\Compiler(
formatter: new \Attitude\PHPX\Compiler\PragmaFormatter(pragma: 'html', fragment: 'fragment'),
);
$compiler->compile('<h1 className="title">Hello!</h1>');
// html('h1', ['className'=>"title"], ['Hello!'])
$compiler->compile('<>Hello, {$name}!</>');
// fragment(['Hello, ', ($name), '!'])Backtick template literals with ${ } interpolation are compiled to PHP string concatenation:
`Hello, my name is ${$name}, and I come from ${$country}!`
'Hello, my name is '.($name).', and I come from '.($country).'!'The Renderer converts the compiled array format into an HTML string.
<?php
require_once 'path/to/phpx/src/index.php';
$renderer = new \Attitude\PHPX\Renderer\Renderer();
$node = ['$', 'h1', ['className' => 'title'], 'Hello, World!'];
echo $renderer($node); // <h1 class="title">Hello, World!</h1>Strings and numbers are HTML-escaped; null and bool render as empty strings (matching React behaviour):
echo $renderer('<b>hi</b>'); // <b>hi</b>
echo $renderer(null); // (empty)
echo $renderer(true); // (empty)Uppercase tag names are compiled to variable references.
formatElement() outputs $Component (a PHP variable reference) for uppercase tag names instead of the string 'Component'. This means <Greeting /> compiles to ['$', $Greeting, ...] rather than ['$', 'Greeting', ...], so the renderer's components map is bypassed — a $Greeting closure must be in scope at evaluation time instead.
If you relied on passing components via the named map, this is a breaking change.
Pass an associative array of named components as closures. Each component receives a $props array (including children):
$node = ['$', $Greeting, ['name' => 'World']];
echo $renderer($node, [
'Greeting' => function(array $props): array {
return ['$', 'p', null, "Hello, {$props['name']}!"];
},
]);
// <p>Hello, World!</p>Pass a \Closure directly as the element type to skip the components map:
$greet = fn(array $props): array => ['$', 'span', null, "Hi, {$props['name']}!"];
echo $renderer(['$', $greet, ['name' => 'Alice']]);
// <span>Hi, Alice!</span>When used directly as the element type (index 1 of the node array), only a \Closure is supported. In the components map, however, values may be any PHP callable (accepting 0 or 1 parameter); children are passed via $props['children'].
className and htmlFor map to class and for. style accepts arrays/objects and is serialised to inline CSS, with camelCase keys converted to kebab-case. data and aria accept arrays/objects and are serialised to data-* / aria-* attributes, with keys lowercased. The same namespace-expansion behaviour applies to any other attribute whose value is an associative array or object (for example, 'x' => (object)['foo' => 'bar'] becomes x-foo="bar"). class accepts an associative array or object for clsx-like conditional classes, where keys are class names and values are booleans — only truthy entries are included. Array attribute values (indexed arrays) are flattened and space-joined. null always omits the attribute; for plain (non-hyphenated) attributes false also omits the attribute and true renders a valueless boolean attribute (e.g. checked), while for hyphenated attributes — including those produced via namespace expansion such as data-*, aria-* or x-* — true and false are serialised as the strings "true" and "false" to preserve their semantic meaning.
Use dangerouslySetInnerHTML to inject raw HTML — value must be ['__html' => '...'] and is not escaped; only use with trusted content.
$pretty (bool) enables indented output; $indentation (string, default "\t") sets the indent character. $void switches void elements to HTML5-style >. $react adds <!-- --> markers around leading and trailing whitespace in string children (even when the text also contains non-whitespace) for React-compatible output. Pass encoding: to the constructor to override the default UTF-8.
All text and attribute values are escaped via htmlspecialchars (ENT_QUOTES | ENT_HTML5 | ENT_SUBSTITUTE) using the renderer's configured encoding (default UTF-8). Tag and attribute names are validated against strict patterns. The only exception is dangerouslySetInnerHTML, which intentionally bypasses escaping — treat it like innerHTML and only pass trusted content.
Compile .phpx files to .php from the command line:
php scripts/compile.php path/to/component.phpxcomposer test # run the test suite
composer test:watch # re-run on file changes
composer test:coverage # generate a coverage report
composer test:update-snapshots # update test snapshotsCreated by martin_adamko