tandrezone/ztemp

A lightweight, secure PHP template engine supporting {{ $variable }}, @include(), and @foreach()/@endforeach directives.

Maintainers

Package info

github.com/tandrezone/ztemp

pkg:composer/tandrezone/ztemp

Statistics

Installs: 7

Dependents: 1

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.3 2026-05-15 11:02 UTC

README

A lightweight, secure PHP template engine for processing plain HTML files.

Features

Directive Syntax Description
Variable output {{ $name }} Inserts a parameter value (HTML-escaped)
Include @include(path/to/partial.html) Embeds another template
Loop @foreach($items) … @endforeach Iterates over an array parameter
Loop (alias) @foreach($items as $item) … @endforeach Iterates with a custom item alias
Loop (key => value) @foreach($items as $k => $v) … @endforeach Iterates with key and value aliases

Security built-in

  • All variable output is HTML-escaped (htmlspecialchars) — no raw HTML injection.
  • @include is path-traversal safe — files outside the configured base directory are rejected.
  • Circular @include chains are detected and stopped (max depth: 10).
  • Null bytes in template paths are rejected.

Requirements

  • PHP 8.1 or higher

Installation

composer require tandrezone/ztemp

Quick Start

<?php

use Tandrezone\Ztemp\TemplateEngine;

$engine = new TemplateEngine(__DIR__ . '/templates');

$html = $engine->render('page.html', [
    'title' => 'Hello World',
    'name'  => 'Alice',
]);

echo $html;

Template Syntax

Variables — {{ $name }}

Use double curly braces to output a parameter. The value is automatically HTML-escaped.

<!-- templates/greeting.html -->
<p>Hello, {{ $name }}!</p>
<p>You have {{ $count }} messages.</p>
$engine->render('greeting.html', [
    'name'  => 'Bob',
    'count' => 3,
]);
// → <p>Hello, Bob!</p>
//   <p>You have 3 messages.</p>

Missing variables are silently removed from the output (no placeholder leaks).

Include — @include(path)

Embed one template inside another. The path is relative to the engine's base directory.

<!-- templates/layout.html -->
<!DOCTYPE html>
<html>
<head><title>{{ $title }}</title></head>
<body>
@include(header.html)
<main>{{ $body }}</main>
</body>
</html>
<!-- templates/header.html -->
<header><h1>{{ $title }}</h1></header>

All parameters are automatically forwarded to included templates.

Security note: Paths containing ../ or absolute paths (e.g. /etc/passwd) that resolve outside the base directory will throw a RuntimeException.

Foreach — @foreach($param) … @endforeach

Iterate over an array parameter. Three styles are supported.

Default style (implicit $item alias)

<!-- templates/list.html -->
<ul>
@foreach($items)
  <li>{{ $item }}</li>
@endforeach
</ul>
$engine->render('list.html', [
    'items' => ['apple', 'banana', 'cherry'],
]);

Custom item alias — @foreach($items as $alias)

<!-- templates/list.html -->
<ul>
@foreach($items as $fruit)
  <li>{{ $fruit }}</li>
@endforeach
</ul>

Key => value aliases — @foreach($items as $key => $val)

Use this form to access the array key alongside the value, or to iterate over associative arrays with named fields.

<!-- templates/attributes.html -->
<dl>
@foreach($attributes as $k => $v)
  <dt>{{ $k }}</dt>
  <dd>{{ $v }}</dd>
@endforeach
</dl>
$engine->render('attributes.html', [
    'attributes' => ['color' => 'red', 'size' => 'large'],
]);

Associative / object array (default alias)

<!-- templates/users.html -->
<table>
@foreach($users)
  <tr>
    <td>{{ $item.name }}</td>
    <td>{{ $item.email }}</td>
  </tr>
@endforeach
</table>

The same works with a custom alias or key=>value syntax:

@foreach($users as $user)
  <p>{{ $user.name }}</p>
@endforeach

@foreach($users as $idx => $user)
  <p>{{ $idx }}: {{ $user.name }}</p>
@endforeach
$engine->render('users.html', [
    'users' => [
        ['name' => 'Alice', 'email' => 'alice@example.com'],
        ['name' => 'Bob',   'email' => 'bob@example.com'],
    ],
]);

If the referenced variable does not exist or is not an array the block is removed from the output.

Nested foreach

@foreach blocks can be nested to any depth.

<!-- templates/catalog.html -->
@foreach($categories)
<h2>{{ $item.name }}</h2>
<ul>
@foreach($tags)
  <li>{{ $item }}</li>
@endforeach
</ul>
@endforeach

When the outer item is an associative array, its fields are also available to the inner @foreach by their key name, so an inner loop can iterate over a sub-array field of the current outer element:

@foreach($users)
<p>{{ $item.name }}</p>
<ul>
@foreach($skills)
  <li>{{ $item }}</li>
@endforeach
</ul>
@endforeach
$engine->render('users.html', [
    'users' => [
        ['name' => 'Alice', 'skills' => ['PHP', 'JS']],
        ['name' => 'Bob',   'skills' => ['Python']],
    ],
]);

API Reference

TemplateEngine::__construct(string $basePath = '')

Parameter Description
$basePath Absolute path to the directory that contains your templates. When empty, template paths are treated as-is (absolute paths allowed).

Throws RuntimeException if the supplied base path does not exist.

TemplateEngine::render(string $templatePath, array $params = []): string

Parameter Description
$templatePath Path to the template file, relative to $basePath.
$params Associative array of parameters available inside the template.

Returns the fully rendered string. Throws RuntimeException on file-not-found, path traversal, or excessive include depth.

Example

See examples/index.php for a complete working demo:

php examples/index.php

Running Tests

composer install
./vendor/bin/phpunit

Security Considerations

What is safe

  • Variable values are always HTML-escaped — you cannot inject raw HTML or JavaScript through {{ $var }}.
  • @include resolves paths against the configured base directory and rejects any path that resolves outside it.

What to watch out for

  • Do not put untrusted content directly in template files — only pass untrusted data through the $params array.
  • The @foreach body is part of the template (trusted), not the data. Only {{ $item }} / {{ $item.key }} placeholders inside the body are data-driven and escaped.

License

MIT