Skip to content
paulmannPublic

About

Single-file PHP 8.3+ template engine with Apache SSI directives, html4.pl-compatible API, XSS-safe variable substitution, hardened includes and zero dependencies - no Composer required.

Topics

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

HTML.PHP β€” Template Processing Library 🧩⚑

Version License PHP Dependencies Tests PHPStan Platform

Production-grade PHP 8.3+/8.5 template engine with full Apache SSI directive support (#if expressions, #exec policies, #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

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 &lt;World&gt; -->
<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

MIT Β© Mikhail Deynekin β€” see LICENSE.

About

Single-file PHP 8.3+ template engine with Apache SSI directives, html4.pl-compatible API, XSS-safe variable substitution, hardened includes and zero dependencies - no Composer required.

Topics

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages