HTML.PHP โ Template Processing Library ๐งฉโก#
Production-grade PHP 8.3+/8.5 template engine with full Apache SSI
directive support (#ifexpressions,#execpolicies,#printenv,
#block/stub), a gettext-compatible i18n layer (PO/MO/JSON/PHP
catalogues, plural rules for 60+ languages, extraction & audit tooling),
layout inheritance, scoped partials, a safe expression language โ inline
in substitutions since v5 (%{= a / b * 100}, nested/dynamic keys,
get()indirection) โ a render cache, a template linter, profiling stats,
an optional WordPress bridge + maintenance toolkit, and a hardened
escape-by-default security model with zero$GLOBALS, zero
superglobal reads in the core, zero dependencies.
What's new in 5.0.0#
Version 5 is an additive release: every 4.x template renders
byte-identically. See docs/MIGRATION-4.x-TO-5.0.md.
| 4.x | 5.0 | |
|---|---|---|
| Math in a substitution tag | compute in @Set first |
%{= done / total * 100 | round:1 } |
| Hash element by a 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) | fixed, indexable: %{= (1..3)[1]} |
| Kill switch for inline math | โ | syntax.expressions: false (fail-closed) |
Hardening that came with it: bounded AST memo (4 096 FIFO), a 100 000-item
iteration budget on every host-iterable walk, dynamic keys that can never
reach an object property or method, get() paths that resolve against the
scope stack only, and INF/NAN-free arithmetic. Inline expressions reuse
the existing sandboxed evaluator โ no eval, whitelisted functions only,
auto-escaped output โ so they add no new trust surface. Details:
docs/SECURITY.md ยง4.
Upgrading#
From 4.x โ 5.0: additive release, no breaking changes, no
deprecations โ docs/MIGRATION-4.x-TO-5.0.md.From 3.x: 4.0 was a clean-break rewrite โ see the full mapping table
in docs/MIGRATION-3.x-TO-4.0.md, then
upgrade to 5.x. Release history: CHANGELOG.md.
Two editions#
| Edition | File | PHP | Language features |
|---|---|---|---|
| 8.3 (portable) | php83/html.php |
8.3 โ 8.5+ | typed class constants, readonly classes, enums |
| 8.5 (modern) | php85/html.php |
8.5+ | everything above plus pipe operator |>, array_find(), property hooks, asymmetric visibility, #[\NoDiscard] |
Both are generated from the same PSR-4 source tree (src/, with
src-8.5/ overlays) by tools/build.php, and
both pass the identical test matrix (386 tests / 1286 assertions per leg,
run with and without ext-intl โ the suite is extension-independent).
Installation#
Option A โ single file (no Composer): copy php83/html.php (or
php85/html.php on PHP 8.5) into your project and require it. Done.
Option B โ Composer (PSR-4 source):
composer require paulmann/html-php # PHP 8.3/8.4
composer require paulmann/html-php85 # PHP 8.5
Option C โ WordPress: install the bundled plugin in
wordpress/html-php/ (see
docs/WORDPRESS.md).
Quick start#
require_once __DIR__ . '/php83/html.php'; // or Composer's autoloader
use HtmlPhp\Engine;
use HtmlPhp\Template\RenderOptions;
$engine = Engine::create([
'templates' => ['paths' => [__DIR__ . '/templates']],
'i18n' => ['locale' => 'de', 'paths' => [__DIR__ . '/languages']],
'cache' => ['enabled' => true, 'driver' => 'file', 'directory' => sys_get_temp_dir() . '/htmlphp'],
]);
echo $engine->render('page.html', [
'title' => 'Hello <World>',
'rows' => [['name' => 'Ada'], ['name' => 'Grace']],
]);
<!-- templates/page.html -->
<h1>%{title}</h1> <!-- auto-escaped: Hello <World> -->
<ul>
<!--@Loop rows-->
<li>%{_position}. %{name|upper}</li>
<!--@Else-->
<li>@{list.empty}</li> <!-- translated when the list is empty -->
<!--@EndLoop-->
</ul>
<!--#include virtual="/partials/footer.shtml" --> <!-- Apache SSI -->
The one-liner tour#
$engine->renderString('%{price|number:2}', ['price' => 1234.5]); // "1,234.50" (locale-aware)
$engine->renderString('%{= done / total * 100 | round:1 }%', $v); // "14.8%" โ inline math (v5)
$engine->renderString('%{= prices[currency]}', $v); // dynamic key (v5)
$engine->renderString('%{= get(fieldName)}', $v); // name from data (v5)
$engine->renderString('<!--@If total > 100 -->FREE<!--@Else-->%{ship|number:2}<!--@EndIf-->', $vars);
$engine->renderString('<!--@Set total="price * qty-->%{total|round:2}', $vars);
$engine->render('child.html'); // <!--@Extends "base.html"--> inheritance
$engine->t('{0} files', [42]); // gettext with interpolation
$engine->processSsi($postContent); // SSI over arbitrary strings (WP content!)
$engine->lintDirectory(__DIR__ . '/templates'); // CI-ready template validation
$engine->stats(); // timings, loops, includes, cache hits
Architecture at a glance#
Engine โโ facade: render / renderFile / renderString / lint / t / processSsi / stats
โ
โโ Config (immutable value object) โโ Container (PSR-11-shaped, lazy)
โโ Template\Renderer โโ pipeline: parse โ layouts โ tree render โ SSI โ cache
โ โโ TagParser โ Node tree โโ TreeRenderer (loops/ifs/sections/partials)
โ โโ Substitution (%{} #{} @{} + filters) โโ Expression (safe evaluator)
โ โโ Linter (reuses the parser)
โโ Ssi\Processor โโ mod_include: scanner + DirectiveRegistry + policies
โโ I18n\Translator โโ catalogues, plural forms, fallbacks, WP delegation
โโ Contracts\CacheInterface โโ file/apcu/array/null/wp/psr16
โโ Log\LoggerInterface โโ null/array/file/errorlog/syslog/wp/psr3
โโ Event\Dispatcher โโ WP-hook-shaped tags & priorities
โโ Diagnostics โโ capped structured collector (codes stable since 3.x)
โโ WordPress\* โโ Bridge, WpCache, WpLogger, TemplateLoader, Shortcodes,
AdminPage, Cli, Mailer + Maintenance toolkit (optional)
Hard rule enforced by the test suite (token-level scan): the core never
calls eval() and never reads $GLOBALS/superglobals โ the only
sanctioned edge points are HtmlPhp\Input (explicit host-input helper) and
the WordPress\* adapters (which exist to talk to WordPress).
Documentation#
| Document | Contents |
|---|---|
| docs/TEMPLATE-SYNTAX.md | substitutions, filters, loops, conditionals, layouts, partials, trans blocks |
| docs/SSI.md | every directive, expression grammar, exec policies, security |
| docs/I18N.md | catalogues, plural rules, extraction, audit, WordPress delegation |
| docs/API.md | full class/method reference |
| docs/CONFIGURATION.md | every config key, bounds and default |
| docs/WORDPRESS.md | bridge, plugin, maintenance toolkit, WP-CLI |
| docs/CLI.md | bin/htmlphp commands |
| docs/SECURITY.md | threat model & hardening guide |
| docs/ARCHITECTURE.md | layering, pipeline, editions, decisions |
| docs/MIGRATION-4.x-TO-5.0.md | 4 โ 5 upgrade: new syntax, hardening checklist |
| docs/MIGRATION-3.x-TO-4.0.md | 1:1 mapping for every removed function |
| CHANGELOG.md | release history |
Development#
php8.3 tests/run.php # source tree
php8.5 tests/run.php --lib=php85/html.php # 8.5 bundle
bash tools/run-matrix.sh # full matrix (8.3/8.4/8.5 ร src/bundles)
php tools/build.php # regenerate both bundles
php tools/bench.php # throughput benchmark
CI (.github/workflows/ci.yml) runs the matrix,
a bundle-drift check (build.php --check), PHPStan and PHP-CS-Fixer.
Requirements#
PHP 8.3+ (8.5 edition: PHP 8.5+). No extensions required; mbstring
improves truncation/slugs, intl improves transliteration, apcu enables
the APCu cache driver.
License#
PolyForm Noncommercial 1.0.0 ยฉ Mikhail Deynekin โ see LICENSE.