celema / boiler
A PHP template engine that doesn't require you to learn a new syntax
Requires
- php: ^8.5
- ext-mbstring: *
Requires (Dev)
- celema/dev: ^5.0
- symfony/html-sanitizer: ^8.0
Suggests
- symfony/html-sanitizer: Provides the built-in sanitize filter
Provides
None
Conflicts
None
Replaces
None
README
Boiler is a small template engine for PHP 8.5+, inspired by Plates. Like Plates, it uses native PHP as its templating language rather than introducing a custom syntax.
Key differences from Plates:
- Automatic escaping of strings and Stringable values for enhanced security
- Inherited render context across layouts, includes, components, and section captures; custom include or layout context merges on top and overrides duplicate keys
Other highlights:
- Layouts, includes, components, and sections, with concepts that map onto Blade's:
slot()prints what a template wraps, andyield()prints a section that any template can write, append to, or prepend to - Wrapper-driven escaping and a pluggable filter system for value transformations
- Custom template methods, including safe HTML helpers, and optional trusted classes
Installation
composer require celema/boiler
Install Symfony's HTML sanitizer when you want Boiler's built-in sanitize filter:
composer require symfony/html-sanitizer
Documentation
Start here: docs/index.md.
Topic overview
Quick start
Consider this example directory structure:
path
`-- to
`-- templates
`-- page.php
Create a template file at /path/to/templates/page.php with this content:
<p>ID <?= $id ?></p>
Then initialize the Engine and render your template:
use Celema\Boiler\Engine; $engine = Engine::create('/path/to/templates'); $html = $engine->render('page', ['id' => 13]); assert($html === '<p>ID 13</p>');
Common patterns
Render from multiple directories, optionally with namespaces:
$engine = Engine::create([ 'theme' => '/path/to/theme', 'app' => '/path/to/templates', ]); // Renders the first match (theme overrides app) $engine->render('page'); // Force a specific namespace $engine->render('app:page');
Control escaping:
$engine = Engine::create('/path/to/templates'); $engine->render('page'); $engine->renderUnescaped('page'); $engine = Engine::unescaped('/path/to/templates'); $engine->render('page'); $engine->renderEscaped('page');
Configure shared defaults and trusted classes:
$engine = Engine::create( '/path/to/templates', defaults: ['siteName' => 'Celema'], trusted: [TrustedHtml::class], );
Register custom template methods with method(). Pass safe: true when a helper returns safe HTML:
use function App\Template\icon; $engine = Engine::create('/path/to/templates') ->method('icon', icon(...), safe: true);
Methods are available as $this->icon() inside templates, includes, and layouts.
Register custom filters with the fluent filter() method:
use Celema\Boiler\Contract\Filter; $engine = Engine::create('/path/to/templates') ->filter('upper', new class implements Filter { public function apply(string $value, mixed ...$args): string { return strtoupper($value); } public function safe(): bool { return false; } });
Filters are available as virtual methods on wrapped string values in templates. In escaped renders, Boiler wraps string values for you. When you need filters on a raw value inside a template, call $this->wrap($value) first. Boiler ships with built-in lower, upper, stripTags, and trim filters, and registers sanitize automatically when symfony/html-sanitizer is installed.
For filter safety rules and advanced wrapper, filter, and escaper customization, see displaying values, engine, and template.
Template helpers available via $this inside templates:
$this->layout('layout'), and<?= $this->slot() ?>in the layout to print the page$this->include('partial', ['value' => '...'])$this->component('partial', ['value' => '...'])…$this->end()to pass the block in between, which the partial prints with<?= $this->slot() ?>$this->hasSlot()to check whether there is a slot to print$this->section('name'),$this->append('name'), or$this->prepend('name')…$this->end()to write a section$this->rewrite('name')…$this->end()to replace a section with content built on it, whichyield()returns inside the block<?= $this->yield('name', 'default') ?>to print a section; for a default made of markup, pass a closure that prints it, such asfn() => $this->include('partial'). With''as the default, the result is''when there is nothing to print$this->unwrap($value)when you need the original value instead of the escaped wrapper$this->escape($value)and$this->wrap($value)when you need proxy behavior such as string filters on a raw value
Wrapped values are proxy objects, and so are string keys in loops over wrapped arrays. === against a plain value is therefore always false, wrapped strings and arrays are truthy even when empty, and native string functions such as str_contains() silently operate on the escaped text. Compare and test through the proxy's predicate methods instead, which work on the raw value: $item->status->is(Status::Active), $status->in(['draft', 'pending']), $title->contains('&'), $url->startsWith('https://'), $file->endsWith('.pdf'), $slug->matches('/^[a-z0-9-]+$/'), and $tags->contains('featured') on wrapped arrays. See comparing wrapped values.
Error handling
Boiler fails fast on invalid lookups and render state, such as missing templates, invalid template names, duplicate layouts, sections captured twice, unclosed sections, missing slots, or unknown methods and filters. See rendering templates, layouts, sections, slots, and template for the exact rules.
Exceptions thrown while a template runs, including those from your own helpers and objects, arrive wrapped in RenderException with the template file and line. getPrevious() returns the original exception, and getCode() its code.
Benchmark
Boiler includes a benchmark in bench/ that renders three pages of a shop site with Boiler, Twig, Blade, and Plates. It is used mainly to catch performance regressions during development.
Run it with composer benchmark, which needs Docker, or on the machine itself with composer benchmark:native. For benchmark scope, caveats, and detailed usage, see bench/README.md.
Run the tests
composer test composer lint composer types composer docs:lint
For the PHP verification pipeline, run:
composer ci
composer ci:full additionally lints Markdown and requires Node (npx).
Mutation testing with Infection is not part of composer ci, but the CI workflow runs it after the coverage step and requires a mutation score of 100%. Pushes only mutate the changed lines; a weekly scheduled run covers the whole codebase. Run it locally with:
composer mutation
Reports are written to .infection/.
License
This project is licensed under the MIT license.