Skip to content

Migration 3.x to 4.0

Mikhail Deynekin edited this page Oct 11, 2026 · 1 revision

Migration 3.x β†’ 4.0

Version 4 is a clean break: the legacy html4.pl API is gone, global configuration is gone, and defaults changed where security demanded it. Budget: a day for a typical site, a sprint for a large estate β€” most of it is mechanical, and the linter finds the rest.

Note

Canonical spec (complete tables): docs/MIGRATION-3.x-TO-4.0.md.

TL;DR

// v3
require 'html.php';
$GLOBALS['HTML_CACHE_DIR'] = '/var/cache/htmlphp';
$GLOBALS['HTML_ESCAPE_VARS'] = true;
echo html_render($template, $vars, ['cache' => true]);

// v4
require 'php83/html.php';
$engine = HtmlPhp\Engine::create([
    'cache' => ['enabled' => true, 'driver' => 'file', 'directory' => '/var/cache/htmlphp'],
]);   // auto-escape is ON by default now
echo $engine->renderString($template, $vars, new HtmlPhp\Template\RenderOptions(cache: true));

Function map (the 3.x modern API)

v3 v4
html_render($tpl, $vars, $opts) $engine->renderString($src, $vars, new RenderOptions(...))
html_render_file($file, …) $engine->renderFile($file, …) / $engine->render($name)
html_ssi($html, $file) $engine->processSsi($html, new DocumentInfo(file: $file))
html_t/n/x/nx $engine->t/n/x/nx
html_set_locale/get_locale $engine->setLocale()/locale()
html_detect_locale($avail, $header) Locale::negotiate($avail, $request->acceptLanguage)
html_load_translations i18n.paths autoload / $engine->loadTranslations()
html_add_filter/apply_filters/add_action/do_action $engine->events()->on/filter/dispatch (tags: htmlphp_render β†’ htmlphp.render)
html_var_filter($name, $fn) $engine->filters()->add($name, $fn)
html_escape/attr/url $engine->escaper()->html/attr/url
html_slugify/truncate Support\Str::slugify/truncate
html_errors()/html_clear_errors() $engine->diagnostics()->all()/clear()
html_cache_clear() $engine->clearCache()
html_config($overrides) Engine::create($config) / $config->merge() (no runtime mutation)
html_version() $engine->version()
html_reset_state() drop the engine instance (no shared state exists)

Removed legacy API (html4.pl) β€” closest equivalents

HTML() processHtml() update_template() LoadTemplate() LoadVar() Read_HTML() Show_HTML() Divide() lc_Tags() normalize() clean() clean_html() WriteFile() TrimSpaces() sanitizeXSS() HTML_Sanitized_Input() _html*() wrappers, HTML_RESERVED_GLOBALS, HTML_SSI_BUILTIN_VARS β€” gone.

Legacy v4
Divide Str::divide() / |div: filter / / in expressions
lc_Tags Β· normalize Β· clean_html Β· TrimSpaces Html\Cleaner::lcTags/normalize/cleanHtml/trimSpaces
WriteFile Io\AtomicWriter::write()
sanitizeXSS Β· HTML_Sanitized_Input Str::sanitizeRecursive() Β· Input::all()
LoadTemplate/Show_HTML blocks @Loop / @Section layouts / @Include partials

Template syntax

Kept: %{NAME} Β· #{NAME} Β· @{KEY} Β· filter chains Β· <!--@Loop rows--> (+ _index/_position/_key/_first/_last/_count/_odd) Β· <!--@If-->/<!--@Else--> Β· all SSI directives.

Removed β†’ replaced:

v3 v4
%[NAME], %{%{NAME}} unnecessary β€” deterministic pipeline
%{K.Round:2} %{K.Clean Round:2} %{K.Div D} %{K|round:2} %{K|cleanround:2} %{K|div:D}
<!--@Block--> <!--@Split--> PARITY @Loop with _odd/_even, or @Section layouts
<!--##echo-->, <!--@var=…--> standard <!--#echo-->, <!--#set-->

Added: @Elif @Unless @Extends/@Section/@SectionParent @Include(with/only/ignore-missing) @Set @Trans/@Plural @Raw @Comment @Break/@Continue, loop alias/k,v/range forms, @Else for loops, expression conditions, loop.*, 18 new filters, SSI #printenv #block+stub= encoding="base64", custom directives.

Behaviour changes that will bite (read all ten)

  1. Auto-escape ON by default. Pre-escaped inputs need |raw (or flip syntax.auto_escape during migration β€” don't ship it off).
  2. Translate.KEY globals fallback gone β†’ catalogues or scope vars.
  3. #set no longer writes $GLOBALS β€” render-scoped only.
  4. SSI includes refuse denied extensions (.php, .env, dotfiles…) β€” v3 leaked sources; scripts belong to #exec cgi or custom directives.
  5. Single deterministic pipeline replaces passes; the SSI re-substitution pass is automatic.
  6. @Loop over a missing var renders @Else + one diagnostic.
  7. Unbalanced/unknown tags throw (v3: diagnose+skip) β†’ lint in CI first.
  8. WP TemplateLoader never locates/renders .php.
  9. fr/pt/kab plural rules fixed (n > 1) β€” remove any workaround.
  10. Diagnostics entries carry time; cap unchanged.

Config key map

Full table: docs/MIGRATION §6. Shape of it: HTML_MAX_SSI_DEPTH → limits.max_ssi_depth · HTML_SSI_EXEC → ssi.exec_mode (true→allow-list) · HTML_ESCAPE_VARS → syntax.auto_escape (default flipped) · HTML_MISSING_VAR → syntax.missing_var · HTML_I18N_* → i18n.* · HTML_CACHE_DIR/TTL → cache.directory/ttl (+enabled) · HTML_WWW_PATH → ssi.document_root/RenderOptions::$root · HTML_ALLOWED_TAGS → security.allowed_tags · HTML_DEBUG → debug (+syntax.strict). HTML_MAX_TEMPLATE_PASSES and HTML_AUTO_SANITIZE_INPUT have no successor by design.

Hooks

htmlphp_render β†’ htmlphp.render (filter) Β· htmlphp_error β†’ htmlphp.error Β· htmlphp_translate β†’ htmlphp.translate Β· htmlphp_filter_apply β†’ htmlphp.filter.apply Β· htmlphp_ssi_* β†’ htmlphp.ssi.*. Under the WP bridge the old underscore names keep working (events are mirrored onto apply_filters('htmlphp_render') etc.).

WordPress

Bridge::boot() now returns the Engine and takes (Config|array, ?setup, options); TemplateLoader/Shortcodes are constructed with the engine instead of using statics + globals; Health::check($engine) / SystemInfo::collect($engine) accept the engine for live stats. Maintenance toolkit APIs are otherwise unchanged.

Suggested order

  1. bin/htmlphp lint templates/ on the old tree β€” inventory removed syntax (%[…], numeric directives, @Block).
  2. Rewrite PHP call sites per the function map (mostly html_* β†’ $engine->*).
  3. Fold $GLOBALS['HTML_*'] assignments into one Engine::create([...]) array.
  4. Escape audit: render key templates with auto_escape off/on, diff, add |raw where HTML is intentional.
  5. i18n:extract + i18n:audit per locale (catches the French-plural fix fallout too).
  6. Turn on CI: matrix + lint gate + drift check. Ship. πŸš€

Clone this wiki locally