[PHP-DEV] [RFC] [Discussion] Native Markup Expressions

Hi internals,

I'd like to open discussion on a new RFC, "Native Markup Expressions":

RFC: PHP: rfc:native_markup_expressions
Implementation (with tests): Native Markup Expressions (JSX in PHP) by imliam · Pull Request #22661 · php/php-src · GitHub

It proposes a native syntax for HTML fragments as first-class PHP expressions,
akin to how the frontend community has adopted JSX, with composition and
escape-by-default output:

    class Greeting implements Markup\Html
    {
        public function __construct(public string $name) {}

        public function toHtml(): Markup\Html
        {
            return <>
                <h1 class="title">Hello, {$this->name}!</h1>
                <p>Welcome to PHP, where markup is a first-class
                expression.</p>
            </>;
        }
    }

    echo <Greeting name="Rasmus" />; // rendered HTML, values escaped

Despite appearances, this is not a template language grafted onto the engine -
the syntax is pure compile-time sugar. Every markup expression lowers
during compilation to a plain `new` expression:

    $html = <button class="btn">Sign in</button>;
    // compiles to exactly:
    $html = new \Markup\Element('button', ['class' => 'btn'], ['Sign in']);

The RFC already answers many anticipated questions, and I am open
to making further adjustments so this can become a feature the whole
community benefits from.

Looking forward to your feedback.

Best regards,
Liam

On Tue, Jul 14, 2026 at 7:31 PM Liam Hammett
<liam+phpinternals@liamhammett.com> wrote:

Hi internals,

I'd like to open discussion on a new RFC, "Native Markup Expressions":

RFC: PHP: rfc:native_markup_expressions
Implementation (with tests): Native Markup Expressions (JSX in PHP) by imliam · Pull Request #22661 · php/php-src · GitHub

It proposes a native syntax for HTML fragments as first-class PHP expressions,
akin to how the frontend community has adopted JSX, with composition and
escape-by-default output:

    class Greeting implements Markup\Html
    {
        public function __construct(public string $name) {}

        public function toHtml(): Markup\Html
        {
            return <>
                <h1 class="title">Hello, {$this->name}!</h1>
                <p>Welcome to PHP, where markup is a first-class
                expression.</p>
            </>;
        }
    }

    echo <Greeting name="Rasmus" />; // rendered HTML, values escaped

Despite appearances, this is not a template language grafted onto the engine -
the syntax is pure compile-time sugar. Every markup expression lowers
during compilation to a plain `new` expression:

    $html = <button class="btn">Sign in</button>;
    // compiles to exactly:
    $html = new \Markup\Element('button', ['class' => 'btn'], ['Sign in']);

The RFC already answers many anticipated questions, and I am open
to making further adjustments so this can become a feature the whole
community benefits from.

Looking forward to your feedback.

Best regards,
Liam

Cool! I'm reading through it now, and one thing gave me pause:

Any tag whose name is capitalized, contains a namespace separator (), or names a static method (::slight_smile: is a component; everything else is a literal HTML element.

Not so sure about that heuristic. PSR-4 is not a binding standard on
the language; class names can be lowercase, and therefore should be
valid as a component name. Instead of looking at whether the tag name
is capitalized, I'd think it would be better handled using the
standard fallback resolution strategy: look in the current namespace,
then look at imports, then global/root namespace, and only then
fallback to a literal HTML tag.

--
Garrett W.

On Wed, Jul 15, 2026 at 1:33 AM Garrett W. <cbdrum05@gmail.com> wrote:

On Tue, Jul 14, 2026 at 7:31 PM Liam Hammett
<liam+phpinternals@liamhammett.com> wrote:
>
> Hi internals,
>
> I'd like to open discussion on a new RFC, "Native Markup Expressions":
>
> RFC: PHP: rfc:native_markup_expressions
> Implementation (with tests): Native Markup Expressions (JSX in PHP) by imliam · Pull Request #22661 · php/php-src · GitHub
>
> It proposes a native syntax for HTML fragments as first-class PHP expressions,
> akin to how the frontend community has adopted JSX, with composition and
> escape-by-default output:
>
>
> class Greeting implements Markup\Html
> {
> public function __construct(public string $name) {}
>
> public function toHtml(): Markup\Html
> {
> return <>
> <h1 class="title">Hello, {$this->name}!</h1>
> <p>Welcome to PHP, where markup is a first-class
> expression.</p>
> </>;
> }
> }
>
> echo <Greeting name="Rasmus" />; // rendered HTML, values escaped
>
>
> Despite appearances, this is not a template language grafted onto the engine -
> the syntax is pure compile-time sugar. Every markup expression lowers
> during compilation to a plain `new` expression:
>
>
> $html = <button class="btn">Sign in</button>;
> // compiles to exactly:
> $html = new \Markup\Element('button', ['class' => 'btn'], ['Sign in']);
>
>
> The RFC already answers many anticipated questions, and I am open
> to making further adjustments so this can become a feature the whole
> community benefits from.
>
> Looking forward to your feedback.
>
> Best regards,
> Liam

Cool! I'm reading through it now, and one thing gave me pause:

> Any tag whose name is capitalized, contains a namespace separator (), or names a static method (::slight_smile: is a component; everything else is a literal HTML element.

Not so sure about that heuristic. PSR-4 is not a binding standard on
the language; class names can be lowercase, and therefore should be
valid as a component name. Instead of looking at whether the tag name
is capitalized, I'd think it would be better handled using the
standard fallback resolution strategy: look in the current namespace,
then look at imports, then global/root namespace, and only then
fallback to a literal HTML tag.

--
Garrett W.

That's fair - PSR-4 isn't binding, and lowercase class names are legal
in PHP - it's a real tradeoff. While in theory I agree, I'd note JSX
uses this exact heuristic (lowercase = regular HTML element,
capitalised = component) and it's held up well with the frontend
ecosystem at scale for more than a decade. A few reasons I think it's
the right call here too:

1. Fallback resolution turns typos into silent bugs. With the
capitalisation rule, `<Layuot />` fails loudly with a class-not-found
error. With fallback resolution, it silently renders as a literal
`<Layuot>` element and you find out in the browser, if you find out at
all.
2. Markup expressions lower to `new` expressions at compile time.
Fallback resolution requires knowing whether a class exists, which
means hitting the autoloader - so tag meaning could no longer be
decided at compile time. It would also make markup
load-order-dependent: whether `<div>` means an element or a component
would depend on whether any loaded code defines a class named `div`,
and defining one later would silently change the meaning of existing
markup elsewhere.
3. The common case is plain HTML with components sprinkled in. The
heuristic lets the compiler treat lowercase tags as pure data with
zero resolution work for a performance boon.

Best regards,
Liam

On Wed, Jul 15, 2026 at 1:33 AM Garrett W. <cbdrum05@gmail.com> wrote:

On Tue, Jul 14, 2026 at 7:31 PM Liam Hammett
<liam+phpinternals@liamhammett.com> wrote:
>
> Hi internals,
>
> I'd like to open discussion on a new RFC, "Native Markup Expressions":
>
> RFC: PHP: rfc:native_markup_expressions
> Implementation (with tests): Native Markup Expressions (JSX in PHP) by imliam · Pull Request #22661 · php/php-src · GitHub
>
> It proposes a native syntax for HTML fragments as first-class PHP expressions,
> akin to how the frontend community has adopted JSX, with composition and
> escape-by-default output:
>
>
> class Greeting implements Markup\Html
> {
> public function __construct(public string $name) {}
>
> public function toHtml(): Markup\Html
> {
> return <>
> <h1 class="title">Hello, {$this->name}!</h1>
> <p>Welcome to PHP, where markup is a first-class
> expression.</p>
> </>;
> }
> }
>
> echo <Greeting name="Rasmus" />; // rendered HTML, values escaped
>
>
> Despite appearances, this is not a template language grafted onto the engine -
> the syntax is pure compile-time sugar. Every markup expression lowers
> during compilation to a plain `new` expression:
>
>
> $html = <button class="btn">Sign in</button>;
> // compiles to exactly:
> $html = new \Markup\Element('button', ['class' => 'btn'], ['Sign in']);
>
>
> The RFC already answers many anticipated questions, and I am open
> to making further adjustments so this can become a feature the whole
> community benefits from.
>
> Looking forward to your feedback.
>
> Best regards,
> Liam

Cool! I'm reading through it now, and one thing gave me pause:

> Any tag whose name is capitalized, contains a namespace separator (), or names a static method (::slight_smile: is a component; everything else is a literal HTML element.

Not so sure about that heuristic. PSR-4 is not a binding standard on
the language; class names can be lowercase, and therefore should be
valid as a component name. Instead of looking at whether the tag name
is capitalized, I'd think it would be better handled using the
standard fallback resolution strategy: look in the current namespace,
then look at imports, then global/root namespace, and only then
fallback to a literal HTML tag.

--
Garrett W.

On Tue, Jul 14, 2026 at 7:31 PM Liam Hammett
<liam+phpinternals@liamhammett.com> wrote:

Hi internals,

I'd like to open discussion on a new RFC, "Native Markup Expressions":

RFC: PHP: rfc:native_markup_expressions
Implementation (with tests): Native Markup Expressions (JSX in PHP) by imliam · Pull Request #22661 · php/php-src · GitHub

It proposes a native syntax for HTML fragments as first-class PHP expressions,
akin to how the frontend community has adopted JSX, with composition and
escape-by-default output:

    class Greeting implements Markup\Html
    {
        public function __construct(public string $name) {}

        public function toHtml(): Markup\Html
        {
            return <>
                <h1 class="title">Hello, {$this->name}!</h1>
                <p>Welcome to PHP, where markup is a first-class
                expression.</p>
            </>;
        }
    }

    echo <Greeting name="Rasmus" />; // rendered HTML, values escaped

Despite appearances, this is not a template language grafted onto the engine -
the syntax is pure compile-time sugar. Every markup expression lowers
during compilation to a plain `new` expression:

    $html = <button class="btn">Sign in</button>;
    // compiles to exactly:
    $html = new \Markup\Element('button', ['class' => 'btn'], ['Sign in']);

The RFC already answers many anticipated questions, and I am open
to making further adjustments so this can become a feature the whole
community benefits from.

Looking forward to your feedback.

Best regards,
Liam

So if I can do `<ClassName::staticMethod ...>`, what about
`<$obj->method ...>` if that method returns a `Markup\Html` (not a
string)? I didn't see that case addressed in the RFC, so I can only
assume that would be an error, but ... should it be allowed?

--
Garrett W.

Hi!
Firt of all thank you a lot for this RFC! It’s a great idea!

I have one small addition.

PHP already provides a way to write extensions that hook into AST parsing. However, it’s too coarse grained. It forces you to intercept the entire AST pipeline instead of extending PHP’s behavior at specific points.

It would be much cleaner and honestly much cooler to make the PHP parser extensible at well defined extension points. That would enable not only JSX-like syntax, but also QL-like expressions to be integrated into PHP cleanly.

In that case, PHP could provide them as separate ext modules.

Best Regards, Ed!

ср, 15 июл. 2026 г. в 16:11, Liam Hammett <liam+phpinternals@liamhammett.com>:

Hi internals,

I’d like to open discussion on a new RFC, “Native Markup Expressions”:

RFC: https://wiki.php.net/rfc/native_markup_expressions
Implementation (with tests): https://github.com/php/php-src/pull/22661

It proposes a native syntax for HTML fragments as first-class PHP expressions,
akin to how the frontend community has adopted JSX, with composition and
escape-by-default output:

class Greeting implements Markup\Html
{
public function __construct(public string $name) {}

public function toHtml(): Markup\Html
{
return <>

Hello, {$this->name}!

Welcome to PHP, where markup is a first-class expression.

; } }

echo ; // rendered HTML, values escaped

Despite appearances, this is not a template language grafted onto the engine -
the syntax is pure compile-time sugar. Every markup expression lowers
during compilation to a plain new expression:

$html = Sign in;
// compiles to exactly:
$html = new \Markup\Element(‘button’, [‘class’ => ‘btn’], [‘Sign in’]);

The RFC already answers many anticipated questions, and I am open
to making further adjustments so this can become a feature the whole
community benefits from.

Looking forward to your feedback.

Best regards,
Liam

On 2026-07-15 11:36, Liam Hammett wrote:

It proposes a native syntax for HTML fragments as first-class PHP expressions,
akin to how the frontend community has adopted JSX, with composition and
escape-by-default output:

Heh, just today by coincidence I was pointed toward https://jsx.lol/ - which, despite the name is more about React in general.

I'm more partial to Edmond Dantes's suggestion of providing parser hooks so that different markup expression syntaxes can be added via extension, rather than locking in one that happened to be popular at one time.

1 Like

Hello!

I’m more partial to Edmond Dantes’s suggestion of providing parser hooks
https://github.com/true-async/php-src/compare/master…dsl-hooks

Your wish has been granted. :slightly_smiling_face:

Here’s an article explaining how it works. I didn’t spend much time thinking about the syntax—I simply borrowed it from JavaScript. :slightly_smiling_face:
A huge thanks to Kirill for answering countless questions on this topic and showing me the different approaches PHP has historically used to solve problems like this.

https://x.com/Edmondif143061/status/2077842921827512720

The performance is expected to be excellent, especially if the parser is implemented in C.

The generated code is compiled and cached immediately. There is no longer any need to rely on backdoor techniques to implement features like this.

PHP could easily gain its own LINQ. This would also speed up SQL-like code, since it would no longer need to be constructed at runtime. Doctrine, for example, would no longer need to implement complex caching mechanisms to compensate for that.

If there’s anything I can do to help with the RFC, I’d be happy to contribute.

чт, 16 июл. 2026 г. в 22:51, Morgan <weedpacket@varteg.nz>:

On 2026-07-15 11:36, Liam Hammett wrote:

It proposes a native syntax for HTML fragments as first-class PHP expressions,
akin to how the frontend community has adopted JSX, with composition and
escape-by-default output:

Heh, just today by coincidence I was pointed toward https://jsx.lol/ -
which, despite the name is more about React in general.

I’m more partial to Edmond Dantes’s suggestion of providing parser hooks
so that different markup expression syntaxes can be added via extension,
rather than locking in one that happened to be popular at one time.

I’d also like to point out that, with relatively small changes, PHP could likely be taught to react to attributes at compile time. That would make it possible to implement AOP, actors, and many other language extensions based on macros.

Essentially, what you’re seeing here is macros in their purest form the same concept that exists in Rust today.

чт, 16 июл. 2026 г. в 22:51, Morgan <weedpacket@varteg.nz>:

On 2026-07-15 11:36, Liam Hammett wrote:

It proposes a native syntax for HTML fragments as first-class PHP expressions,
akin to how the frontend community has adopted JSX, with composition and
escape-by-default output:

Heh, just today by coincidence I was pointed toward https://jsx.lol/ -
which, despite the name is more about React in general.

I’m more partial to Edmond Dantes’s suggestion of providing parser hooks
so that different markup expression syntaxes can be added via extension,
rather than locking in one that happened to be popular at one time.

Just came across this RFC. Given I've been maintaining XHP as a functioning PHP extension for years now (not sure if sending links in my very first internals message is going to flag something but it's TJ09/xhp-php-extension on github) I wanted to weigh in on the proposal here.

1.

> Technically, markup must begin at a bare < in operand position, and telling that apart from the comparison and shift operators requires the scanner change described under Backward Incompatible Changes - state no extension can reach. The only route open to an extension is rewriting source text before compilation (the original XHP extension's approach), which shifts line numbers in errors and stack traces and leaves the raw file something token_get_all() - and therefore every tool built on it - cannot tokenize.

The extension takes care to avoid shifting line numbers; the preprocessed code ends up looking quite messy as a result but human readability was I think never the goal for what is functionally an intermediate representation.

> Practically, a syntax lives or dies by its ecosystem. Code using an extension-only syntax cannot be reasonably published to Packagist - it is a parse error on any install without the extension - and no IDE, formatter, or static analyser updates its grammar for syntax that may not exist on a given machine. That chicken-and-egg is part of where XHP for PHP stalled, while the same feature thrived in Hack, where it is part of the language. Shipping in core is what makes markup part of PHP itself: parsers, IDEs, and analysis tools implement it once, and every developer's markup gets the same highlighting, formatting, and static analysis their ordinary code already enjoys.

This, though. This is the big thing. While it is *technically* possible for extensions to add new syntax, it is unreasonable to expect tools to be aware of that syntax. I can absolutely confirm that the biggest point of friction in using XHP today is the fact that static analysis tools like psalm or phpstan can't analyze files, code using XHP cannot be formatted or linted with php-cs-fixer, etc.

2.

One thing I think is missing that is commonly used in both my own XHP code but also various JSX frameworks is the idea of context. That is, the ability to pass data through parts of a tree without explicitly passing it through attributes. Factories/decorators may solve some of the need for this, but they seem to be global, so it doesn't seem possible to inject context into only a single subtree.

3.

> This RFC is deliberately narrower than XHP: it targets HTML specifically, not general XML.

Does this mean that inline SVG is out of the question? It's not uncommon to embed SVG in HTML, but it does often rely on XML features...

-T.J. L

On 7/14/26 7:36 PM, Liam Hammett wrote:

Hi internals,

I'd like to open discussion on a new RFC, "Native Markup Expressions":

RFC: PHP: rfc:native_markup_expressions
Implementation (with tests): [RFC] Native Markup Expressions (JSX in PHP) by imliam · Pull Request #22661 · php/php-src · GitHub

It proposes a native syntax for HTML fragments as first-class PHP expressions,
akin to how the frontend community has adopted JSX, with composition and
escape-by-default output:

     class Greeting implements Markup\Html
     {
         public function __construct(public string $name) {}

         public function toHtml(): Markup\Html
         {
             return <>
                 <h1 class="title">Hello, {$this->name}!</h1>
                 <p>Welcome to PHP, where markup is a first-class
                 expression.</p>
             </>;
         }
     }

     echo <Greeting name="Rasmus" />; // rendered HTML, values escaped

Despite appearances, this is not a template language grafted onto the engine -
the syntax is pure compile-time sugar. Every markup expression lowers
during compilation to a plain `new` expression:

     $html = <button class="btn">Sign in</button>;
     // compiles to exactly:
     $html = new \Markup\Element('button', ['class' => 'btn'], ['Sign in']);

The RFC already answers many anticipated questions, and I am open
to making further adjustments so this can become a feature the whole
community benefits from.

Looking forward to your feedback.

Best regards,
Liam

> That's fair - PSR-4 isn't binding, and lowercase class names are legal
in PHP - it's a real tradeoff. While in theory I agree, I'd note JSX
uses this exact heuristic (lowercase = regular HTML element,
capitalised = component) and it's held up well with the frontend
ecosystem at scale for more than a decade.

Doing this also means there is no runtime enforcement or type-checking of HTML tags or their attributes. One of the original selling points of XHP is that it won't let you output invalid HTML, and runtime validation is important for that.

JSX gets away with it in large part because of the Typescript ecosystem, where static analysis covers a lot of ground. I don't think core PHP syntax should be assuming people are using such tools, though. Is the performance improvement of not instantiating every HTML tag expected to be significant?

-T.J. L

On 7/14/26 8:50 PM, Liam Hammett wrote:

On Wed, Jul 15, 2026 at 1:33 AM Garrett W. <cbdrum05@gmail.com> wrote:

On Tue, Jul 14, 2026 at 7:31 PM Liam Hammett
<liam+phpinternals@liamhammett.com> wrote:

Hi internals,

I'd like to open discussion on a new RFC, "Native Markup Expressions":

RFC: PHP: rfc:native_markup_expressions
Implementation (with tests): [RFC] Native Markup Expressions (JSX in PHP) by imliam · Pull Request #22661 · php/php-src · GitHub

It proposes a native syntax for HTML fragments as first-class PHP expressions,
akin to how the frontend community has adopted JSX, with composition and
escape-by-default output:

     class Greeting implements Markup\Html
     {
         public function __construct(public string $name) {}

         public function toHtml(): Markup\Html
         {
             return <>
                 <h1 class="title">Hello, {$this->name}!</h1>
                 <p>Welcome to PHP, where markup is a first-class
                 expression.</p>
             </>;
         }
     }

     echo <Greeting name="Rasmus" />; // rendered HTML, values escaped

Despite appearances, this is not a template language grafted onto the engine -
the syntax is pure compile-time sugar. Every markup expression lowers
during compilation to a plain `new` expression:

     $html = <button class="btn">Sign in</button>;
     // compiles to exactly:
     $html = new \Markup\Element('button', ['class' => 'btn'], ['Sign in']);

The RFC already answers many anticipated questions, and I am open
to making further adjustments so this can become a feature the whole
community benefits from.

Looking forward to your feedback.

Best regards,
Liam

Cool! I'm reading through it now, and one thing gave me pause:

Any tag whose name is capitalized, contains a namespace separator (), or names a static method (::slight_smile: is a component; everything else is a literal HTML element.

Not so sure about that heuristic. PSR-4 is not a binding standard on
the language; class names can be lowercase, and therefore should be
valid as a component name. Instead of looking at whether the tag name
is capitalized, I'd think it would be better handled using the
standard fallback resolution strategy: look in the current namespace,
then look at imports, then global/root namespace, and only then
fallback to a literal HTML tag.

--
Garrett W.

That's fair - PSR-4 isn't binding, and lowercase class names are legal
in PHP - it's a real tradeoff. While in theory I agree, I'd note JSX
uses this exact heuristic (lowercase = regular HTML element,
capitalised = component) and it's held up well with the frontend
ecosystem at scale for more than a decade. A few reasons I think it's
the right call here too:

1. Fallback resolution turns typos into silent bugs. With the
capitalisation rule, `<Layuot />` fails loudly with a class-not-found
error. With fallback resolution, it silently renders as a literal
`<Layuot>` element and you find out in the browser, if you find out at
all.
2. Markup expressions lower to `new` expressions at compile time.
Fallback resolution requires knowing whether a class exists, which
means hitting the autoloader - so tag meaning could no longer be
decided at compile time. It would also make markup
load-order-dependent: whether `<div>` means an element or a component
would depend on whether any loaded code defines a class named `div`,
and defining one later would silently change the meaning of existing
markup elsewhere.
3. The common case is plain HTML with components sprinkled in. The
heuristic lets the compiler treat lowercase tags as pure data with
zero resolution work for a performance boon.

Best regards,
Liam

On Wed, Jul 15, 2026 at 1:33 AM Garrett W. <cbdrum05@gmail.com> wrote:

On Tue, Jul 14, 2026 at 7:31 PM Liam Hammett
<liam+phpinternals@liamhammett.com> wrote:

Hi internals,

I'd like to open discussion on a new RFC, "Native Markup Expressions":

RFC: PHP: rfc:native_markup_expressions
Implementation (with tests): [RFC] Native Markup Expressions (JSX in PHP) by imliam · Pull Request #22661 · php/php-src · GitHub

It proposes a native syntax for HTML fragments as first-class PHP expressions,
akin to how the frontend community has adopted JSX, with composition and
escape-by-default output:

     class Greeting implements Markup\Html
     {
         public function __construct(public string $name) {}

         public function toHtml(): Markup\Html
         {
             return <>
                 <h1 class="title">Hello, {$this->name}!</h1>
                 <p>Welcome to PHP, where markup is a first-class
                 expression.</p>
             </>;
         }
     }

     echo <Greeting name="Rasmus" />; // rendered HTML, values escaped

Despite appearances, this is not a template language grafted onto the engine -
the syntax is pure compile-time sugar. Every markup expression lowers
during compilation to a plain `new` expression:

     $html = <button class="btn">Sign in</button>;
     // compiles to exactly:
     $html = new \Markup\Element('button', ['class' => 'btn'], ['Sign in']);

The RFC already answers many anticipated questions, and I am open
to making further adjustments so this can become a feature the whole
community benefits from.

Looking forward to your feedback.

Best regards,
Liam

Cool! I'm reading through it now, and one thing gave me pause:

Any tag whose name is capitalized, contains a namespace separator (), or names a static method (::slight_smile: is a component; everything else is a literal HTML element.

Not so sure about that heuristic. PSR-4 is not a binding standard on
the language; class names can be lowercase, and therefore should be
valid as a component name. Instead of looking at whether the tag name
is capitalized, I'd think it would be better handled using the
standard fallback resolution strategy: look in the current namespace,
then look at imports, then global/root namespace, and only then
fallback to a literal HTML tag.

--
Garrett W.