Skip to content

Migration 4.x to 5.0

Mikhail Deynekin edited this page Oct 11, 2026 · 1 revision

Migration 4.x β†’ 5.0

Note

Canonical document: docs/MIGRATION-4.x-TO-5.0.md. For the 3.x rewrite migration see Migration 3.x β†’ 4.0.

5.0 is additive. Every template that rendered under 4.x renders byte-identically under 5.0 β€” no syntax removed, no default changed, no behaviour reinterpreted. The major number reflects the size of the language addition (inline expressions reach into the substitution pipeline), not a compatibility break. There are no deprecations.

composer require paulmann/html-php:^5.0     # PSR-4 source
# β€” or copy php83/html.php (resp. php85/html.php) over the old bundle

WordPress: replace wordpress/html-php/, then Tools β†’ HTML.PHP β†’ Cache: clear once.

What you gain

Feature 4.x 5.0
Math in a substitution tag ❌ β€” compute in @Set first βœ… %{= done / total * 100 | round:1 }
Hash element by dynamic key ❌ β€” static dot paths only βœ… %{= prices[currency]} Β· %{= rows[i].name}
"Variable inside a variable" ❌ βœ… %{= get(fieldName)} Β· %{= get('rows[1].name')}
Arithmetic filters div only add sub mul div mod pow sqrt pct sum avg clamp
Expression functions 17 23 (pow sqrt sum avg clamp get)
a..b ranges in expressions broken (syntax error outside @Loop) βœ… fixed, indexable: %{= (1..3)[1]}
Kill switch for inline math β€” syntax.expressions: false (fail-closed)

β†’ Full guide: Expressions & Math

Upgrading existing templates: nothing to do

The = prefix is mandatory for expressions, and a bare payload keeps its v4 meaning β€” including names with dashes:

%{a-b}      β†’ the variable literally named "a-b"   (unchanged v4 lookup)
%{= a - b}  β†’ subtraction                          (new in v5)

CSS/JS braces, @{} translations, filter chains, @If/@Set, SSI β€” all untouched. The two internal renames (Scope::walk β†’ public Scope::step, Substitution constructor + Config\Limits) affect host code that constructed those classes by hand; container-wired engines are unaffected.

Adopting the new syntax

<!-- 4.x -->
<!--@Set share="done / total * 100"-->
<div class="bar" style="width: %{share|round:0}%"></div>

<!-- 5.0 β€” same result, one tag -->
<div class="bar" style="width: %{= done / total * 100 | round:0}%"></div>

Warning

The one trap: filter arguments are still literals (unchanged rule). %{done|pct:total} divides by the string total β†’ 0. The linter now warns (filter-arg-not-numeric) and points at the %{= …} form.

Hardening checklist (user-editable templates)

Engine::create([
    'syntax' => [
        'expressions'      => false,  // refuse %{= …} outright (fail-closed)
        'allow_raw_filter' => false,  // no unescape hatch
        'strict'           => true,  // template bugs throw, never half-render
    ],
    'ssi'    => ['exec_mode' => 'deny'],
]);

With the gate closed, %{= …} tags stay visible in the output and record an expr-disabled diagnostic β€” nothing is evaluated. @If/@Set remain available (structural directives; a host that cannot trust a template with @If cannot trust it at all). β†’ Security Model

What changed under the hood

All hardening or bug fixes; none alter 4.x output:

  • Bounded AST memo β€” Template\Expression parse trees were memoized in an unbounded static array; now a 4 096-entry FIFO.
  • Iteration budget β€” count/sum/avg/in stop at 100 000 items when walking a host Traversable (an infinite generator used to hang).
  • matches via Support\Pcre β€” backtrack-limit hits are clean no-matches.
  • Range parsing fixed β€” a..b / (1..3) in @If/@Set/%{=} threw "Trailing input" before (lexer/parser token-type mismatch); @Loop ranges were and stay unaffected.
  • INF/NAN-free arithmetic β€” pow(0,-1), pow(10,9999), sqrt(-1), overflowing filter products all degrade to 0.
  • CLI option parser β€” --var k=v (the documented space form) used to be silently dropped into positional args; both --key=value and --key value now work for every option.

New codes at a glance

Code Where Meaning
expr-invalid diagnostics malformed inline expression; tag kept visible
expr-disabled diagnostics %{= …} met syntax.expressions: false
bad-expression lint (existing code, new source) %{= …} failed validation
expressions-disabled lint %{= …} while the gate is off
filter-arg-not-numeric lint (warning) numeric filter argument looks like a variable

β†’ Diagnostics & Troubleshooting

Clone this wiki locally