Skip to content

ROADMAP

Mikhail Deynekin edited this page Oct 11, 2026 · 2 revisions

πŸ—ΊοΈ 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.


Vision

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)
Loading

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


πŸ”œ Now β€” 5.1 / 5.2

Platform currency

  • 🟒 PHP 8.6 support (label: platform) β€” 8.6.0 ships 2026-11-19. Add to the CI matrix and run-matrix.sh; review new deprecations (E_STRICT cleanup 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.php an --edition=8.6 overlay 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).

Developer experience (highest leverage per hour)

  • 🟑 VS Code extension htmlphp (label: dx) β€” TextMate grammar for %{…}/@{…}/<!--@Tag-->/<!--#ssi--> inside .html, snippets for every tag & filter, and live lint by shelling to bin/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) β€” --fix for: mismatched end-tag names (<!--@EndLoop rows--> completion), filter typos against the registry (upprβ†’upper when 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 validate htmlphp.config.json files and gives the --config CLI flag a schema-checked JSON sibling.

Performance (measured, not vibes)

  • 🟑 Serialized AST cache tier (label: performance) β€” persist TagParser trees (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.php on a pinned runner, store JSON, fail PRs regressing > 10 % vs main median. Numbers live in the build, not in READMEs.
  • πŸ”΄ Compile-to-PHP backend (opt-in) (label: performance) β€” Renderer already 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); behind Config + a hard "never for untrusted templates" doc wall. Only if the AST tier isn't enough.

Cache & integrations

  • 🟒 Redis & Memcached cache drivers (label: cache) β€” thin CacheInterface impls guarded by extension existence, factory-wired via cache.driver: redis|memcached + DSN in config. (PSR-16 hosts can already plug in via Psr16Adapter; native drivers remove the dependency argument.)
  • 🟒 First-party PSR bridge packages β€” htmlphp/psr-bridges (composer-suggested): typed wrappers implementing real Psr\Log, Psr\SimpleCache, Psr\Container, Psr\EventDispatcher around our contracts, so framework DI can autowire both ways. Keeps the core dependency-free while making interop official.
  • 🟑 Streaming render (yield) (label: performance) β€” generator variant Engine::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).

πŸ”­ Next β€” 5.3+

i18n depth

  • 🟑 ICU MessageFormat subset (label: i18n) β€” plural{}, select{}, selectordinal{} inside catalogue strings (parser + renderer, no ext-intl requirement), 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.php diffs PluralForms::LANGUAGE_RULES against the CLDR JSON release and opens an issue on drift (the v3 French bug becomes structurally impossible).

WordPress depth

  • 🟑 Gutenberg block htmlphp/template (label: wordpress) β€” server-rendered block, attributes template + JSON data, 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, document template-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\Cron job that re-renders a declared template list (option htmlphp_warm_list) after autoptimize- style cache flushes; warms the output cache so first-hit latency dies.
  • 🟒 translate.wordpress.org sync β€” wp htmlphp i18n push/pull mapping our domains onto GlotPress projects for plugin authors.

Engine & language

  • 🟒 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).
  • 🟒 RenderOptions ergonomics β€” 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) β€” one SecurityPolicy value 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.

Operations

  • 🟒 bench.php --profile β€” per-stage timings (parse/tree/SSI/cache) from RenderStats, so "which stage regressed" is a command, not a debugger session.
  • 🟑 OpenTelemetry-friendly events β€” a suggested (not required) bridge package mapping htmlphp.rendered stats onto OTel span attributes. Core stays dependency-free; the bridge lives in htmlphp/psr-bridges.
  • 🟒 security.txt + private reporting via GitHub β€” formalize the disclosure path already documented in SECURITY.md.

βœ… Shipped in 5.0 (2026-10-12)

  • 🟒 Inline expressions in substitutions β€” %{= done / total * 100 | round:1 }, gated by syntax.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, and get(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 functions pow 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), matches through Support\Pcre, INF/NAN-free arithmetic, index chains bounded by the depth guard.
  • 🟒 Expression-level a..b ranges 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-expression for %{= …}, expressions-disabled, and a filter-arg-not-numeric warning for the "filter arguments are literals" trap; new codes expr-invalid, expr-disabled.
  • 🟒 Release tooling β€” tools/build.php reads the version from src/Version.php instead 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.

πŸŒ… Later β€” 6.0 candidates (breaking, RFC-gated)

  • πŸ”΄ 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 in href="%{= url }" still gets the flat HTML escaper.
  • πŸ”΄ Macros + typed template contracts (if the 5.x @Macro RFC 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\Node public 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::index became public in 5.0 β€” the first deliberate step in that direction.)

πŸ§ͺ Considered & declined (with reasons)

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

Support & versioning policy

  • 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.

Governance

  • 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/Next with 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. πŸ™‚

Clone this wiki locally