Skip to content

Getting Started

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

Getting Started

Get from zero to a rendering, translating, SSI-processing page in five minutes. No Composer required (but supported).

Requirements

PHP 8.3+ (portable edition) · 8.5+ (modern edition)
Extensions none required — mbstring improves truncation/slugs, intl improves transliteration, apcu enables a cache driver
OS Windows / Linux / macOS

Installation

Option A — single file (shared hosting, FTP deploys)

Copy one file next to your code and require it:

require_once __DIR__ . '/lib/php83/html.php';   // PHP 8.3 / 8.4
// or
require_once __DIR__ . '/lib/php85/html.php';   // PHP 8.5+

The bundles are in php83/ and php85/. Each starts with a version guard: wrong PHP version = one clear message, not a parse-error mystery.

Option B — Composer

composer require paulmann/html-php      # PHP 8.3/8.4 — PSR-4 source
composer require paulmann/html-php85    # PHP 8.5 — modern bundle

Option C — WordPress

Install the bundled plugin (instructions) — it auto-selects the right edition.

Your first render

use HtmlPhp\Engine;

$engine = Engine::create([
    'templates' => ['paths' => [__DIR__ . '/templates']],
]);

echo $engine->render('hello.html', [
    'name'  => 'World <3',
    'rows'  => [['task' => 'Read docs', 'done' => true],
                ['task' => 'Ship v5',   'done' => false]],
]);
<!-- templates/hello.html -->
<h1>Hello %{name}!</h1>
<ul>
<!--@Loop rows-->
    <li <!--@If done-->class="done"<!--@EndIf-->>%{_position}. %{task}</li>
<!--@Else-->
    <li>@{todo.empty}</li>
<!--@EndLoop-->
</ul>

Rendered with the data above:

<h1>Hello World &lt;3!</h1>
<ul>
    <li class="done">1. Read docs</li>
    <li >2. Ship v5</li>
</ul>
  • %{name} was auto-escaped (<3 → &lt;3).
  • @{todo.empty} never ran — the list wasn't empty. When it is, that line becomes the translation of todo.empty (or the literal key, untranslated).
  • Ordinary HTML comments in templates are left untouched; only the <!--@Tag--> and <!--#ssi--> forms are special.

The six concepts that matter

  1. One engine, explicit config. Engine::create(array|Config, ?setup) builds an immutable, validated configuration and a lazy service container. No globals; two engines never share state. → Configuration
  2. Substitutions escape by default. %{var}, #{var}, @{translation_key}, filter chains %{var|upper|truncate:20}. Opt out per variable with |raw. Since v5 a tag can also compute: %{= done / total * 100 | round:1 }, with nested/dynamic keys (%{= rows[i].name}, %{= prices[currency]}, %{= get(fieldName)}). → Template Syntax · Expressions & Math
  3. Structure lives in HTML comments. @Loop, @If/@Elif/@Else, @Extends/@Section layouts, @Include partials, @Set, @Trans. Unbalanced tags are hard errors — run bin/htmlphp lint in CI.
  4. SSI runs last, over the assembled document — exactly like Apache would: <!--#include virtual="/footer.html" -->, #if, #echo, #set… #exec is denied unless you build an explicit policy. → SSI Guide
  5. i18n is a first-class citizen. PO/MO/JSON/PHP catalogues, plurals, contexts, fallback chains, and CLI extraction/audit. → I18n Guide
  6. Nothing fails silently. Misses land in $engine->diagnostics(), optionally forwarded to a logger; renders emit RenderStats. → Troubleshooting

Typical project layout

project/
├── lib/php85/html.php        # the single-file edition (or vendor/ via Composer)
├── templates/
│   ├── layouts/base.html     # <!--@Section …--> skeletons
│   ├── pages/…
│   └── parts/…               # @Include partials, SSI includes
├── languages/
│   ├── messages-de.po
│   └── messages-ru.json
├── config/htmlphp.php        # return [ … ] — feed to Engine::create / --config=
└── public/index.php

Wiring an application

use HtmlPhp\Engine;
use HtmlPhp\RequestContext;
use HtmlPhp\Template\RenderOptions;

$config = require __DIR__ . '/config/htmlphp.php';

$engine = Engine::create($config, function (HtmlPhp\Container $c): void {
    // swap any service: cache, logger, translator, clock, escaper…
    // $c->instance(Psr\SimpleCache\CacheInterface::class, $myCache);
});

// Build the request context ONCE at the edge — the engine never reads $_SERVER.
$request = RequestContext::fromServer($_SERVER);

echo $engine->render('pages/article.html', [
    'article' => $article,
], new RenderOptions(
    file: $_SERVER['SCRIPT_FILENAME'] ?? null,   // anchors SSI includes
    request: $request,                            // feeds DATE_LOCAL, HTTP_* …
    cache: true,
    ttl: 300,
));

Verify your setup

php bin/htmlphp version          # 5.0.0 (8.3 edition) · PHP 8.3.x
php bin/htmlphp lint templates/  # exit 0 = ship-able structure
php bin/htmlphp render templates/pages/article.html --var=title=Hi
php tools/bench.php 500          # throughput sanity check

Where to next?

You want… Go to
the complete template language Template Syntax
Apache SSI under PHP SSI Guide
translations with real tooling I18n Guide
WordPress: shortcodes, cache, admin, CLI WordPress Integration
every config key & bound Configuration Reference
to understand why it's shaped like this Problem Domain · Architecture
to upgrade a 3.x site Migration Guide

Tip

Runnable versions of everything above live in examples/ — bash examples/run-all.sh php8.3 walks through substitution, layouts, SSI and i18n against temp fixtures.

Clone this wiki locally