Skip to content

attitude/phpx

Repository files navigation

PHPX — JSX-like syntax for PHP

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.

How it works

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.


Installation

Important

This project is not yet published to Packagist. Add the repository manually or include it as a submodule.

Option 1: Git submodule

git submodule add git@github.com:attitude/phpx.git path/to/phpx

Option 2: Composer (VCS repository)

Add to your project's composer.json:

{
    "repositories": [
        {
            "type": "vcs",
            "url": "https://github.com/attitude/phpx"
        }
    ],
    "require": {
        "attitude/phpx": "dev-main"
    }
}
composer install

Option 3: Download ZIP

Download and extract the repository, then require_once 'path/to/phpx/src/index.php' in your project.


Usage

Compiler

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;

Pragma formatter

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), '!'])

Template literals

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).'!'

Renderer

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>'); // &lt;b&gt;hi&lt;/b&gt;
echo $renderer(null);        // (empty)
echo $renderer(true);        // (empty)

Components

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'].

Prop conventions

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.

Renderer options

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


Security

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.


CLI compilation

Compile .phpx files to .php from the command line:

php scripts/compile.php path/to/component.phpx

Running tests

composer 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 snapshots

Created by martin_adamko

About

PHPX is to PHP what JSX is to JS

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages