%%'', ''<'' is in operand position followed by a letter → markup.
The operand-position decision is an explicit allowlist that **fails closed**: markup begins only after a token that //expects// an operand - ''return'', ''echo'', ''='', ''('', '','', ''%%=>%%'', binary and assignment operators, casts, and the other expression-introducing contexts - which are exactly the positions where an infix ''<'' cannot legally appear today, and where ''<'' followed by a letter, ''$'', ''{'', or ''>'' is //always a syntax error//. After every other token, ''<'' keeps its existing meaning. Two properties follow:
* **No valid program changes meaning, by construction.** Reclaiming ''<'' only ever applies where the source could not previously compile.
* **The design is future-proof in the safe direction.** If a token is missing from the allowlist (or a future PHP version adds a new operator and forgets to include it), the only consequence is that markup cannot directly follow that token - a loud, feature-side parse error worked around with parentheses and fixed by a one-line addition. A gap can never silently change what existing code means.
''%%<>%%'' is the legacy alias of ''!='' and is preserved in infix position (''$a <> $b'' is still not-equal). ''%%<>%%'' is only reinterpreted as a fragment opener in **operand position**, where it is currently a syntax error - so no valid program needs changes. Two positions are deliberately excluded from the allowlist because an infix ''<'' after them is (or may be) valid PHP: after a bare ''yield'' (''yield < LIM'' is valid, so yielding markup is written ''%%yield (
)%%'') and after a closing ''}'' (which ends ''match'' and interpolation values), so markup as a bare expression statement immediately following a block is assigned or echoed instead.
This leaves just one backwards compatibility concern:
- **New ''Markup\'' namespace** - introduces ''Markup\Html'', ''Markup\Element'', ''Markup\Fragment'', ''Markup\Raw'', ''Markup\Slot'', ''Markup\raw()'', ''Markup\escape()''. Top-level ''Markup\'' follows the established precedent for bundled extensions (''Random\'', ''Dom\'', ''Uri\''). Existing userland use of the prefix is minimal: a handful of Packagist packages autoload sub-namespaces such as ''Markup\Json\'' (which coexist with internal ''Markup\'' classes, exactly as userland ''Random\*'' sub-namespaces did when ''Random\'' shipped in PHP 8.2), and a public-code search finds no class or function whose fully-qualified name collides with the symbols introduced here. No existing keywords are reserved and no new global reserved words are introduced (the syntax uses bare ''<'', and the ''#lazy'' directive is only meaningful inside a markup tag).
===== Proposed PHP Version(s) =====
Next minor version of PHP 8.x.
===== RFC Impact =====
==== To the Ecosystem ====
The scanner emits new ''T_MARKUP_*'' tokens (exposed as constants and through ''token_get_all()''/''PhpToken''); ordinary code continues to tokenize identically. Tools that parse PHP source (nikic/php-parser, PHPStan, Psalm, php-cs-fixer, IDEs) will need grammar updates to understand markup expressions, as with any new syntax.
A working syntax-highlighting/IDE extension for VS Code was built alongside the reference implementation to validate that the syntax is toolable.
Because markup lowers to ordinary AST at compile time, everything downstream of the parser is unaffected: opcache caches and restores markup-containing files normally (verified, including the file cache), and reflection, serialization, ''var_export()'', ''clone'' and ''json_encode()'' on the value objects behave as for any other class.
A related question is what this means for **template engines** (Blade, Twig, Latte). This RFC does not replace them, and does not aim to: a full engine is much more than syntax plus escaping - layout inheritance, a designer-facing dialect that is deliberately //not// PHP, sandboxed rendering of untrusted templates, context-aware escaping, fragment caching, and years of ecosystem conventions. Nothing here removes any of that; markup is opt-in syntax, and code that never uses it is untouched.
What this RFC offers the engines is a first-class substrate to build on. Today each engine maintains its own parser, its own escaping, and its own component model, because the language provides none. With markup native, an engine can adopt the shared foundation where it fits: ''Markup\Html'' is a common currency (a Blade view and a markup expression can accept and return the same interface and compose within one page); the dispatch hooks exist precisely so a framework can route component tags through its existing container and resolution logic; and an engine's component layer could compile //to// markup expressions, inheriting escape-by-default and end-to-end static analysis for free. The likely long-term shape is convergence rather than replacement: engines keep their higher-level features and progressively delegate parsing, escaping, and the value model to the language - much as database layers did not disappear when PDO shipped, but re-based onto it.
==== To Existing Extensions ====
A new always-enabled bundled extension, ''ext/markup'', provides the runtime symbols (like ''ext/random'', it cannot be disabled: the syntax lowers into these symbols at compile time). ''ext/tokenizer'' exposes the new ''T_MARKUP_*'' tokens; ordinary code tokenizes identically. ''ext/opcache'' needs no changes - markup compiles to ordinary opcode literals that persist and restore through the file cache (verified). ''ext/dom'' is unaffected; interoperability happens through the string boundary in userland (see Element model).
==== To SAPIs ====
None. The feature is a compile-time syntax plus a bundled runtime extension; it behaves identically across CLI, FPM, the development web server, and embedded PHP.
===== Open Issues =====
None at this time.
===== Future Scope =====
Some natural extensions are deliberately left out of this RFC to keep its scope tight and expandable in the future; none is required for the feature to be useful:
* **Context-aware escaping** (URL/JS/CSS contexts), as in Go's ''html/template'' or Latte's escaping - so that e.g. a ''javascript:'' URL in ''href'' is caught. This RFC escapes HTML context only; the value-tree model is designed so this can be layered on later without a syntax change. Some examples include:
* A JavaScript escaping context inside ''%%