{"id":47715078,"url":"https://github.com/devtheorem/php-handlebars","last_synced_at":"2026-04-28T05:03:34.956Z","repository":{"id":270300101,"uuid":"909527089","full_name":"devtheorem/php-handlebars","owner":"devtheorem","description":"A blazing fast, spec-compliant PHP implementation of Handlebars.","archived":false,"fork":false,"pushed_at":"2026-03-27T04:27:08.000Z","size":4854,"stargazers_count":19,"open_issues_count":0,"forks_count":6,"subscribers_count":4,"default_branch":"master","last_synced_at":"2026-03-27T16:47:43.828Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":"zordius/lightncandy","license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/devtheorem.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2024-12-29T01:06:52.000Z","updated_at":"2026-03-27T04:05:36.000Z","dependencies_parsed_at":"2025-05-02T06:23:33.048Z","dependency_job_id":"fb69bdae-addc-474f-a096-629f161b12bd","html_url":"https://github.com/devtheorem/php-handlebars","commit_stats":null,"previous_names":["theodorejb/lightncandy","devtheorem/php-handlebars"],"tags_count":14,"template":false,"template_full_name":null,"purl":"pkg:github/devtheorem/php-handlebars","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devtheorem%2Fphp-handlebars","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devtheorem%2Fphp-handlebars/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devtheorem%2Fphp-handlebars/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devtheorem%2Fphp-handlebars/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/devtheorem","download_url":"https://codeload.github.com/devtheorem/php-handlebars/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devtheorem%2Fphp-handlebars/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31313488,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-02T12:59:32.332Z","status":"ssl_error","status_checked_at":"2026-04-02T12:54:48.875Z","response_time":89,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-04-02T18:51:50.224Z","updated_at":"2026-04-28T05:03:34.948Z","avatar_url":"https://github.com/devtheorem.png","language":"PHP","readme":"# PHP Handlebars\n\nA blazing fast, spec-compliant PHP implementation of [Handlebars](https://handlebarsjs.com).\n\nThe syntax of Handlebars is generally a superset of Mustache, so in most cases it is\npossible to swap out Mustache for Handlebars and continue using the same templates.\n\n## Features\n\n* Supports all Handlebars syntax and language features, including expressions, subexpressions, helpers,\npartials, hooks, `@data` variables, whitespace control, and `.length` on arrays.\n* Templates are parsed using [PHP Handlebars Parser](https://github.com/devtheorem/php-handlebars-parser),\nwhich implements the same lexical analysis and AST grammar specification as Handlebars.js.\n* Tested against the [Handlebars.js spec](https://github.com/jbboehr/handlebars-spec)\n  and the [Mustache spec](https://github.com/mustache/spec).\n\n## Performance\n\nPHP Handlebars started as a fork of [LightnCandy](https://github.com/zordius/lightncandy),\nbut has been rewritten with an AST-based parser and optimized runtime to enable full\nHandlebars.js compatibility with better performance.\n\nPHP Handlebars compiles and executes complex templates over 40% faster than LightnCandy, with 60% lower memory usage:\n\n| Library            | Compile time | Runtime | Total time | Peak memory usage |\n|--------------------|--------------|---------|------------|-------------------|\n| LightnCandy 1.2.6  | 5.2 ms       | 2.8 ms  | 8.0 ms     | 5.3 MB            |\n| PHP Handlebars 2.0 | 3.0 ms       | 1.4 ms  | 4.4 ms     | 1.8 MB            |\n\n_Tested on PHP 8.5 with the JIT enabled. See the `benchmark` branch to run the same test._\n\n## Installation\n```\ncomposer require devtheorem/php-handlebars\n```\n\n## Usage\n```php\nuse DevTheorem\\Handlebars\\Handlebars;\n\n$source = \u003c\u003c\u003c'HBS'\n    \u003cp\u003eHi {{user.name}}, you have {{notifications.length}} new notification(s):\u003c/p\u003e\n    \u003cul\u003e\n    {{#notifications}}\n        \u003cli\u003e{{count}} {{message}} ({{time}})\u003c/li\u003e\n    {{/notifications}}\n    \u003c/ul\u003e\n    HBS;\n\n$data = [\n    'user' =\u003e ['name' =\u003e 'Jane'],\n    'notifications' =\u003e [\n        ['count' =\u003e 4, 'message' =\u003e 'new comments', 'time' =\u003e '5 min ago'],\n        ['count' =\u003e 3, 'message' =\u003e 'new followers', 'time' =\u003e '1 hr ago'],\n    ],\n];\n\n$template = Handlebars::compile($source);\necho $template($data);\n```\n\nOutput:\n```html\n\u003cp\u003eHi Jane, you have 2 new notification(s):\u003c/p\u003e\n\u003cul\u003e\n    \u003cli\u003e4 new comments (5 min ago)\u003c/li\u003e\n    \u003cli\u003e3 new followers (1 hr ago)\u003c/li\u003e\n\u003c/ul\u003e\n```\n\n## Precompilation\n\nTemplates and partials can be precompiled to native PHP for later execution,\navoiding the overhead of parsing and compilation on each request.\n\n**Build step** - compile all templates in a directory and cache the generated PHP:\n\n```php\nuse DevTheorem\\Handlebars\\Handlebars;\n\n$templateDir = 'templates';\n$cacheDir = 'templateCache';\n\nforeach (glob(\"$templateDir/*.hbs\") ?: [] as $file) {\n    $name = basename($file, '.hbs');\n    $code = Handlebars::precompile(file_get_contents($file));\n    file_put_contents(\"$cacheDir/$name.php\", \"\u003c?php $code\");\n}\n```\n\n**Runtime** - load only needed templates, with precompiled partials resolved on demand:\n\n```php\n$template = require 'templateCache/page.php';\n\n$data = ['title' =\u003e 'My Page', 'user' =\u003e ['name' =\u003e 'Jane']];\necho $template($data, [\n    'partialResolver' =\u003e fn(string $name) =\u003e require \"templateCache/$name.php\",\n]);\n```\n\nEach `{{\u003e partial}}` call triggers the resolver on first use, and the result is cached for\nthe rest of that render. Only the partials that the page actually references are ever loaded.\n\n\u003e [!IMPORTANT]  \n\u003e Precompiled templates must be regenerated whenever PHP Handlebars is updated, as the generated\n\u003e PHP code depends on the current version of the runtime. The build step above should be part of\n\u003e a deployment process so that precompiled output does not need to be committed to source control.\n\n## Compile Options\n\nYou can alter the template compilation by passing an `Options` instance as the second argument to `compile` or `precompile`.\nFor example, the `strict` option may be set to `true` to generate a template which will throw an exception for missing data:\n\n```php\nuse DevTheorem\\Handlebars\\{Handlebars, Options};\n\n$template = Handlebars::compile('Hi {{first}} {{last}}!', new Options(\n    strict: true,\n));\n\necho $template(['first' =\u003e 'John']); // Error: \"last\" not defined\n```\n\n### Available Options\n\n* `compat`: Set to `true` to enable recursive field lookup. If a template variable is not found in the current scope,\n  it will automatically be looked up in parent scopes, matching Mustache's default behavior.\n\n\u003e [!NOTE]  \n\u003e Recursive lookup has a runtime cost, so it is recommended that performance-sensitive\n\u003e operations should avoid `compat` mode and instead opt for explicit path references.\n\n* `knownHelpers`: Associative array (`helperName =\u003e bool`) of helpers that will be registered at runtime.\n  The compiler uses this to emit direct helper calls instead of dynamic dispatch,\n  which is faster and required when `knownHelpersOnly` is set.\n  Built-in helpers (`if`, `unless`, `each`, `with`, `lookup`, `log`) are pre-populated as `true` and may be excluded\n  by setting them to `false`. Setting `if` or `unless` to `false` also disables the inline ternary optimization and\n  allows those helpers to be overridden at runtime.\n\n* `knownHelpersOnly`: Restricts templates to only the helpers in `knownHelpers`, enabling further compile-time optimizations:\n  block sections and bare `{{identifier}}` expressions skip the runtime helper table and use a direct context lookup,\n  and any use of an unknown helper throws a compile-time exception instead of falling back to dynamic dispatch.\n\n* `noEscape`: Set to `true` to disable HTML escaping of output.\n\n* `strict`: Run in strict mode. In this mode, templates will throw rather than silently ignore missing fields.\n  This has the side effect of disabling inverse operations such as `{{^foo}}{{/foo}}`\n  unless fields are explicitly included in the source object.\n\n* `assumeObjects`: A looser alternative to `strict` mode. A null intermediate in a path\n  (e.g. `foo` is null when resolving `foo.bar`) throws an exception, but a missing terminal key returns null silently.\n\n* `preventIndent`: Prevents an indented partial call from indenting the entire partial output by the same amount.\n\n* `ignoreStandalone`: Disables standalone tag removal.\n  When set, blocks and partials that are on their own line will not remove the whitespace on that line.\n\n* `explicitPartialContext`: Disables implicit context for partials.\n  When enabled, partials that are not passed a context value will execute against an empty object.\n\n## Runtime Options\n\n`Handlebars::compile` returns a closure which can be invoked as `$template($context, $options)`.\nThe `$options` parameter takes an array of runtime options, accepting the following keys:\n\n* `data`: An associative array of custom `@data` variables (e.g. `['version' =\u003e '1.0']` makes `@version` available in the template).\n\n* `helpers`: An `array\u003cstring, Closure\u003e` of helpers to merge with the built-in helpers.\n  Can also be used to override a built-in helper by using the same name.\n\n* `partials`: An `array\u003cstring, Closure\u003e` of partials compiled with `Handlebars::compile`.\n  Useful for eagerly providing a known set of partials.\n\n* `partialResolver`: A `Closure(string $name): ?Closure` called lazily when a partial is referenced\n  but not found in the `partials` map. Should return a compiled partial closure, or `null` if the partial\n  does not exist. The resolved closure is cached for the remainder of the render, so each partial is loaded\n  at most once per template invocation.\n\n## Custom Helpers\n\nHelper functions will be passed any arguments provided to the helper in the template.\nIf needed, a final `$options` parameter can be included which will be passed a `HelperOptions` instance.\n\nFor example, a custom `#equals` helper with JS equality semantics could be implemented as follows:\n\n```php\nuse DevTheorem\\Handlebars\\{Handlebars, HelperOptions};\n\n$template = Handlebars::compile('{{#equals my_var false}}Equal to false{{else}}Not equal{{/equals}}');\n$helpers = [\n    'equals' =\u003e function (mixed $a, mixed $b, HelperOptions $options) {\n        // In JS, null is not equal to blank string or false or zero,\n        // and when both operands are strings no coercion is performed.\n        $equal = ($a === null || $b === null || is_string($a) \u0026\u0026 is_string($b))\n            ? $a === $b\n            : $a == $b;\n\n        return $equal ? $options-\u003efn() : $options-\u003einverse();\n    },\n];\n$runtimeOptions = ['helpers' =\u003e $helpers];\n\necho $template(['my_var' =\u003e 0], $runtimeOptions); // Equal to false\necho $template(['my_var' =\u003e 1], $runtimeOptions); // Not equal\necho $template(['my_var' =\u003e null], $runtimeOptions); // Not equal\n```\n\n### HelperOptions Properties\n\n* `name` (readonly `string`): The helper name as it appeared in the template.\n  Useful in `helperMissing`/`blockHelperMissing` hooks to identify which name was called.\n\n* `hash` (readonly `array`): Key/value pairs passed as hash arguments in the template\n  (e.g. `{{helper foo=1 bar=\"x\"}}` produces `['foo' =\u003e 1, 'bar' =\u003e 'x']`).\n\n* `blockParams` (readonly `int`): The number of block parameters declared by the helper call\n  (e.g. `{{#helper as |a b|}}` produces `2`).\n\n* `scope` (`mixed`): The current evaluation context (equivalent to `this` in a Handlebars.js helper).\n\n* `data` (`array`): The current `@data` frame. The `root` key refers to the top-level context.\n  `index`, `key`, `first`, and `last` are set by `{{#each}}` blocks. Can be read or modified inside a helper.\n\n### HelperOptions Methods\n\n* `fn(mixed $context = \u003ccurrent scope\u003e, mixed $data = null): string`: Renders the block body.\n  Pass a new context as `$context` to change what the block renders against (equivalent to `options.fn(newContext)` in JS).\n  Pass a `$data` array with a `'data'` key to inject `@`-prefixed variables into the block,\n  and/or a `'blockParams'` key containing an array of values to expose as block parameters.\n\n* `inverse(mixed $context = \u003ccurrent scope\u003e, mixed $data = null): string`: Renders the `{{else}}` / inverse block.\n  Returns an empty string if no inverse block was provided.\n  Accepts the same optional `$context` and `$data` arguments as `fn()`.\n\n* `hasPartial(string $name): bool`: Returns `true` if a partial with the given name is registered.\n  Useful alongside `registerPartial()` to implement dynamic partial loading.\n\n* `registerPartial(string $name, Closure $partial): void`: Registers a compiled partial closure for the\n  remainder of the render. The closure can be produced via `Handlebars::compile`, or by importing a\n  cached closure created with `Handlebars::precompile`.\n\n\u003e [!NOTE]  \n\u003e `isset($options-\u003efn)` and `isset($options-\u003einverse)` return `true` if the helper was called as a block,\n\u003e and `false` for inline helper calls.\n\n## Hooks\n\nIf a custom helper named `helperMissing` is defined, it will be called when a mustache or a block-statement\nis not a registered helper AND is not a property of the current evaluation context.\n\nIf a custom helper named `blockHelperMissing` is defined, it will be called when a block-expression calls\na helper that is not registered, even when the name matches a property in the current evaluation context.\n\nFor example:\n\n```php\nuse DevTheorem\\Handlebars\\{Handlebars, HelperOptions};\n\n$template = Handlebars::compile('{{foo 2 \"value\"}}\n{{#person}}{{firstName}} {{lastName}}{{/person}}');\n\n$helpers = [\n    'helperMissing' =\u003e function (...$args) {\n        $options = array_pop($args);\n        return \"Missing {$options-\u003ename}(\" . implode(',', $args) . ')';\n    },\n    'blockHelperMissing' =\u003e function (mixed $context, HelperOptions $options) {\n        return \"'{$options-\u003ename}' not found. Printing block: {$options-\u003efn($context)}\";\n    },\n];\n\n$data = ['person' =\u003e ['firstName' =\u003e 'John', 'lastName' =\u003e 'Doe']];\necho $template($data, ['helpers' =\u003e $helpers]);\n```\nOutput:\n\u003e Missing foo(2,value)  \n\u003e 'person' not found. Printing block: John Doe\n\n## String Escaping\n\nIf a custom helper is executed in a `{{ }}` expression, the return value will be HTML escaped.\nWhen a helper is executed in a `{{{ }}}` expression, the original return value will be output directly.\n\nHelpers may return a `DevTheorem\\Handlebars\\SafeString` instance to prevent escaping the return value.\nBecause `SafeString` bypasses the automatic HTML escaping that `{{ }}` applies, any user-supplied content\nembedded in it must first be escaped with `Handlebars::escapeExpression()` to prevent XSS vulnerabilities.\n\n## Data Frames\n\nBlock helpers that inject `@`-prefixed variables should create a child data frame using\n`Handlebars::createFrame($options-\u003edata)`, add their variables to it, and pass it to `fn()` or `inverse()`\nvia the `data` key (e.g. `$options-\u003efn($context, ['data' =\u003e $frame])`). This mirrors `Handlebars.createFrame()`\nin Handlebars.js, isolating the helper's variables while still inheriting parent data such as `@root`.\n\n## Missing Features\n\nAll syntax and language features from Handlebars.js 4.7.9 should work the same in PHP Handlebars,\nwith the following exceptions:\n\n* Custom Decorators have not been implemented, as they are [deprecated in Handlebars.js](https://github.com/handlebars-lang/handlebars.js/blob/master/docs/decorators-api.md).\n* The `data` compilation option has not been implemented.\n* The [runtime options to control prototype access](https://handlebarsjs.com/api-reference/runtime-options.html#options-to-control-prototype-access),\nalong with the `lookupProperty()` helper option method have not been implemented, since they aren't relevant for PHP. \n\n## Mustache Compatibility\n\nHandlebars is largely compatible with Mustache syntax, with a few notable differences:\n\n- Handlebars does not perform recursive field lookup by default.\n  The `compat` compile option must be set to enable this behavior.\n- Alternative Mustache delimiters (e.g. `{{=\u003c% %\u003e=}}`) are not supported.\n- Spaces are not allowed between the opening `{{` and a command character such as `#`, `/`, or `\u003e`.\n  For example, `{{\u003e partial}}` works but `{{ \u003e partial}}` does not.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdevtheorem%2Fphp-handlebars","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdevtheorem%2Fphp-handlebars","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdevtheorem%2Fphp-handlebars/lists"}