Skip to content

Templating

A TemplateEngine contract with three implementations — PhpEngine (native PHP templates with inheritance and blocks), TwigEngine (wraps Twig), and DelegatingEngine (routes by file extension). View helpers are injectable via the TemplateHelper contract.

Application\Templating
├── TemplateEngine (interface)
├── TemplateHelper (interface)
└── Exception\
    ├── TemplatingException
    ├── TemplateNotFoundException
    └── DuplicateHelperException

Adapter\Templating
├── PhpEngine             — native PHP with extends/blocks
├── TwigEngine            — Twig adapter
└── DelegatingEngine      — routes to sub-engines by supports()

Table of Contents

  1. TemplateEngine Interface
  2. TemplateHelper Interface
  3. PhpEngine
  4. TwigEngine
  5. DelegatingEngine
  6. Exceptions

TemplateEngine Interface

Fight\Common\Application\Templating\TemplateEngine

interface TemplateEngine
{
    public function render(string $template, array $data = []): string;
    public function exists(string $template): bool;
    public function supports(string $template): bool;
    public function addHelper(TemplateHelper $helper): void;
    public function hasHelper(TemplateHelper $helper): bool;
}
  • render() — evaluates the template with the given data, returns the output string
  • exists() — checks whether the template can be resolved
  • supports() — checks whether this engine can handle the template (typically by file extension)
  • addHelper() / hasHelper() — manage named view helpers

Throws TemplatingException on render failure.


TemplateHelper Interface

Fight\Common\Application\Templating\TemplateHelper

interface TemplateHelper
{
    public function getName(): string;
}

Helpers are identified by name and registered on an engine. Each implementation retrieves the helper by name and makes it available in the template context. A helper can provide any number of public methods for use in templates.

use Fight\Common\Application\Templating\TemplateHelper;

final class AssetHelper implements TemplateHelper
{
    public function getName(): string
    {
        return 'asset';
    }

    public function path(string $name): string
    {
        return '/assets/' . $name;
    }
}

Registered on any engine:

$engine->addHelper(new AssetHelper());

PhpEngine

Fight\Common\Adapter\Templating\PhpEngine

A full native PHP template engine with template inheritance, a block system, HTML escaping, and name-based helper access. No external dependencies.

Construction

use Fight\Common\Adapter\Templating\PhpEngine;

$engine = new PhpEngine(
    paths: ['/var/www/templates', '/var/www/vendor/templates'],
    helpers: [new AssetHelper()]
);

Paths are searched in order. The colon separator in template names is converted to DIRECTORY_SEPARATOR: Controller:action.phpController/action.php.

Rendering

$engine->render('Controller:action.php', ['name' => 'Alice']);

The data array is extracted into the template scope. The key this is reserved and throws TemplatingException if present.

Template Inheritance

A child template declares its parent with $this->extends():

<!-- Controller/action.php -->
<?php $this->extends('Layout:base.php'); ?>

<?php $this->startBlock('content'); ?>
<h1>Hello, <?= $this->escape($name) ?></h1>
<?php $this->endBlock(); ?>
<!-- Layout/base.php -->
<!DOCTYPE html>
<html>
<body>
<?php $this->outputContent('content', 'Default content'); ?>
</body>
</html>

extends() must be called at the top of the template. The engine resolves the parent chain recursively — a parent can itself extend another template.

Block System

Method Purpose
startBlock(string $name) Begins capturing output into a named block
endBlock() Stops capturing, stores content (first-definition-wins)
hasBlock(string $name) Checks if a block is defined
setContent(string $name, string $content) Overwrites block content programmatically
getContent(string $name, ?string $default): ?string Retrieves block content
outputContent(string $name, ?string $default): bool Echoes block content

First-definition-wins semantics: When a child overrides a block, the child's content is used. If the child does not override, the parent's content (set via startBlock/endBlock in the parent) persists. This is enforced by endBlock() only storing content when $this->blocks[$name] is empty.

Escaping

$this->escape($userInput);   // htmlspecialchars with ENT_QUOTES | ENT_SUBSTITUTE, UTF-8

Helper Access

$this->has('asset');         // bool
$this->get('asset');         // TemplateHelper instance
$this->get('asset')->path('style.css');

Throws TemplatingException if the helper is not registered.

Template Loading & Caching

Templates are resolved to absolute file paths on first access and cached internally. loadTemplate()getTemplatePath() iterates the configured paths and returns the first readable file match. Throws TemplateNotFoundException if no path matches.

$engine->exists('Controller:action.php');     // checks all paths

TwigEngine

Fight\Common\Adapter\Templating\TwigEngine

Wraps a Twig Environment. Supports .twig templates.

use Fight\Common\Adapter\Templating\TwigEngine;
use Twig\Environment;
use Twig\Loader\FilesystemLoader;

$loader = new FilesystemLoader('/var/www/templates');
$twig = new Twig\Environment($loader);

$engine = new TwigEngine($twig);
Method Behavior
render() Delegates to $environment->render(), wraps Twig errors in TemplatingException
exists() Delegates to $environment->getLoader()->exists()
supports() Returns true for templates ending in .twig
addHelper() Stores the helper and adds it as a Twig global ($environment->addGlobal($name, $helper))
$engine->addHelper(new AssetHelper());
// In Twig: {{ asset.path('style.css') }}

DelegatingEngine

Fight\Common\Adapter\Templating\DelegatingEngine

Routes templates to sub-engines based on the supports() check. Useful when a project uses multiple template formats.

use Fight\Common\Adapter\Templating\DelegatingEngine;
use Fight\Common\Adapter\Templating\PhpEngine;
use Fight\Common\Adapter\Templating\TwigEngine;

$engine = new DelegatingEngine([
    new PhpEngine(['/var/www/templates']),
    new TwigEngine($environment),
]);

// Routes by file extension
$engine->render('page.php');          // → PhpEngine
$engine->render('page.html.twig');    // → TwigEngine

Helper Injection

Helpers registered on the DelegatingEngine are not immediately forwarded to sub-engines. Instead, they are stored locally and lazily injected into the resolved sub-engine at render() time:

$engine->addHelper(new AssetHelper());
// On render: $resolvedEngine->addHelper($helper) is called for each stored helper

This means sub-engines only receive helpers when they actually render, and each sub-engine gets all the delegates' helpers.

Routing

getEngine() iterates sub-engines in order and returns the first match:

$engine->supports('page.php');       // true (PhpEngine supports .php)
$engine->exists('page.php');         // false if no path can resolve it

Throws TemplatingException if no engine supports() the template.


Exceptions

Fight\Common\Application\Templating\Exception

Exception Extends Purpose
TemplatingException SystemException Base for all templating errors
TemplateNotFoundException TemplatingException Template file could not be resolved
DuplicateHelperException TemplatingException Two helpers registered with the same name
throw TemplateNotFoundException::fromName('Controller:missing.php');
// "Template not found: Controller:missing.php"

throw DuplicateHelperException::fromName('asset');
// "Duplicate helper: asset"
  • TemplateNotFoundException::getTemplate(): ?string
  • DuplicateHelperException::getName(): ?string