Repository navigation
ROADMAP
Note
Living document β last reviewed 2026-10-12 against v5.0.0. Priorities follow the project's constitution: zero dependencies, templates are data, explicit state, two editions, boring operations. Anything that breaks one of those needs an RFC, not a PR.
How to read this: Now = committed for the next minor releases Β·
Next = funded by interest, sequenced Β· Later = 5.x-shaped ideas Β·
Considered & declined = already decided, with reasons (so we don't relitigate).
Effort: π’ β€ 1 week Β· π‘ 1β4 weeks Β· π΄ > 1 month. Each item links its
tracking label.
By 2028 HTML.PHP should be the default rescue-and-modernize path for legacy PHP template estates: the thing you drop onto shared hosting or wire into a FrankenPHP worker, that lints in CI, translates with tooling instead of folklore, and keeps WordPress sites maintainable β without ever growing a dependency, a daemon, or a global.
timeline
title Release train
4.0 (done) : Clean-break rewrite : Config+DI, parser, linter, i18n tooling, WP layer
5.0 (done) : Expressions everywhere : inline %{= β¦}, nested/dynamic keys, math filters, evaluator hardening
5.1 (Q4 2026) : PHP 8.6 ready : CI matrix +8.6, edition policy, editor DX
5.2 (Q1 2027) : Performance & adapters : AST cache tier, Redis/Memcached, streaming render
5.3 (Q2 2027) : i18n depth : ICU-subset, XLIFF, in-context mode
5.x : WordPress depth : Gutenberg block, FSE, network admin
6.0 (2028?) : Contextual auto-escape, macros, sandbox policy : (breaking, RFC-gated)
5.0 shipped 2026-10-12 as an additive major: inline expressions in substitutions, nested/dynamic variable access, math filters and a hardening pass over the evaluator (bounded AST memo, iteration budgets, dynamic-key containment). The RFC-gated breaking candidates below (contextual escaping, macros) move to the 6.0 train β nothing in 5.0 pre-commits either way. β Migration 4.x β 5.0 Β· Expressions & Math
- π’ PHP 8.6 support (label:
platform) β 8.6.0 ships 2026-11-19. Add to the CI matrix andrun-matrix.sh; review new deprecations (E_STRICTcleanup aftermath, locale behaviour); confirm the 8.5 bundle parses unchanged. Done when: matrix 6/6 green on 8.6. - π’ Edition policy automation β the 8.5 overlay mechanism generalizes:
teach
tools/build.phpan--edition=8.6overlay dir so future language features land without forking files. Document the overlay contribution rule (behaviour-identical, whole-file, matrix-tested). - π‘ PHP support window: track upstream; announce EOL-driven minor bumps (8.3 security ends Dec 2030 β the portable edition lives at least that long).
- π‘ VS Code extension
htmlphp(label:dx) β TextMate grammar for%{β¦}/@{β¦}/<!--@Tag-->/<!--#ssi-->inside.html, snippets for every tag & filter, and live lint by shelling tobin/htmlphp lint --format=json(annotations in-editor). No language server needed for v1; LSP later if demand. - π’ JetBrains/PHPStorm file-template + inspections recipe (docs page; the grammar is 80 % Twig-shaped β document the mapping for existing plugins).
- π’ GitHub Action
htmlphp/lint@v1β wraps the CLI, emits::error file=β¦,line=β¦annotations so PR diffs show template errors inline. - π’ Linter autofix (safe subset) (label:
linter) β--fixfor: mismatched end-tag names (<!--@EndLoop rows-->completion), filter typos against the registry (upprβupperwhen unique),%[NAME]legacy form rewrite. Never touches semantics it can't prove. - π‘
config.schema.json+ editor validation β export the Config tree as JSON Schema from the readonly classes (reflection over defaults + enums + bounds); lets any editor validatehtmlphp.config.jsonfiles and gives the--configCLI flag a schema-checked JSON sibling.
- π‘ Serialized AST cache tier (label:
performance) β persistTagParsertrees (memo is per-process today) into the configured cache backend keyed by source hash + engine version; skips parsing on cold workers. Guarded benchmark first: only ship if p95 render improves β₯ 15 % on the bench suite. - π’ CI perf gate β run
tools/bench.phpon a pinned runner, store JSON, fail PRs regressing > 10 % vsmainmedian. Numbers live in the build, not in READMEs. - π΄ Compile-to-PHP backend (opt-in) (label:
performance) βRendereralready walks a node tree; add a backend that emits closure-per- template PHP files into the cache dir (opcache-friendly). Off by default (an executable cache dir is an attack surface on shared hosting β the exact thing v4 avoided); behindConfig+ a hard "never for untrusted templates" doc wall. Only if the AST tier isn't enough.
- π’ Redis & Memcached cache drivers (label:
cache) β thinCacheInterfaceimpls guarded by extension existence, factory-wired viacache.driver: redis|memcached+ DSN in config. (PSR-16 hosts can already plug in viaPsr16Adapter; native drivers remove the dependency argument.) - π’ First-party PSR bridge packages β
htmlphp/psr-bridges(composer-suggested): typed wrappers implementing realPsr\Log,Psr\SimpleCache,Psr\Container,Psr\EventDispatcheraround our contracts, so framework DI can autowire both ways. Keeps the core dependency-free while making interop official. - π‘ Streaming render (
yield) (label:performance) β generator variantEngine::stream()flushing per top-level node; target: MB-sized SSI-heavy documents and CLI exports without holding the whole string. Output cache disabled in streaming mode (documented).
- π‘ ICU MessageFormat subset (label:
i18n) βplural{},select{},selectordinal{}inside catalogue strings (parser + renderer, noext-intlrequirement), because gendered/ordinal languages hit the wall of{0}placeholders. Falls back to gettext forms when absent. - π’ XLIFF 2.0 import/export in
Extractor/Catalogβ agency translation workflows (Loco, Crowdin, Phrase) all speak XLIFF; today they round-trip through POT. - π‘ In-context translation mode β
?htmlphp_i18n=edit(capability- gated under WP, config-gated elsewhere): renders each message wrapped in a marker span with its msgid, plus a JS-free form endpoint to submit translations straight into a JSON catalogue. The "translator sees the button size" problem, solved without a SaaS. - π’ CLDR sync tool β
tools/sync-plurals.phpdiffsPluralForms::LANGUAGE_RULESagainst the CLDR JSON release and opens an issue on drift (the v3 French bug becomes structurally impossible).
- π‘ Gutenberg block
htmlphp/template(label:wordpress) β server-rendered block, attributestemplate+ JSONdata, template picker built from the locator roots. Needs a small JS build (@wordpress/scripts) β the first and only JS in the project, shipped prebuilt in the plugin. - π’ FSE compatibility pass β verify
renderPart()inside block themes, documenttemplate-parts/conventions; no code expected beyond locator roots. - π’ Multisite network admin tab β per-site cache sizes, per-site
audit summaries, bulk
cache:clear(wp htmlphp cache clear --network). - π‘ Cache warm cron β
Maintenance\Cronjob that re-renders a declared template list (optionhtmlphp_warm_list) afterautoptimize- style cache flushes; warms the output cache so first-hit latency dies. - π’ translate.wordpress.org sync β
wp htmlphp i18n push/pullmapping our domains onto GlotPress projects for plugin authors.
- π’ Whitespace control (label:
template-language) β<!--@Loop- β¦-->style trim markers (-left/right) for tag lines; the #1 cosmetic complaint with comment-tag syntax (blank lines in source). Linter-visible, config-off-able. - π‘
@Macro/@Importβ parameterized reusable fragments beyond partials (named args, defaults). Gated on an RFC: macros blur the "small surface" line; the design must stay declarative (no closures). - π’
RenderOptionsergonomics βfromArray()already exists; add named constructors (RenderOptions::cached(ttl:),::raw()), because positional booleans at call sites are a readability tax. - π‘ Sandbox policy object (label:
security) β oneSecurityPolicyvalue object bundling today's scattered switches (raw filter, exec, printenv, echo allow-list, include roots) into a single "trust level" (host | tenant | untrusted) with a documented matrix β for hosts that render user-supplied templates (page builders!). Prep work for 5.0.
- π’
bench.php --profileβ per-stage timings (parse/tree/SSI/cache) fromRenderStats, so "which stage regressed" is a command, not a debugger session. - π‘ OpenTelemetry-friendly events β a suggested (not required)
bridge package mapping
htmlphp.renderedstats onto OTel span attributes. Core stays dependency-free; the bridge lives inhtmlphp/psr-bridges. - π’
security.txt+ private reporting via GitHub β formalize the disclosure path already documented in SECURITY.md.
- π’ Inline expressions in substitutions β
%{= done / total * 100 | round:1 }, gated bysyntax.expressions, reusing the existing sandboxed evaluator (no new trust surface). Quote/bracket-aware payload splitting; fail-visible errors; linter coverage. β Expressions & Math - π’ Nested & dynamic variable access β
rows[0].name,prices[currency],hash['x-1 y'], UTF-8 string offsets, andget(name)indirection with a strict path grammar. Dynamic keys can never reach a plain object's properties or methods. - π’ Math surface β filters
add sub mul mod pow sqrt pct sum avg clamp(57 built-ins) and functionspow sqrt sum avg clamp get(23 whitelisted). - π’ Evaluator hardening β AST memo is a 4 096-entry FIFO (was
unbounded), 100 000-item budget on every host-iterable walk (infinite
generators used to hang
count/in),matchesthroughSupport\Pcre,INF/NAN-free arithmetic, index chains bounded by the depth guard. - π’ Expression-level
a..branges fixed β the lexer emitted a token type the parser never tested, so the documented range grammar was unreachable outside@Loop. Now parsed, capped and indexable. - π’ Linter & diagnostics β
bad-expressionfor%{= β¦},expressions-disabled, and afilter-arg-not-numericwarning for the "filter arguments are literals" trap; new codesexpr-invalid,expr-disabled. - π’ Release tooling β
tools/build.phpreads the version fromsrc/Version.phpinstead of hardcoding it in two places.
Partially advanced: the sandbox policy object below now has its first
concrete switch (syntax.expressions); the trust-level value object that
bundles it with allow_raw_filter / exec_mode / include roots is still
open.
- π΄ Contextual auto-escaping β lexer detects attribute/JS/URL/CSS
positions and picks the escaper per context (Twig-class behaviour). Big
correctness win; big parser change; only with a deprecation runway
(
syntax.escaping: flat|contextual). v5's inline expressions make this more valuable, not less: a computed value inhref="%{= url }"still gets the flat HTML escaper. - π΄ Macros + typed template contracts (if the 5.x
@MacroRFC proves demand): declared input schemas per template, lint-checked at call sites β "props for HTML templates". - π‘ Drop the 8.3 edition? β default: NO. The portable edition is the product for our hosting reality. Revisit only when PHP 8.3 leaves security support (Dec 2030).
- π‘
Template\Nodepublic stability promise β freeze the node tree as public API (it's internal today) to let third parties write renderers, formatters, and a future HTML-minifier pass. (Scope::step/Scope::indexbecame public in 5.0 β the first deliberate step in that direction.)
| Idea | Verdict | Why |
|---|---|---|
Depend on psr/log etc. directly |
β | Breaks the zero-dependency single-file promise; adapters give 95 % of the value |
| Twig-syntax compatibility layer | β | Two grammars = two linters, two mental models; migration docs are cheaper forever |
eval-based expression speedup |
βπ« | Violates principle #1 (templates are data). Non-negotiable |
| Auto-registering WP REST render endpoint | β | Authenticated template rendering over HTTP is an information-disclosure magnet; CLI + shortcodes cover the need |
| Daemon/watch mode in CLI | β | Ops surface without ops benefit; lint in CI + editor extension is the same DX |
| Bundling a markdown filter | β (for now) | Pulls a parser family into scope; host hook (filters()->add('markdown', β¦)) is two lines |
Reintroducing %[NAME] multi-pass |
β | The deterministic pipeline replaced it; see Migration Β§5.5 |
- Semver, with one honest caveat: 4.0 broke everything on purpose and said so in a migration guide. 5.0 did not β it is an additive major (the bump marks the language addition, and there are no deprecations); future breaking majors ship with β₯ 1 minor of deprecation runway.
- PHP window: each minor supports all PHP versions in security support at release time (today: 8.3 β 8.5, +8.6 from Nov 2026).
-
Editions: identical behaviour is a test-enforced contract
(
tests/run.php --lib=β¦over the matrix), not a promise. - Security fixes backport to the latest minor of each supported PHP line.
- Propose: GitHub Discussions β Ideas
or an issue labelled
rfc. Anything touching Security/Principles needs an RFC issue with the "constitution" quoted. - This file is updated at every minor release; accepted RFCs move from
Discussions into
Now/Nextwith their tracking label. - Want to pick something up? Items marked π’ with a label are intentionally small β see Contributing β Good first contributions.
Roadmap items are intentions, not commitments β except the ones CI already enforces. π
HTML.PHP 5.0.0 Β· MIT Β© Mikhail Deynekin Β· Repository Β· Releases Β· Report a security issue
Wiki pages mirror docs/ β found a mistake? Edit the page or open an issue.
π Home Problem Domain Getting Started
Template Syntax Expressions & Math SSI Guide I18n Guide WordPress Integration CLI
Configuration API Cheatsheet Diagnostics & Troubleshooting
Architecture Security Model Testing & CI
πΊοΈ ROADMAP Migration 4.x β 5.0 Migration 3.x β 4.0 Contributing Repository β