A blazing fast, spec-compliant PHP implementation of Handlebars.
Originally based on LightnCandy, but rewritten to enable full Handlebars.js compatibility without excessive feature flags or performance tradeoffs.
PHP Handlebars compiles and executes complex templates up to 40% faster than LightnCandy:
| Library | Compile time | Runtime | Total time | Peak memory usage |
|---|---|---|---|---|
| LightnCandy 1.2.6 | 5.2 ms | 2.8 ms | 8.0 ms | 5.3 MB |
| PHP Handlebars 1.1 | 3.5 ms | 1.6 ms | 5.1 ms | 3.6 MB |
Tested on PHP 8.5 with the JIT enabled. See the benchmark branch to run the same test.
- Supports all Handlebars syntax and language features, including expressions, subexpressions, helpers,
partials, hooks,
@datavariables, whitespace control, and.lengthon arrays. - Templates are parsed using PHP Handlebars Parser, which implements the same lexical analysis and AST grammar specification as Handlebars.js.
- Tested against the full Handlebars.js spec.
composer require devtheorem/php-handlebars
use DevTheorem\Handlebars\Handlebars;
$template = Handlebars::compile('Hello {{name}}!');
echo $template(['name' => 'World']); // Hello World!Templates can be pre-compiled to native PHP for later execution:
use DevTheorem\Handlebars\Handlebars;
$code = Handlebars::precompile('<p>{{org.name}}</p>');
// save the compiled code into a PHP file
file_put_contents('render.php', "<?php $code");
// later import the template function from the PHP file
$template = require 'render.php';
echo $template(['org' => ['name' => 'DevTheorem']]);You can alter the template compilation by passing an Options instance as the second argument to compile or precompile.
For example, the strict option may be set to true to generate a template which will throw an exception for missing data:
use DevTheorem\Handlebars\{Handlebars, Options};
$template = Handlebars::compile('Hi {{first}} {{last}}!', new Options(
strict: true,
));
echo $template(['first' => 'John']); // Error: "last" not defined-
knownHelpers: Associative array (helperName => bool) of helpers that will be registered at runtime. The compiler uses this to emit direct helper calls instead of dynamic dispatch, which is faster and required whenknownHelpersOnlyis set. Built-in helpers (if,unless,each,with,lookup,log) are pre-populated astrueand may be excluded by setting them tofalse. Settingiforunlesstofalsealso disables the inline ternary optimization and allows those helpers to be overridden at runtime. -
knownHelpersOnly: Restricts templates to only the helpers inknownHelpers, enabling further compile-time optimizations: block sections and bare{{identifier}}expressions skip the runtime helper table and use a direct context lookup, and any use of an unregistered helper throws a compile-time exception instead of falling back to dynamic dispatch. -
noEscape: Set totrueto disable HTML escaping of output. -
strict: Run in strict mode. In this mode, templates will throw rather than silently ignore missing fields. This has the side effect of disabling inverse operations such as{{^foo}}{{/foo}}unless fields are explicitly included in the source object. -
assumeObjects: A looser alternative tostrictmode. A null intermediate in a path (e.g.foois null when resolvingfoo.bar) throws an exception, but a missing terminal key returns null silently. -
preventIndent: Prevents an indented partial call from indenting the entire partial output by the same amount. -
ignoreStandalone: Disables standalone tag removal. When set, blocks and partials that are on their own line will not remove the whitespace on that line. -
explicitPartialContext: Disables implicit context for partials. When enabled, partials that are not passed a context value will execute against an empty object. -
partials: An associative array of custom partial template strings (name => template). -
partialResolver: A closure that will be called at compile time for any partial not found in thepartialsarray, and should return a template string for it.
Handlebars::compile returns a closure which can be invoked as $template($context, $options).
The $options parameter takes an array of runtime options, accepting the following keys:
-
data: An associative array of initial@datavariables (e.g.['version' => '1.0']makes@versionavailable in the template). -
helpers: Anarray<string, \Closure>of helpers to merge with the built-in helpers. Can also be used to override a built-in helper by using the same name. -
partials: Anarray<string, \Closure>of partial closures precompiled withHandlebars::compile. Useful when multiple templates share the same partials, and you want to avoid recompiling them for each template.
Helper functions will be passed any arguments provided to the helper in the template.
If needed, a final $options parameter can be included which will be passed a HelperOptions instance.
For example, a custom #equals helper with JS equality semantics could be implemented as follows:
use DevTheorem\Handlebars\{Handlebars, HelperOptions};
$template = Handlebars::compile('{{#equals my_var false}}Equal to false{{else}}Not equal{{/equals}}');
$helpers = [
'equals' => function (mixed $a, mixed $b, HelperOptions $options) {
// In JS, null is not equal to blank string or false or zero,
// and when both operands are strings no coercion is performed.
$equal = ($a === null || $b === null || is_string($a) && is_string($b))
? $a === $b
: $a == $b;
return $equal ? $options->fn() : $options->inverse();
},
];
$runtimeOptions = ['helpers' => $helpers];
echo $template(['my_var' => 0], $runtimeOptions); // Equal to false
echo $template(['my_var' => 1], $runtimeOptions); // Not equal
echo $template(['my_var' => null], $runtimeOptions); // Not equal-
name(readonlystring): The helper name as it appeared in the template. Useful inhelperMissing/blockHelperMissinghooks to identify which name was called. -
hash(readonlyarray): Key/value pairs passed as hash arguments in the template (e.g.{{helper foo=1 bar="x"}}produces['foo' => 1, 'bar' => 'x']). -
blockParams(readonlyint): The number of block parameters declared by the helper call (e.g.{{#helper as |a b|}}produces2). -
scope(mixed): The current evaluation context (equivalent tothisin a Handlebars.js helper). Can be reassigned inside a helper to change the context passed tofn(). -
data(array): The current@dataframe. Contains@-prefixed private variables such asroot,index,key,first,last, and_parent. Can be read or modified inside a helper.
-
fn(mixed $context = <current scope>, mixed $data = null): string: Renders the block body. Pass a new context as$contextto change what the block renders against (equivalent tooptions.fn(newContext)in JS). Pass a$dataarray with a'data'key to inject additional@-prefixed variables into the block, and/or a'blockParams'key containing an array of values to expose as block parameters. -
inverse(mixed $context = null, mixed $data = null): string: Renders the{{else}}/ inverse block. Returns an empty string if no inverse block was provided. Accepts the same optional$contextand$dataarguments asfn(). -
hasPartial(string $name): bool: Returnstrueif a partial with the given name is registered. Useful alongsideregisterPartial()to implement lazy partial loading. -
registerPartial(string $name, \Closure $partial): void: Registers a compiled partial closure for the remainder of the render. The closure must be produced byHandlebars::compile.
Note
isset($options->fn) and isset($options->inverse) return true if the helper was called as a block,
and false for inline helper calls.
If a custom helper named helperMissing is defined, it will be called when a mustache or a block-statement
is not a registered helper AND is not a property of the current evaluation context.
If a custom helper named blockHelperMissing is defined, it will be called when a block-expression calls
a helper that is not registered, even when the name matches a property in the current evaluation context.
For example:
use DevTheorem\Handlebars\{Handlebars, HelperOptions};
$template = Handlebars::compile('{{foo 2 "value"}}
{{#person}}{{firstName}} {{lastName}}{{/person}}');
$helpers = [
'helperMissing' => function (...$args) {
$options = array_pop($args);
return "Missing {$options->name}(" . implode(',', $args) . ')';
},
'blockHelperMissing' => function (mixed $context, HelperOptions $options) {
return "'{$options->name}' not found. Printing block: {$options->fn($context)}";
},
];
$data = ['person' => ['firstName' => 'John', 'lastName' => 'Doe']];
echo $template($data, ['helpers' => $helpers]);Output:
Missing foo(2,value)
'person' not found. Printing block: John Doe
If a custom helper is executed in a {{ }} expression, the return value will be HTML escaped.
When a helper is executed in a {{{ }}} expression, the original return value will be output directly.
Helpers may return a DevTheorem\Handlebars\SafeString instance to prevent escaping the return value.
When constructing the string that will be marked as safe, any external content should be properly escaped
using the Handlebars::escapeExpression() method to avoid potential security concerns.
All syntax and language features from Handlebars.js 4.7.9 should work the same in PHP Handlebars, with the following exceptions:
- Custom Decorators have not been implemented, as they are deprecated in Handlebars.js.
- The
dataandcompatcompilation options have not been implemented. - The runtime options to control prototype access,
along with the
lookupProperty()helper option method have not been implemented, since they aren't relevant for PHP.