tandrezone / ztemp
A lightweight, secure PHP template engine supporting {{ $variable }}, @include(), and @foreach()/@endforeach directives.
Requires
- php: ^8.1
Requires (Dev)
- phpunit/phpunit: ^11.0
This package is not auto-updated.
Last update: 2026-05-15 11:04:42 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. @includeis path-traversal safe — files outside the configured base directory are rejected.- Circular
@includechains 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 }}. @includeresolves 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
$paramsarray. - The
@foreachbody is part of the template (trusted), not the data. Only{{ $item }}/{{ $item.key }}placeholders inside the body are data-driven and escaped.
License
MIT