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 <>
<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: https://wiki.php.net/rfc/native_markup_expressions
Implementation (with tests): https://github.com/php/php-src/pull/22661It 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 escapedDespite 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 plainnewexpression:$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 (::) 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: https://wiki.php.net/rfc/native_markup_expressions
Implementation (with tests): https://github.com/php/php-src/pull/22661It 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 escapedDespite 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 plainnewexpression:$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,
LiamCool! 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 (::) 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:
- 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. - Markup expressions lower to
newexpressions 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 nameddiv,
and defining one later would silently change the meaning of existing
markup elsewhere. - 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 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: https://wiki.php.net/rfc/native_markup_expressions
Implementation (with tests): https://github.com/php/php-src/pull/22661It 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 escapedDespite 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 plainnewexpression:$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,
LiamCool! 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 (::) 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: https://wiki.php.net/rfc/native_markup_expressions
Implementation (with tests): https://github.com/php/php-src/pull/22661It 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 escapedDespite 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 plainnewexpression:$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/22661It 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 escapedDespite 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 plainnewexpression:$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