Repository files navigation

PHP Handlebars

A blazing fast, spec-compliant PHP implementation of Handlebars.

The syntax of Handlebars is generally a superset of Mustache, so in most cases it is possible to swap out Mustache for Handlebars and continue using the same templates.

Features

  • Supports all Handlebars syntax and language features, including expressions, subexpressions, helpers, partials, hooks, @data variables, whitespace control, and .length on arrays and strings.
  • Arrays and objects can both be used as context values. {{#each}} over an object iterates its public properties.
  • Templates are parsed using PHP Handlebars Parser, which implements the same lexical analysis and AST grammar specification as Handlebars.js.
  • Tested against the Handlebars.js spec and the Mustache spec.

Performance

PHP Handlebars started as a fork of LightnCandy, but has been rewritten with an AST-based parser and optimized runtime to enable full Handlebars.js compatibility with better performance.

PHP Handlebars compiles and executes complex templates over 40% faster than LightnCandy, with 60% lower memory usage:

LibraryCompile timeRuntimeTotal timePeak memory usage
LightnCandy 1.2.65.0 ms2.4 ms7.4 ms5.3 MB
PHP Handlebars 2.12.8 ms1.4 ms4.2 ms1.8 MB

Tested on PHP 8.5 with the JIT enabled. See the benchmark branch to run the same test.

Installation

composer require devtheorem/php-handlebars

Usage

useDevTheorem\Handlebars\Handlebars;
$source = <<<'HBS' <p>Hi {{user.name}}, you have {{notifications.length}} new notification(s):</p> <ul> {{#notifications}} <li>{{count}} {{message}} ({{time}})</li> {{/notifications}} </ul>
HBS;
$data = [
'user' => ['name' => 'Jane'],
'notifications' => [
['count' => 4, 'message' => 'new comments', 'time' => '5 min ago'],
['count' => 3, 'message' => 'new followers', 'time' => '1 hr ago'],
],
];
$template = Handlebars::compile($source);
echo$template($data);

Output:

<p>Hi Jane, you have 2 new notification(s):</p><ul><li>4 new comments (5 min ago)</li><li>3 new followers (1 hr ago)</li></ul>

Precompilation

Templates and partials can be precompiled to native PHP for later execution, avoiding the overhead of parsing and compilation on each request.

Build step - compile all templates in a directory and cache the generated PHP:

useDevTheorem\Handlebars\Handlebars;
$templateDir = 'templates';
$cacheDir = 'templateCache';
foreach (glob("$templateDir/*.hbs") ?: [] as$file) {
$name = basename($file, '.hbs');
$code = Handlebars::precompile(file_get_contents($file));
file_put_contents("$cacheDir/$name.php", "<?php $code");
}

Runtime - load only needed templates, with precompiled partials resolved on demand:

$template = require'templateCache/page.php';
$data = ['title' => 'My Page', 'user' => ['name' => 'Jane']];
echo$template($data, [
'partialResolver' => fn(string$name) => require"templateCache/$name.php",
]);

Each {{> partial}} call triggers the resolver on first use, and the result is cached for the rest of that render. Only the partials that the page actually references are ever loaded.

Important

Precompiled templates must be regenerated whenever PHP Handlebars is updated, as the generated PHP code depends on the current version of the runtime. The build step above should be part of a deployment process so that precompiled output does not need to be committed to source control.

Compile Options

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:

useDevTheorem\Handlebars\{Handlebars, Options};
$template = Handlebars::compile('Hi {{first}} {{last}}!', newOptions(
strict: true,
));
echo$template(['first' => 'John']); // Error: "last" not defined

Available Options

  • compat: Set to true to enable recursive field lookup. If a template variable is not found in the current scope, it will automatically be looked up in parent scopes, matching Mustache's default behavior.

Note

Recursive lookup has a runtime cost, so it is recommended that performance-sensitive operations should avoid compat mode and instead opt for explicit path references.

  • 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 when knownHelpersOnly is set. Built-in helpers (if, unless, each, with, lookup, log) are pre-populated as true and may be excluded by setting them to false. Setting if or unless to false also disables the inline ternary optimization and allows those helpers to be overridden at runtime.

  • knownHelpersOnly: Restricts templates to only the helpers in knownHelpers, 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 unknown helper throws a compile-time exception instead of falling back to dynamic dispatch.

  • noEscape: Set to true to 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 to strict mode. A null intermediate in a path (e.g. foo is null when resolving foo.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.

Runtime Options

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 custom @data variables (e.g. ['version' => '1.0'] makes @version available in the template).

  • helpers: An array<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: An array<string, Closure> of partials compiled with Handlebars::compile. Useful for eagerly providing a known set of partials.

  • partialResolver: A Closure(string $name): ?Closure called lazily when a partial is referenced but not found in the partials map. Should return a compiled partial closure, or null if the partial does not exist. The resolved closure is cached for the remainder of the render, so each partial is loaded at most once per template invocation.

Custom Helpers

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:

useDevTheorem\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 falseecho$template(['my_var' => 1], $runtimeOptions); // Not equalecho$template(['my_var' => null], $runtimeOptions); // Not equal

HelperOptions Properties

  • name (readonly string): The helper name as it appeared in the template. Useful in helperMissing/blockHelperMissing hooks to identify which name was called.

  • hash (readonly array): Key/value pairs passed as hash arguments in the template (e.g. {{helper foo=1 bar="x"}} produces ['foo' => 1, 'bar' => 'x']).

  • blockParams (readonly int): The number of block parameters declared by the helper call (e.g. {{#helper as |a b|}} produces 2).

  • scope (mixed): The current evaluation context (equivalent to this in a Handlebars.js helper).

  • data (array): The current @data frame. The root key refers to the top-level context. index, key, first, and last are set by {{#each}} blocks. Can be read or modified inside a helper.

HelperOptions Methods

  • fn(mixed $context = <current scope>, mixed $data = null): string: Renders the block body. Pass a new context as $context to change what the block renders against (equivalent to options.fn(newContext) in JS). Pass a $data array with a 'data' key to inject @-prefixed variables into the block, and/or a 'blockParams' key containing an array of values to expose as block parameters.

  • inverse(mixed $context = <current scope>, mixed $data = null): string: Renders the {{else}} / inverse block. Returns an empty string if no inverse block was provided. Accepts the same optional $context and $data arguments as fn().

  • lookupProperty(mixed $parent, string|int $key): mixed: Reads a property from $parent without the helper needing to know whether it's an array or an object, returning $parent[$key] for arrays, $parent->$key for objects, and null for a missing key or any other value.

  • hasPartial(string $name): bool: Returns true if a partial with the given name is registered. Useful alongside registerPartial() to implement dynamic partial loading.

  • registerPartial(string $name, Closure $partial): void: Registers a compiled partial closure for the remainder of the render. The closure can be produced via Handlebars::compile, or by importing a cached closure created with Handlebars::precompile.

Note

isset($options->fn) and isset($options->inverse) return true if the helper was called as a block, and false for inline helper calls.

Hooks

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:

useDevTheorem\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

String Escaping

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. Because SafeString bypasses the automatic HTML escaping that {{ }} applies, any user-supplied content embedded in it must first be escaped with Handlebars::escapeExpression() to prevent XSS vulnerabilities.

Data Frames

Block helpers that inject @-prefixed variables should create a child data frame using Handlebars::createFrame($options->data), add their variables to it, and pass it to fn() or inverse() via the data key (e.g. $options->fn($context, ['data' => $frame])). This mirrors Handlebars.createFrame() in Handlebars.js, isolating the helper's variables while still inheriting parent data such as @root.

Missing Features

All syntax and language features from Handlebars.js 4.7.9 should work the same in PHP Handlebars, with the following exceptions:

Mustache Compatibility

Handlebars is largely compatible with Mustache syntax, with a few notable differences:

  • Handlebars does not perform recursive field lookup by default. The compat compile option must be set to enable this behavior.
  • Alternative Mustache delimiters (e.g. {{=<% %>=}}) are not supported.
  • Spaces are not allowed between the opening {{ and a command character such as #, /, or >. For example, {{> partial}} works but {{ > partial}} does not.

About

A blazing fast, spec-compliant PHP implementation of Handlebars.

Resources

Stars

40 stars

Watchers

4 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

PHP Handlebars

A blazing fast, spec-compliant PHP implementation of Handlebars.

The syntax of Handlebars is generally a superset of Mustache, so in most cases it is possible to swap out Mustache for Handlebars and continue using the same templates.

Features

  • Supports all Handlebars syntax and language features, including expressions, subexpressions, helpers, partials, hooks, @data variables, whitespace control, and .length on arrays and strings.
  • Arrays and objects can both be used as context values. {{#each}} over an object iterates its public properties.
  • Templates are parsed using PHP Handlebars Parser, which implements the same lexical analysis and AST grammar specification as Handlebars.js.
  • Tested against the Handlebars.js spec and the Mustache spec.

Performance

PHP Handlebars started as a fork of LightnCandy, but has been rewritten with an AST-based parser and optimized runtime to enable full Handlebars.js compatibility with better performance.

PHP Handlebars compiles and executes complex templates over 40% faster than LightnCandy, with 60% lower memory usage:

LibraryCompile timeRuntimeTotal timePeak memory usage
LightnCandy 1.2.65.0 ms2.4 ms7.4 ms5.3 MB
PHP Handlebars 2.12.8 ms1.4 ms4.2 ms1.8 MB

Tested on PHP 8.5 with the JIT enabled. See the benchmark branch to run the same test.

Installation

composer require devtheorem/php-handlebars

Usage

useDevTheorem\Handlebars\Handlebars;
$source = <<<'HBS' <p>Hi {{user.name}}, you have {{notifications.length}} new notification(s):</p> <ul> {{#notifications}} <li>{{count}} {{message}} ({{time}})</li> {{/notifications}} </ul>
HBS;
$data = [
'user' => ['name' => 'Jane'],
'notifications' => [
['count' => 4, 'message' => 'new comments', 'time' => '5 min ago'],
['count' => 3, 'message' => 'new followers', 'time' => '1 hr ago'],
],
];
$template = Handlebars::compile($source);
echo$template($data);

Output:

<p>Hi Jane, you have 2 new notification(s):</p><ul><li>4 new comments (5 min ago)</li><li>3 new followers (1 hr ago)</li></ul>

Precompilation

Templates and partials can be precompiled to native PHP for later execution, avoiding the overhead of parsing and compilation on each request.

Build step - compile all templates in a directory and cache the generated PHP:

useDevTheorem\Handlebars\Handlebars;
$templateDir = 'templates';
$cacheDir = 'templateCache';
foreach (glob("$templateDir/*.hbs") ?: [] as$file) {
$name = basename($file, '.hbs');
$code = Handlebars::precompile(file_get_contents($file));
file_put_contents("$cacheDir/$name.php", "<?php $code");
}

Runtime - load only needed templates, with precompiled partials resolved on demand:

$template = require'templateCache/page.php';
$data = ['title' => 'My Page', 'user' => ['name' => 'Jane']];
echo$template($data, [
'partialResolver' => fn(string$name) => require"templateCache/$name.php",
]);

Each {{> partial}} call triggers the resolver on first use, and the result is cached for the rest of that render. Only the partials that the page actually references are ever loaded.

Important

Precompiled templates must be regenerated whenever PHP Handlebars is updated, as the generated PHP code depends on the current version of the runtime. The build step above should be part of a deployment process so that precompiled output does not need to be committed to source control.

Compile Options

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:

useDevTheorem\Handlebars\{Handlebars, Options};
$template = Handlebars::compile('Hi {{first}} {{last}}!', newOptions(
strict: true,
));
echo$template(['first' => 'John']); // Error: "last" not defined

Available Options

  • compat: Set to true to enable recursive field lookup. If a template variable is not found in the current scope, it will automatically be looked up in parent scopes, matching Mustache's default behavior.

Note

Recursive lookup has a runtime cost, so it is recommended that performance-sensitive operations should avoid compat mode and instead opt for explicit path references.

  • 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 when knownHelpersOnly is set. Built-in helpers (if, unless, each, with, lookup, log) are pre-populated as true and may be excluded by setting them to false. Setting if or unless to false also disables the inline ternary optimization and allows those helpers to be overridden at runtime.

  • knownHelpersOnly: Restricts templates to only the helpers in knownHelpers, 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 unknown helper throws a compile-time exception instead of falling back to dynamic dispatch.

  • noEscape: Set to true to 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 to strict mode. A null intermediate in a path (e.g. foo is null when resolving foo.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.

Runtime Options

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 custom @data variables (e.g. ['version' => '1.0'] makes @version available in the template).

  • helpers: An array<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: An array<string, Closure> of partials compiled with Handlebars::compile. Useful for eagerly providing a known set of partials.

  • partialResolver: A Closure(string $name): ?Closure called lazily when a partial is referenced but not found in the partials map. Should return a compiled partial closure, or null if the partial does not exist. The resolved closure is cached for the remainder of the render, so each partial is loaded at most once per template invocation.

Custom Helpers

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:

useDevTheorem\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 falseecho$template(['my_var' => 1], $runtimeOptions); // Not equalecho$template(['my_var' => null], $runtimeOptions); // Not equal

HelperOptions Properties

  • name (readonly string): The helper name as it appeared in the template. Useful in helperMissing/blockHelperMissing hooks to identify which name was called.

  • hash (readonly array): Key/value pairs passed as hash arguments in the template (e.g. {{helper foo=1 bar="x"}} produces ['foo' => 1, 'bar' => 'x']).

  • blockParams (readonly int): The number of block parameters declared by the helper call (e.g. {{#helper as |a b|}} produces 2).

  • scope (mixed): The current evaluation context (equivalent to this in a Handlebars.js helper).

  • data (array): The current @data frame. The root key refers to the top-level context. index, key, first, and last are set by {{#each}} blocks. Can be read or modified inside a helper.

HelperOptions Methods

  • fn(mixed $context = <current scope>, mixed $data = null): string: Renders the block body. Pass a new context as $context to change what the block renders against (equivalent to options.fn(newContext) in JS). Pass a $data array with a 'data' key to inject @-prefixed variables into the block, and/or a 'blockParams' key containing an array of values to expose as block parameters.

  • inverse(mixed $context = <current scope>, mixed $data = null): string: Renders the {{else}} / inverse block. Returns an empty string if no inverse block was provided. Accepts the same optional $context and $data arguments as fn().

  • lookupProperty(mixed $parent, string|int $key): mixed: Reads a property from $parent without the helper needing to know whether it's an array or an object, returning $parent[$key] for arrays, $parent->$key for objects, and null for a missing key or any other value.

  • hasPartial(string $name): bool: Returns true if a partial with the given name is registered. Useful alongside registerPartial() to implement dynamic partial loading.

  • registerPartial(string $name, Closure $partial): void: Registers a compiled partial closure for the remainder of the render. The closure can be produced via Handlebars::compile, or by importing a cached closure created with Handlebars::precompile.

Note

isset($options->fn) and isset($options->inverse) return true if the helper was called as a block, and false for inline helper calls.

Hooks

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:

useDevTheorem\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

String Escaping

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. Because SafeString bypasses the automatic HTML escaping that {{ }} applies, any user-supplied content embedded in it must first be escaped with Handlebars::escapeExpression() to prevent XSS vulnerabilities.

Data Frames

Block helpers that inject @-prefixed variables should create a child data frame using Handlebars::createFrame($options->data), add their variables to it, and pass it to fn() or inverse() via the data key (e.g. $options->fn($context, ['data' => $frame])). This mirrors Handlebars.createFrame() in Handlebars.js, isolating the helper's variables while still inheriting parent data such as @root.

Missing Features

All syntax and language features from Handlebars.js 4.7.9 should work the same in PHP Handlebars, with the following exceptions:

Mustache Compatibility

Handlebars is largely compatible with Mustache syntax, with a few notable differences:

  • Handlebars does not perform recursive field lookup by default. The compat compile option must be set to enable this behavior.
  • Alternative Mustache delimiters (e.g. {{=<% %>=}}) are not supported.
  • Spaces are not allowed between the opening {{ and a command character such as #, /, or >. For example, {{> partial}} works but {{ > partial}} does not.

About

A blazing fast, spec-compliant PHP implementation of Handlebars.

Resources

Stars

40 stars

Watchers

4 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

PHP Handlebars

A blazing fast, spec-compliant PHP implementation of Handlebars.

The syntax of Handlebars is generally a superset of Mustache, so in most cases it is possible to swap out Mustache for Handlebars and continue using the same templates.

Features

  • Supports all Handlebars syntax and language features, including expressions, subexpressions, helpers, partials, hooks, @data variables, whitespace control, and .length on arrays and strings.
  • Arrays and objects can both be used as context values. {{#each}} over an object iterates its public properties.
  • Templates are parsed using PHP Handlebars Parser, which implements the same lexical analysis and AST grammar specification as Handlebars.js.
  • Tested against the Handlebars.js spec and the Mustache spec.

Performance

PHP Handlebars started as a fork of LightnCandy, but has been rewritten with an AST-based parser and optimized runtime to enable full Handlebars.js compatibility with better performance.

PHP Handlebars compiles and executes complex templates over 40% faster than LightnCandy, with 60% lower memory usage:

LibraryCompile timeRuntimeTotal timePeak memory usage
LightnCandy 1.2.65.0 ms2.4 ms7.4 ms5.3 MB
PHP Handlebars 2.12.8 ms1.4 ms4.2 ms1.8 MB

Tested on PHP 8.5 with the JIT enabled. See the benchmark branch to run the same test.

Installation

composer require devtheorem/php-handlebars

Usage

useDevTheorem\Handlebars\Handlebars;
$source = <<<'HBS' <p>Hi {{user.name}}, you have {{notifications.length}} new notification(s):</p> <ul> {{#notifications}} <li>{{count}} {{message}} ({{time}})</li> {{/notifications}} </ul>
HBS;
$data = [
'user' => ['name' => 'Jane'],
'notifications' => [
['count' => 4, 'message' => 'new comments', 'time' => '5 min ago'],
['count' => 3, 'message' => 'new followers', 'time' => '1 hr ago'],
],
];
$template = Handlebars::compile($source);
echo$template($data);

Output:

<p>Hi Jane, you have 2 new notification(s):</p><ul><li>4 new comments (5 min ago)</li><li>3 new followers (1 hr ago)</li></ul>

Precompilation

Templates and partials can be precompiled to native PHP for later execution, avoiding the overhead of parsing and compilation on each request.

Build step - compile all templates in a directory and cache the generated PHP:

useDevTheorem\Handlebars\Handlebars;
$templateDir = 'templates';
$cacheDir = 'templateCache';
foreach (glob("$templateDir/*.hbs") ?: [] as$file) {
$name = basename($file, '.hbs');
$code = Handlebars::precompile(file_get_contents($file));
file_put_contents("$cacheDir/$name.php", "<?php $code");
}

Runtime - load only needed templates, with precompiled partials resolved on demand:

$template = require'templateCache/page.php';
$data = ['title' => 'My Page', 'user' => ['name' => 'Jane']];
echo$template($data, [
'partialResolver' => fn(string$name) => require"templateCache/$name.php",
]);

Each {{> partial}} call triggers the resolver on first use, and the result is cached for the rest of that render. Only the partials that the page actually references are ever loaded.

Important

Precompiled templates must be regenerated whenever PHP Handlebars is updated, as the generated PHP code depends on the current version of the runtime. The build step above should be part of a deployment process so that precompiled output does not need to be committed to source control.

Compile Options

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:

useDevTheorem\Handlebars\{Handlebars, Options};
$template = Handlebars::compile('Hi {{first}} {{last}}!', newOptions(
strict: true,
));
echo$template(['first' => 'John']); // Error: "last" not defined

Available Options

  • compat: Set to true to enable recursive field lookup. If a template variable is not found in the current scope, it will automatically be looked up in parent scopes, matching Mustache's default behavior.

Note

Recursive lookup has a runtime cost, so it is recommended that performance-sensitive operations should avoid compat mode and instead opt for explicit path references.

  • 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 when knownHelpersOnly is set. Built-in helpers (if, unless, each, with, lookup, log) are pre-populated as true and may be excluded by setting them to false. Setting if or unless to false also disables the inline ternary optimization and allows those helpers to be overridden at runtime.

  • knownHelpersOnly: Restricts templates to only the helpers in knownHelpers, 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 unknown helper throws a compile-time exception instead of falling back to dynamic dispatch.

  • noEscape: Set to true to 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 to strict mode. A null intermediate in a path (e.g. foo is null when resolving foo.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.

Runtime Options

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 custom @data variables (e.g. ['version' => '1.0'] makes @version available in the template).

  • helpers: An array<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: An array<string, Closure> of partials compiled with Handlebars::compile. Useful for eagerly providing a known set of partials.

  • partialResolver: A Closure(string $name): ?Closure called lazily when a partial is referenced but not found in the partials map. Should return a compiled partial closure, or null if the partial does not exist. The resolved closure is cached for the remainder of the render, so each partial is loaded at most once per template invocation.

Custom Helpers

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:

useDevTheorem\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 falseecho$template(['my_var' => 1], $runtimeOptions); // Not equalecho$template(['my_var' => null], $runtimeOptions); // Not equal

HelperOptions Properties

  • name (readonly string): The helper name as it appeared in the template. Useful in helperMissing/blockHelperMissing hooks to identify which name was called.

  • hash (readonly array): Key/value pairs passed as hash arguments in the template (e.g. {{helper foo=1 bar="x"}} produces ['foo' => 1, 'bar' => 'x']).

  • blockParams (readonly int): The number of block parameters declared by the helper call (e.g. {{#helper as |a b|}} produces 2).

  • scope (mixed): The current evaluation context (equivalent to this in a Handlebars.js helper).

  • data (array): The current @data frame. The root key refers to the top-level context. index, key, first, and last are set by {{#each}} blocks. Can be read or modified inside a helper.

HelperOptions Methods

  • fn(mixed $context = <current scope>, mixed $data = null): string: Renders the block body. Pass a new context as $context to change what the block renders against (equivalent to options.fn(newContext) in JS). Pass a $data array with a 'data' key to inject @-prefixed variables into the block, and/or a 'blockParams' key containing an array of values to expose as block parameters.

  • inverse(mixed $context = <current scope>, mixed $data = null): string: Renders the {{else}} / inverse block. Returns an empty string if no inverse block was provided. Accepts the same optional $context and $data arguments as fn().

  • lookupProperty(mixed $parent, string|int $key): mixed: Reads a property from $parent without the helper needing to know whether it's an array or an object, returning $parent[$key] for arrays, $parent->$key for objects, and null for a missing key or any other value.

  • hasPartial(string $name): bool: Returns true if a partial with the given name is registered. Useful alongside registerPartial() to implement dynamic partial loading.

  • registerPartial(string $name, Closure $partial): void: Registers a compiled partial closure for the remainder of the render. The closure can be produced via Handlebars::compile, or by importing a cached closure created with Handlebars::precompile.

Note

isset($options->fn) and isset($options->inverse) return true if the helper was called as a block, and false for inline helper calls.

Hooks

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:

useDevTheorem\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

String Escaping

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. Because SafeString bypasses the automatic HTML escaping that {{ }} applies, any user-supplied content embedded in it must first be escaped with Handlebars::escapeExpression() to prevent XSS vulnerabilities.

Data Frames

Block helpers that inject @-prefixed variables should create a child data frame using Handlebars::createFrame($options->data), add their variables to it, and pass it to fn() or inverse() via the data key (e.g. $options->fn($context, ['data' => $frame])). This mirrors Handlebars.createFrame() in Handlebars.js, isolating the helper's variables while still inheriting parent data such as @root.

Missing Features

All syntax and language features from Handlebars.js 4.7.9 should work the same in PHP Handlebars, with the following exceptions:

Mustache Compatibility

Handlebars is largely compatible with Mustache syntax, with a few notable differences:

  • Handlebars does not perform recursive field lookup by default. The compat compile option must be set to enable this behavior.
  • Alternative Mustache delimiters (e.g. {{=<% %>=}}) are not supported.
  • Spaces are not allowed between the opening {{ and a command character such as #, /, or >. For example, {{> partial}} works but {{ > partial}} does not.

About

A blazing fast, spec-compliant PHP implementation of Handlebars.

Resources

Stars

40 stars

Watchers

4 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

PHP Handlebars

A blazing fast, spec-compliant PHP implementation of Handlebars.

The syntax of Handlebars is generally a superset of Mustache, so in most cases it is possible to swap out Mustache for Handlebars and continue using the same templates.

Features

  • Supports all Handlebars syntax and language features, including expressions, subexpressions, helpers, partials, hooks, @data variables, whitespace control, and .length on arrays and strings.
  • Arrays and objects can both be used as context values. {{#each}} over an object iterates its public properties.
  • Templates are parsed using PHP Handlebars Parser, which implements the same lexical analysis and AST grammar specification as Handlebars.js.
  • Tested against the Handlebars.js spec and the Mustache spec.

Performance

PHP Handlebars started as a fork of LightnCandy, but has been rewritten with an AST-based parser and optimized runtime to enable full Handlebars.js compatibility with better performance.

PHP Handlebars compiles and executes complex templates over 40% faster than LightnCandy, with 60% lower memory usage:

LibraryCompile timeRuntimeTotal timePeak memory usage
LightnCandy 1.2.65.0 ms2.4 ms7.4 ms5.3 MB
PHP Handlebars 2.12.8 ms1.4 ms4.2 ms1.8 MB

Tested on PHP 8.5 with the JIT enabled. See the benchmark branch to run the same test.

Installation

composer require devtheorem/php-handlebars

Usage

useDevTheorem\Handlebars\Handlebars;
$source = <<<'HBS' <p>Hi {{user.name}}, you have {{notifications.length}} new notification(s):</p> <ul> {{#notifications}} <li>{{count}} {{message}} ({{time}})</li> {{/notifications}} </ul>
HBS;
$data = [
'user' => ['name' => 'Jane'],
'notifications' => [
['count' => 4, 'message' => 'new comments', 'time' => '5 min ago'],
['count' => 3, 'message' => 'new followers', 'time' => '1 hr ago'],
],
];
$template = Handlebars::compile($source);
echo$template($data);

Output:

<p>Hi Jane, you have 2 new notification(s):</p><ul><li>4 new comments (5 min ago)</li><li>3 new followers (1 hr ago)</li></ul>

Precompilation

Templates and partials can be precompiled to native PHP for later execution, avoiding the overhead of parsing and compilation on each request.

Build step - compile all templates in a directory and cache the generated PHP:

useDevTheorem\Handlebars\Handlebars;
$templateDir = 'templates';
$cacheDir = 'templateCache';
foreach (glob("$templateDir/*.hbs") ?: [] as$file) {
$name = basename($file, '.hbs');
$code = Handlebars::precompile(file_get_contents($file));
file_put_contents("$cacheDir/$name.php", "<?php $code");
}

Runtime - load only needed templates, with precompiled partials resolved on demand:

$template = require'templateCache/page.php';
$data = ['title' => 'My Page', 'user' => ['name' => 'Jane']];
echo$template($data, [
'partialResolver' => fn(string$name) => require"templateCache/$name.php",
]);

Each {{> partial}} call triggers the resolver on first use, and the result is cached for the rest of that render. Only the partials that the page actually references are ever loaded.

Important

Precompiled templates must be regenerated whenever PHP Handlebars is updated, as the generated PHP code depends on the current version of the runtime. The build step above should be part of a deployment process so that precompiled output does not need to be committed to source control.

Compile Options

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:

useDevTheorem\Handlebars\{Handlebars, Options};
$template = Handlebars::compile('Hi {{first}} {{last}}!', newOptions(
strict: true,
));
echo$template(['first' => 'John']); // Error: "last" not defined

Available Options

  • compat: Set to true to enable recursive field lookup. If a template variable is not found in the current scope, it will automatically be looked up in parent scopes, matching Mustache's default behavior.

Note

Recursive lookup has a runtime cost, so it is recommended that performance-sensitive operations should avoid compat mode and instead opt for explicit path references.

  • 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 when knownHelpersOnly is set. Built-in helpers (if, unless, each, with, lookup, log) are pre-populated as true and may be excluded by setting them to false. Setting if or unless to false also disables the inline ternary optimization and allows those helpers to be overridden at runtime.

  • knownHelpersOnly: Restricts templates to only the helpers in knownHelpers, 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 unknown helper throws a compile-time exception instead of falling back to dynamic dispatch.

  • noEscape: Set to true to 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 to strict mode. A null intermediate in a path (e.g. foo is null when resolving foo.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.

Runtime Options

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 custom @data variables (e.g. ['version' => '1.0'] makes @version available in the template).

  • helpers: An array<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: An array<string, Closure> of partials compiled with Handlebars::compile. Useful for eagerly providing a known set of partials.

  • partialResolver: A Closure(string $name): ?Closure called lazily when a partial is referenced but not found in the partials map. Should return a compiled partial closure, or null if the partial does not exist. The resolved closure is cached for the remainder of the render, so each partial is loaded at most once per template invocation.

Custom Helpers

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:

useDevTheorem\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 falseecho$template(['my_var' => 1], $runtimeOptions); // Not equalecho$template(['my_var' => null], $runtimeOptions); // Not equal

HelperOptions Properties

  • name (readonly string): The helper name as it appeared in the template. Useful in helperMissing/blockHelperMissing hooks to identify which name was called.

  • hash (readonly array): Key/value pairs passed as hash arguments in the template (e.g. {{helper foo=1 bar="x"}} produces ['foo' => 1, 'bar' => 'x']).

  • blockParams (readonly int): The number of block parameters declared by the helper call (e.g. {{#helper as |a b|}} produces 2).

  • scope (mixed): The current evaluation context (equivalent to this in a Handlebars.js helper).

  • data (array): The current @data frame. The root key refers to the top-level context. index, key, first, and last are set by {{#each}} blocks. Can be read or modified inside a helper.

HelperOptions Methods

  • fn(mixed $context = <current scope>, mixed $data = null): string: Renders the block body. Pass a new context as $context to change what the block renders against (equivalent to options.fn(newContext) in JS). Pass a $data array with a 'data' key to inject @-prefixed variables into the block, and/or a 'blockParams' key containing an array of values to expose as block parameters.

  • inverse(mixed $context = <current scope>, mixed $data = null): string: Renders the {{else}} / inverse block. Returns an empty string if no inverse block was provided. Accepts the same optional $context and $data arguments as fn().

  • lookupProperty(mixed $parent, string|int $key): mixed: Reads a property from $parent without the helper needing to know whether it's an array or an object, returning $parent[$key] for arrays, $parent->$key for objects, and null for a missing key or any other value.

  • hasPartial(string $name): bool: Returns true if a partial with the given name is registered. Useful alongside registerPartial() to implement dynamic partial loading.

  • registerPartial(string $name, Closure $partial): void: Registers a compiled partial closure for the remainder of the render. The closure can be produced via Handlebars::compile, or by importing a cached closure created with Handlebars::precompile.

Note

isset($options->fn) and isset($options->inverse) return true if the helper was called as a block, and false for inline helper calls.

Hooks

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:

useDevTheorem\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

String Escaping

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. Because SafeString bypasses the automatic HTML escaping that {{ }} applies, any user-supplied content embedded in it must first be escaped with Handlebars::escapeExpression() to prevent XSS vulnerabilities.

Data Frames

Block helpers that inject @-prefixed variables should create a child data frame using Handlebars::createFrame($options->data), add their variables to it, and pass it to fn() or inverse() via the data key (e.g. $options->fn($context, ['data' => $frame])). This mirrors Handlebars.createFrame() in Handlebars.js, isolating the helper's variables while still inheriting parent data such as @root.

Missing Features

All syntax and language features from Handlebars.js 4.7.9 should work the same in PHP Handlebars, with the following exceptions:

Mustache Compatibility

Handlebars is largely compatible with Mustache syntax, with a few notable differences:

  • Handlebars does not perform recursive field lookup by default. The compat compile option must be set to enable this behavior.
  • Alternative Mustache delimiters (e.g. {{=<% %>=}}) are not supported.
  • Spaces are not allowed between the opening {{ and a command character such as #, /, or >. For example, {{> partial}} works but {{ > partial}} does not.

About

A blazing fast, spec-compliant PHP implementation of Handlebars.

Resources

Stars

40 stars

Watchers

4 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

PHP Handlebars

A blazing fast, spec-compliant PHP implementation of Handlebars.

The syntax of Handlebars is generally a superset of Mustache, so in most cases it is possible to swap out Mustache for Handlebars and continue using the same templates.

Features

  • Supports all Handlebars syntax and language features, including expressions, subexpressions, helpers, partials, hooks, @data variables, whitespace control, and .length on arrays and strings.
  • Arrays and objects can both be used as context values. {{#each}} over an object iterates its public properties.
  • Templates are parsed using PHP Handlebars Parser, which implements the same lexical analysis and AST grammar specification as Handlebars.js.
  • Tested against the Handlebars.js spec and the Mustache spec.

Performance

PHP Handlebars started as a fork of LightnCandy, but has been rewritten with an AST-based parser and optimized runtime to enable full Handlebars.js compatibility with better performance.

PHP Handlebars compiles and executes complex templates over 40% faster than LightnCandy, with 60% lower memory usage:

LibraryCompile timeRuntimeTotal timePeak memory usage
LightnCandy 1.2.65.0 ms2.4 ms7.4 ms5.3 MB
PHP Handlebars 2.12.8 ms1.4 ms4.2 ms1.8 MB

Tested on PHP 8.5 with the JIT enabled. See the benchmark branch to run the same test.

Installation

composer require devtheorem/php-handlebars

Usage

useDevTheorem\Handlebars\Handlebars;
$source = <<<'HBS' <p>Hi {{user.name}}, you have {{notifications.length}} new notification(s):</p> <ul> {{#notifications}} <li>{{count}} {{message}} ({{time}})</li> {{/notifications}} </ul>
HBS;
$data = [
'user' => ['name' => 'Jane'],
'notifications' => [
['count' => 4, 'message' => 'new comments', 'time' => '5 min ago'],
['count' => 3, 'message' => 'new followers', 'time' => '1 hr ago'],
],
];
$template = Handlebars::compile($source);
echo$template($data);

Output:

<p>Hi Jane, you have 2 new notification(s):</p><ul><li>4 new comments (5 min ago)</li><li>3 new followers (1 hr ago)</li></ul>

Precompilation

Templates and partials can be precompiled to native PHP for later execution, avoiding the overhead of parsing and compilation on each request.

Build step - compile all templates in a directory and cache the generated PHP:

useDevTheorem\Handlebars\Handlebars;
$templateDir = 'templates';
$cacheDir = 'templateCache';
foreach (glob("$templateDir/*.hbs") ?: [] as$file) {
$name = basename($file, '.hbs');
$code = Handlebars::precompile(file_get_contents($file));
file_put_contents("$cacheDir/$name.php", "<?php $code");
}

Runtime - load only needed templates, with precompiled partials resolved on demand:

$template = require'templateCache/page.php';
$data = ['title' => 'My Page', 'user' => ['name' => 'Jane']];
echo$template($data, [
'partialResolver' => fn(string$name) => require"templateCache/$name.php",
]);

Each {{> partial}} call triggers the resolver on first use, and the result is cached for the rest of that render. Only the partials that the page actually references are ever loaded.

Important

Precompiled templates must be regenerated whenever PHP Handlebars is updated, as the generated PHP code depends on the current version of the runtime. The build step above should be part of a deployment process so that precompiled output does not need to be committed to source control.

Compile Options

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:

useDevTheorem\Handlebars\{Handlebars, Options};
$template = Handlebars::compile('Hi {{first}} {{last}}!', newOptions(
strict: true,
));
echo$template(['first' => 'John']); // Error: "last" not defined

Available Options

  • compat: Set to true to enable recursive field lookup. If a template variable is not found in the current scope, it will automatically be looked up in parent scopes, matching Mustache's default behavior.

Note

Recursive lookup has a runtime cost, so it is recommended that performance-sensitive operations should avoid compat mode and instead opt for explicit path references.

  • 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 when knownHelpersOnly is set. Built-in helpers (if, unless, each, with, lookup, log) are pre-populated as true and may be excluded by setting them to false. Setting if or unless to false also disables the inline ternary optimization and allows those helpers to be overridden at runtime.

  • knownHelpersOnly: Restricts templates to only the helpers in knownHelpers, 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 unknown helper throws a compile-time exception instead of falling back to dynamic dispatch.

  • noEscape: Set to true to 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 to strict mode. A null intermediate in a path (e.g. foo is null when resolving foo.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.

Runtime Options

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 custom @data variables (e.g. ['version' => '1.0'] makes @version available in the template).

  • helpers: An array<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: An array<string, Closure> of partials compiled with Handlebars::compile. Useful for eagerly providing a known set of partials.

  • partialResolver: A Closure(string $name): ?Closure called lazily when a partial is referenced but not found in the partials map. Should return a compiled partial closure, or null if the partial does not exist. The resolved closure is cached for the remainder of the render, so each partial is loaded at most once per template invocation.

Custom Helpers

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:

useDevTheorem\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 falseecho$template(['my_var' => 1], $runtimeOptions); // Not equalecho$template(['my_var' => null], $runtimeOptions); // Not equal

HelperOptions Properties

  • name (readonly string): The helper name as it appeared in the template. Useful in helperMissing/blockHelperMissing hooks to identify which name was called.

  • hash (readonly array): Key/value pairs passed as hash arguments in the template (e.g. {{helper foo=1 bar="x"}} produces ['foo' => 1, 'bar' => 'x']).

  • blockParams (readonly int): The number of block parameters declared by the helper call (e.g. {{#helper as |a b|}} produces 2).

  • scope (mixed): The current evaluation context (equivalent to this in a Handlebars.js helper).

  • data (array): The current @data frame. The root key refers to the top-level context. index, key, first, and last are set by {{#each}} blocks. Can be read or modified inside a helper.

HelperOptions Methods

  • fn(mixed $context = <current scope>, mixed $data = null): string: Renders the block body. Pass a new context as $context to change what the block renders against (equivalent to options.fn(newContext) in JS). Pass a $data array with a 'data' key to inject @-prefixed variables into the block, and/or a 'blockParams' key containing an array of values to expose as block parameters.

  • inverse(mixed $context = <current scope>, mixed $data = null): string: Renders the {{else}} / inverse block. Returns an empty string if no inverse block was provided. Accepts the same optional $context and $data arguments as fn().

  • lookupProperty(mixed $parent, string|int $key): mixed: Reads a property from $parent without the helper needing to know whether it's an array or an object, returning $parent[$key] for arrays, $parent->$key for objects, and null for a missing key or any other value.

  • hasPartial(string $name): bool: Returns true if a partial with the given name is registered. Useful alongside registerPartial() to implement dynamic partial loading.

  • registerPartial(string $name, Closure $partial): void: Registers a compiled partial closure for the remainder of the render. The closure can be produced via Handlebars::compile, or by importing a cached closure created with Handlebars::precompile.

Note

isset($options->fn) and isset($options->inverse) return true if the helper was called as a block, and false for inline helper calls.

Hooks

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:

useDevTheorem\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

String Escaping

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. Because SafeString bypasses the automatic HTML escaping that {{ }} applies, any user-supplied content embedded in it must first be escaped with Handlebars::escapeExpression() to prevent XSS vulnerabilities.

Data Frames

Block helpers that inject @-prefixed variables should create a child data frame using Handlebars::createFrame($options->data), add their variables to it, and pass it to fn() or inverse() via the data key (e.g. $options->fn($context, ['data' => $frame])). This mirrors Handlebars.createFrame() in Handlebars.js, isolating the helper's variables while still inheriting parent data such as @root.

Missing Features

All syntax and language features from Handlebars.js 4.7.9 should work the same in PHP Handlebars, with the following exceptions:

Mustache Compatibility

Handlebars is largely compatible with Mustache syntax, with a few notable differences:

  • Handlebars does not perform recursive field lookup by default. The compat compile option must be set to enable this behavior.
  • Alternative Mustache delimiters (e.g. {{=<% %>=}}) are not supported.
  • Spaces are not allowed between the opening {{ and a command character such as #, /, or >. For example, {{> partial}} works but {{ > partial}} does not.

About

A blazing fast, spec-compliant PHP implementation of Handlebars.

Resources

Stars

40 stars

Watchers

4 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

PHP Handlebars

A blazing fast, spec-compliant PHP implementation of Handlebars.

The syntax of Handlebars is generally a superset of Mustache, so in most cases it is possible to swap out Mustache for Handlebars and continue using the same templates.

Features

  • Supports all Handlebars syntax and language features, including expressions, subexpressions, helpers, partials, hooks, @data variables, whitespace control, and .length on arrays and strings.
  • Arrays and objects can both be used as context values. {{#each}} over an object iterates its public properties.
  • Templates are parsed using PHP Handlebars Parser, which implements the same lexical analysis and AST grammar specification as Handlebars.js.
  • Tested against the Handlebars.js spec and the Mustache spec.

Performance

PHP Handlebars started as a fork of LightnCandy, but has been rewritten with an AST-based parser and optimized runtime to enable full Handlebars.js compatibility with better performance.

PHP Handlebars compiles and executes complex templates over 40% faster than LightnCandy, with 60% lower memory usage:

LibraryCompile timeRuntimeTotal timePeak memory usage
LightnCandy 1.2.65.0 ms2.4 ms7.4 ms5.3 MB
PHP Handlebars 2.12.8 ms1.4 ms4.2 ms1.8 MB

Tested on PHP 8.5 with the JIT enabled. See the benchmark branch to run the same test.

Installation

composer require devtheorem/php-handlebars

Usage

useDevTheorem\Handlebars\Handlebars;
$source = <<<'HBS' <p>Hi {{user.name}}, you have {{notifications.length}} new notification(s):</p> <ul> {{#notifications}} <li>{{count}} {{message}} ({{time}})</li> {{/notifications}} </ul>
HBS;
$data = [
'user' => ['name' => 'Jane'],
'notifications' => [
['count' => 4, 'message' => 'new comments', 'time' => '5 min ago'],
['count' => 3, 'message' => 'new followers', 'time' => '1 hr ago'],
],
];
$template = Handlebars::compile($source);
echo$template($data);

Output:

<p>Hi Jane, you have 2 new notification(s):</p><ul><li>4 new comments (5 min ago)</li><li>3 new followers (1 hr ago)</li></ul>

Precompilation

Templates and partials can be precompiled to native PHP for later execution, avoiding the overhead of parsing and compilation on each request.

Build step - compile all templates in a directory and cache the generated PHP:

useDevTheorem\Handlebars\Handlebars;
$templateDir = 'templates';
$cacheDir = 'templateCache';
foreach (glob("$templateDir/*.hbs") ?: [] as$file) {
$name = basename($file, '.hbs');
$code = Handlebars::precompile(file_get_contents($file));
file_put_contents("$cacheDir/$name.php", "<?php $code");
}

Runtime - load only needed templates, with precompiled partials resolved on demand:

$template = require'templateCache/page.php';
$data = ['title' => 'My Page', 'user' => ['name' => 'Jane']];
echo$template($data, [
'partialResolver' => fn(string$name) => require"templateCache/$name.php",
]);

Each {{> partial}} call triggers the resolver on first use, and the result is cached for the rest of that render. Only the partials that the page actually references are ever loaded.

Important

Precompiled templates must be regenerated whenever PHP Handlebars is updated, as the generated PHP code depends on the current version of the runtime. The build step above should be part of a deployment process so that precompiled output does not need to be committed to source control.

Compile Options

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:

useDevTheorem\Handlebars\{Handlebars, Options};
$template = Handlebars::compile('Hi {{first}} {{last}}!', newOptions(
strict: true,
));
echo$template(['first' => 'John']); // Error: "last" not defined

Available Options

  • compat: Set to true to enable recursive field lookup. If a template variable is not found in the current scope, it will automatically be looked up in parent scopes, matching Mustache's default behavior.

Note

Recursive lookup has a runtime cost, so it is recommended that performance-sensitive operations should avoid compat mode and instead opt for explicit path references.

  • 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 when knownHelpersOnly is set. Built-in helpers (if, unless, each, with, lookup, log) are pre-populated as true and may be excluded by setting them to false. Setting if or unless to false also disables the inline ternary optimization and allows those helpers to be overridden at runtime.

  • knownHelpersOnly: Restricts templates to only the helpers in knownHelpers, 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 unknown helper throws a compile-time exception instead of falling back to dynamic dispatch.

  • noEscape: Set to true to 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 to strict mode. A null intermediate in a path (e.g. foo is null when resolving foo.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.

Runtime Options

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 custom @data variables (e.g. ['version' => '1.0'] makes @version available in the template).

  • helpers: An array<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: An array<string, Closure> of partials compiled with Handlebars::compile. Useful for eagerly providing a known set of partials.

  • partialResolver: A Closure(string $name): ?Closure called lazily when a partial is referenced but not found in the partials map. Should return a compiled partial closure, or null if the partial does not exist. The resolved closure is cached for the remainder of the render, so each partial is loaded at most once per template invocation.

Custom Helpers

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:

useDevTheorem\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 falseecho$template(['my_var' => 1], $runtimeOptions); // Not equalecho$template(['my_var' => null], $runtimeOptions); // Not equal

HelperOptions Properties

  • name (readonly string): The helper name as it appeared in the template. Useful in helperMissing/blockHelperMissing hooks to identify which name was called.

  • hash (readonly array): Key/value pairs passed as hash arguments in the template (e.g. {{helper foo=1 bar="x"}} produces ['foo' => 1, 'bar' => 'x']).

  • blockParams (readonly int): The number of block parameters declared by the helper call (e.g. {{#helper as |a b|}} produces 2).

  • scope (mixed): The current evaluation context (equivalent to this in a Handlebars.js helper).

  • data (array): The current @data frame. The root key refers to the top-level context. index, key, first, and last are set by {{#each}} blocks. Can be read or modified inside a helper.

HelperOptions Methods

  • fn(mixed $context = <current scope>, mixed $data = null): string: Renders the block body. Pass a new context as $context to change what the block renders against (equivalent to options.fn(newContext) in JS). Pass a $data array with a 'data' key to inject @-prefixed variables into the block, and/or a 'blockParams' key containing an array of values to expose as block parameters.

  • inverse(mixed $context = <current scope>, mixed $data = null): string: Renders the {{else}} / inverse block. Returns an empty string if no inverse block was provided. Accepts the same optional $context and $data arguments as fn().

  • lookupProperty(mixed $parent, string|int $key): mixed: Reads a property from $parent without the helper needing to know whether it's an array or an object, returning $parent[$key] for arrays, $parent->$key for objects, and null for a missing key or any other value.

  • hasPartial(string $name): bool: Returns true if a partial with the given name is registered. Useful alongside registerPartial() to implement dynamic partial loading.

  • registerPartial(string $name, Closure $partial): void: Registers a compiled partial closure for the remainder of the render. The closure can be produced via Handlebars::compile, or by importing a cached closure created with Handlebars::precompile.

Note

isset($options->fn) and isset($options->inverse) return true if the helper was called as a block, and false for inline helper calls.

Hooks

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:

useDevTheorem\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

String Escaping

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. Because SafeString bypasses the automatic HTML escaping that {{ }} applies, any user-supplied content embedded in it must first be escaped with Handlebars::escapeExpression() to prevent XSS vulnerabilities.

Data Frames

Block helpers that inject @-prefixed variables should create a child data frame using Handlebars::createFrame($options->data), add their variables to it, and pass it to fn() or inverse() via the data key (e.g. $options->fn($context, ['data' => $frame])). This mirrors Handlebars.createFrame() in Handlebars.js, isolating the helper's variables while still inheriting parent data such as @root.

Missing Features

All syntax and language features from Handlebars.js 4.7.9 should work the same in PHP Handlebars, with the following exceptions:

Mustache Compatibility

Handlebars is largely compatible with Mustache syntax, with a few notable differences:

  • Handlebars does not perform recursive field lookup by default. The compat compile option must be set to enable this behavior.
  • Alternative Mustache delimiters (e.g. {{=<% %>=}}) are not supported.
  • Spaces are not allowed between the opening {{ and a command character such as #, /, or >. For example, {{> partial}} works but {{ > partial}} does not.

About

A blazing fast, spec-compliant PHP implementation of Handlebars.

Resources

Stars

40 stars

Watchers

4 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

PHP Handlebars

A blazing fast, spec-compliant PHP implementation of Handlebars.

The syntax of Handlebars is generally a superset of Mustache, so in most cases it is possible to swap out Mustache for Handlebars and continue using the same templates.

Features

  • Supports all Handlebars syntax and language features, including expressions, subexpressions, helpers, partials, hooks, @data variables, whitespace control, and .length on arrays and strings.
  • Arrays and objects can both be used as context values. {{#each}} over an object iterates its public properties.
  • Templates are parsed using PHP Handlebars Parser, which implements the same lexical analysis and AST grammar specification as Handlebars.js.
  • Tested against the Handlebars.js spec and the Mustache spec.

Performance

PHP Handlebars started as a fork of LightnCandy, but has been rewritten with an AST-based parser and optimized runtime to enable full Handlebars.js compatibility with better performance.

PHP Handlebars compiles and executes complex templates over 40% faster than LightnCandy, with 60% lower memory usage:

LibraryCompile timeRuntimeTotal timePeak memory usage
LightnCandy 1.2.65.0 ms2.4 ms7.4 ms5.3 MB
PHP Handlebars 2.12.8 ms1.4 ms4.2 ms1.8 MB

Tested on PHP 8.5 with the JIT enabled. See the benchmark branch to run the same test.

Installation

composer require devtheorem/php-handlebars

Usage

useDevTheorem\Handlebars\Handlebars;
$source = <<<'HBS' <p>Hi {{user.name}}, you have {{notifications.length}} new notification(s):</p> <ul> {{#notifications}} <li>{{count}} {{message}} ({{time}})</li> {{/notifications}} </ul>
HBS;
$data = [
'user' => ['name' => 'Jane'],
'notifications' => [
['count' => 4, 'message' => 'new comments', 'time' => '5 min ago'],
['count' => 3, 'message' => 'new followers', 'time' => '1 hr ago'],
],
];
$template = Handlebars::compile($source);
echo$template($data);

Output:

<p>Hi Jane, you have 2 new notification(s):</p><ul><li>4 new comments (5 min ago)</li><li>3 new followers (1 hr ago)</li></ul>

Precompilation

Templates and partials can be precompiled to native PHP for later execution, avoiding the overhead of parsing and compilation on each request.

Build step - compile all templates in a directory and cache the generated PHP:

useDevTheorem\Handlebars\Handlebars;
$templateDir = 'templates';
$cacheDir = 'templateCache';
foreach (glob("$templateDir/*.hbs") ?: [] as$file) {
$name = basename($file, '.hbs');
$code = Handlebars::precompile(file_get_contents($file));
file_put_contents("$cacheDir/$name.php", "<?php $code");
}

Runtime - load only needed templates, with precompiled partials resolved on demand:

$template = require'templateCache/page.php';
$data = ['title' => 'My Page', 'user' => ['name' => 'Jane']];
echo$template($data, [
'partialResolver' => fn(string$name) => require"templateCache/$name.php",
]);

Each {{> partial}} call triggers the resolver on first use, and the result is cached for the rest of that render. Only the partials that the page actually references are ever loaded.

Important

Precompiled templates must be regenerated whenever PHP Handlebars is updated, as the generated PHP code depends on the current version of the runtime. The build step above should be part of a deployment process so that precompiled output does not need to be committed to source control.

Compile Options

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:

useDevTheorem\Handlebars\{Handlebars, Options};
$template = Handlebars::compile('Hi {{first}} {{last}}!', newOptions(
strict: true,
));
echo$template(['first' => 'John']); // Error: "last" not defined

Available Options

  • compat: Set to true to enable recursive field lookup. If a template variable is not found in the current scope, it will automatically be looked up in parent scopes, matching Mustache's default behavior.

Note

Recursive lookup has a runtime cost, so it is recommended that performance-sensitive operations should avoid compat mode and instead opt for explicit path references.

  • 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 when knownHelpersOnly is set. Built-in helpers (if, unless, each, with, lookup, log) are pre-populated as true and may be excluded by setting them to false. Setting if or unless to false also disables the inline ternary optimization and allows those helpers to be overridden at runtime.

  • knownHelpersOnly: Restricts templates to only the helpers in knownHelpers, 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 unknown helper throws a compile-time exception instead of falling back to dynamic dispatch.

  • noEscape: Set to true to 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 to strict mode. A null intermediate in a path (e.g. foo is null when resolving foo.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.

Runtime Options

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 custom @data variables (e.g. ['version' => '1.0'] makes @version available in the template).

  • helpers: An array<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: An array<string, Closure> of partials compiled with Handlebars::compile. Useful for eagerly providing a known set of partials.

  • partialResolver: A Closure(string $name): ?Closure called lazily when a partial is referenced but not found in the partials map. Should return a compiled partial closure, or null if the partial does not exist. The resolved closure is cached for the remainder of the render, so each partial is loaded at most once per template invocation.

Custom Helpers

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:

useDevTheorem\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 falseecho$template(['my_var' => 1], $runtimeOptions); // Not equalecho$template(['my_var' => null], $runtimeOptions); // Not equal

HelperOptions Properties

  • name (readonly string): The helper name as it appeared in the template. Useful in helperMissing/blockHelperMissing hooks to identify which name was called.

  • hash (readonly array): Key/value pairs passed as hash arguments in the template (e.g. {{helper foo=1 bar="x"}} produces ['foo' => 1, 'bar' => 'x']).

  • blockParams (readonly int): The number of block parameters declared by the helper call (e.g. {{#helper as |a b|}} produces 2).

  • scope (mixed): The current evaluation context (equivalent to this in a Handlebars.js helper).

  • data (array): The current @data frame. The root key refers to the top-level context. index, key, first, and last are set by {{#each}} blocks. Can be read or modified inside a helper.

HelperOptions Methods

  • fn(mixed $context = <current scope>, mixed $data = null): string: Renders the block body. Pass a new context as $context to change what the block renders against (equivalent to options.fn(newContext) in JS). Pass a $data array with a 'data' key to inject @-prefixed variables into the block, and/or a 'blockParams' key containing an array of values to expose as block parameters.

  • inverse(mixed $context = <current scope>, mixed $data = null): string: Renders the {{else}} / inverse block. Returns an empty string if no inverse block was provided. Accepts the same optional $context and $data arguments as fn().

  • lookupProperty(mixed $parent, string|int $key): mixed: Reads a property from $parent without the helper needing to know whether it's an array or an object, returning $parent[$key] for arrays, $parent->$key for objects, and null for a missing key or any other value.

  • hasPartial(string $name): bool: Returns true if a partial with the given name is registered. Useful alongside registerPartial() to implement dynamic partial loading.

  • registerPartial(string $name, Closure $partial): void: Registers a compiled partial closure for the remainder of the render. The closure can be produced via Handlebars::compile, or by importing a cached closure created with Handlebars::precompile.

Note

isset($options->fn) and isset($options->inverse) return true if the helper was called as a block, and false for inline helper calls.

Hooks

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:

useDevTheorem\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

String Escaping

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. Because SafeString bypasses the automatic HTML escaping that {{ }} applies, any user-supplied content embedded in it must first be escaped with Handlebars::escapeExpression() to prevent XSS vulnerabilities.

Data Frames

Block helpers that inject @-prefixed variables should create a child data frame using Handlebars::createFrame($options->data), add their variables to it, and pass it to fn() or inverse() via the data key (e.g. $options->fn($context, ['data' => $frame])). This mirrors Handlebars.createFrame() in Handlebars.js, isolating the helper's variables while still inheriting parent data such as @root.

Missing Features

All syntax and language features from Handlebars.js 4.7.9 should work the same in PHP Handlebars, with the following exceptions:

Mustache Compatibility

Handlebars is largely compatible with Mustache syntax, with a few notable differences:

  • Handlebars does not perform recursive field lookup by default. The compat compile option must be set to enable this behavior.
  • Alternative Mustache delimiters (e.g. {{=<% %>=}}) are not supported.
  • Spaces are not allowed between the opening {{ and a command character such as #, /, or >. For example, {{> partial}} works but {{ > partial}} does not.

About

A blazing fast, spec-compliant PHP implementation of Handlebars.

Resources

Stars

40 stars

Watchers

4 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

PHP Handlebars

A blazing fast, spec-compliant PHP implementation of Handlebars.

The syntax of Handlebars is generally a superset of Mustache, so in most cases it is possible to swap out Mustache for Handlebars and continue using the same templates.

Features

  • Supports all Handlebars syntax and language features, including expressions, subexpressions, helpers, partials, hooks, @data variables, whitespace control, and .length on arrays and strings.
  • Arrays and objects can both be used as context values. {{#each}} over an object iterates its public properties.
  • Templates are parsed using PHP Handlebars Parser, which implements the same lexical analysis and AST grammar specification as Handlebars.js.
  • Tested against the Handlebars.js spec and the Mustache spec.

Performance

PHP Handlebars started as a fork of LightnCandy, but has been rewritten with an AST-based parser and optimized runtime to enable full Handlebars.js compatibility with better performance.

PHP Handlebars compiles and executes complex templates over 40% faster than LightnCandy, with 60% lower memory usage:

LibraryCompile timeRuntimeTotal timePeak memory usage
LightnCandy 1.2.65.0 ms2.4 ms7.4 ms5.3 MB
PHP Handlebars 2.12.8 ms1.4 ms4.2 ms1.8 MB

Tested on PHP 8.5 with the JIT enabled. See the benchmark branch to run the same test.

Installation

composer require devtheorem/php-handlebars

Usage

useDevTheorem\Handlebars\Handlebars;
$source = <<<'HBS' <p>Hi {{user.name}}, you have {{notifications.length}} new notification(s):</p> <ul> {{#notifications}} <li>{{count}} {{message}} ({{time}})</li> {{/notifications}} </ul>
HBS;
$data = [
'user' => ['name' => 'Jane'],
'notifications' => [
['count' => 4, 'message' => 'new comments', 'time' => '5 min ago'],
['count' => 3, 'message' => 'new followers', 'time' => '1 hr ago'],
],
];
$template = Handlebars::compile($source);
echo$template($data);

Output:

<p>Hi Jane, you have 2 new notification(s):</p><ul><li>4 new comments (5 min ago)</li><li>3 new followers (1 hr ago)</li></ul>

Precompilation

Templates and partials can be precompiled to native PHP for later execution, avoiding the overhead of parsing and compilation on each request.

Build step - compile all templates in a directory and cache the generated PHP:

useDevTheorem\Handlebars\Handlebars;
$templateDir = 'templates';
$cacheDir = 'templateCache';
foreach (glob("$templateDir/*.hbs") ?: [] as$file) {
$name = basename($file, '.hbs');
$code = Handlebars::precompile(file_get_contents($file));
file_put_contents("$cacheDir/$name.php", "<?php $code");
}

Runtime - load only needed templates, with precompiled partials resolved on demand:

$template = require'templateCache/page.php';
$data = ['title' => 'My Page', 'user' => ['name' => 'Jane']];
echo$template($data, [
'partialResolver' => fn(string$name) => require"templateCache/$name.php",
]);

Each {{> partial}} call triggers the resolver on first use, and the result is cached for the rest of that render. Only the partials that the page actually references are ever loaded.

Important

Precompiled templates must be regenerated whenever PHP Handlebars is updated, as the generated PHP code depends on the current version of the runtime. The build step above should be part of a deployment process so that precompiled output does not need to be committed to source control.

Compile Options

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:

useDevTheorem\Handlebars\{Handlebars, Options};
$template = Handlebars::compile('Hi {{first}} {{last}}!', newOptions(
strict: true,
));
echo$template(['first' => 'John']); // Error: "last" not defined

Available Options

  • compat: Set to true to enable recursive field lookup. If a template variable is not found in the current scope, it will automatically be looked up in parent scopes, matching Mustache's default behavior.

Note

Recursive lookup has a runtime cost, so it is recommended that performance-sensitive operations should avoid compat mode and instead opt for explicit path references.

  • 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 when knownHelpersOnly is set. Built-in helpers (if, unless, each, with, lookup, log) are pre-populated as true and may be excluded by setting them to false. Setting if or unless to false also disables the inline ternary optimization and allows those helpers to be overridden at runtime.

  • knownHelpersOnly: Restricts templates to only the helpers in knownHelpers, 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 unknown helper throws a compile-time exception instead of falling back to dynamic dispatch.

  • noEscape: Set to true to 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 to strict mode. A null intermediate in a path (e.g. foo is null when resolving foo.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.

Runtime Options

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 custom @data variables (e.g. ['version' => '1.0'] makes @version available in the template).

  • helpers: An array<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: An array<string, Closure> of partials compiled with Handlebars::compile. Useful for eagerly providing a known set of partials.

  • partialResolver: A Closure(string $name): ?Closure called lazily when a partial is referenced but not found in the partials map. Should return a compiled partial closure, or null if the partial does not exist. The resolved closure is cached for the remainder of the render, so each partial is loaded at most once per template invocation.

Custom Helpers

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:

useDevTheorem\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 falseecho$template(['my_var' => 1], $runtimeOptions); // Not equalecho$template(['my_var' => null], $runtimeOptions); // Not equal

HelperOptions Properties

  • name (readonly string): The helper name as it appeared in the template. Useful in helperMissing/blockHelperMissing hooks to identify which name was called.

  • hash (readonly array): Key/value pairs passed as hash arguments in the template (e.g. {{helper foo=1 bar="x"}} produces ['foo' => 1, 'bar' => 'x']).

  • blockParams (readonly int): The number of block parameters declared by the helper call (e.g. {{#helper as |a b|}} produces 2).

  • scope (mixed): The current evaluation context (equivalent to this in a Handlebars.js helper).

  • data (array): The current @data frame. The root key refers to the top-level context. index, key, first, and last are set by {{#each}} blocks. Can be read or modified inside a helper.

HelperOptions Methods

  • fn(mixed $context = <current scope>, mixed $data = null): string: Renders the block body. Pass a new context as $context to change what the block renders against (equivalent to options.fn(newContext) in JS). Pass a $data array with a 'data' key to inject @-prefixed variables into the block, and/or a 'blockParams' key containing an array of values to expose as block parameters.

  • inverse(mixed $context = <current scope>, mixed $data = null): string: Renders the {{else}} / inverse block. Returns an empty string if no inverse block was provided. Accepts the same optional $context and $data arguments as fn().

  • lookupProperty(mixed $parent, string|int $key): mixed: Reads a property from $parent without the helper needing to know whether it's an array or an object, returning $parent[$key] for arrays, $parent->$key for objects, and null for a missing key or any other value.

  • hasPartial(string $name): bool: Returns true if a partial with the given name is registered. Useful alongside registerPartial() to implement dynamic partial loading.

  • registerPartial(string $name, Closure $partial): void: Registers a compiled partial closure for the remainder of the render. The closure can be produced via Handlebars::compile, or by importing a cached closure created with Handlebars::precompile.

Note

isset($options->fn) and isset($options->inverse) return true if the helper was called as a block, and false for inline helper calls.

Hooks

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:

useDevTheorem\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

String Escaping

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. Because SafeString bypasses the automatic HTML escaping that {{ }} applies, any user-supplied content embedded in it must first be escaped with Handlebars::escapeExpression() to prevent XSS vulnerabilities.

Data Frames

Block helpers that inject @-prefixed variables should create a child data frame using Handlebars::createFrame($options->data), add their variables to it, and pass it to fn() or inverse() via the data key (e.g. $options->fn($context, ['data' => $frame])). This mirrors Handlebars.createFrame() in Handlebars.js, isolating the helper's variables while still inheriting parent data such as @root.

Missing Features

All syntax and language features from Handlebars.js 4.7.9 should work the same in PHP Handlebars, with the following exceptions:

Mustache Compatibility

Handlebars is largely compatible with Mustache syntax, with a few notable differences:

  • Handlebars does not perform recursive field lookup by default. The compat compile option must be set to enable this behavior.
  • Alternative Mustache delimiters (e.g. {{=<% %>=}}) are not supported.
  • Spaces are not allowed between the opening {{ and a command character such as #, /, or >. For example, {{> partial}} works but {{ > partial}} does not.

About

A blazing fast, spec-compliant PHP implementation of Handlebars.

Resources

Stars

40 stars

Watchers

4 watching

Forks

Releases

Used by

Contributors

Languages