You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This change enables precompiling all partials in a directory, and then lazily importing them
43
+
on first use for optimal performance. See the example in the readme.
44
+
45
+
### Fixed
46
+
- Failure to invoke `@data` variables containing a closure when passed to `if` or `unless` helpers.
47
+
- Hoisted block closures leaked into the caller's scope when a precompiled template was loaded via `include`/`require`.
48
+
- Hash arguments passed to a partial were ignored when the partial was invoked in certain non-array contexts.
49
+
- Block helpers returning a nested or non-list array were not stringified correctly.
50
+
- Partials with literal names (`{{> true}}`, `{{> false}}`, `{{> null}}`, `{{> undefined}}`) were not
51
+
resolved correctly: boolean names caused a type error, and `null`/`undefined` silently rendered nothing.
52
+
53
+
6
54
## [1.2.3] Hoisted Closures - 2026-04-10
7
55
### Changed
8
56
- Improved rendering performance by hoisting the closures for block bodies and `{{else}}` clauses,
@@ -20,14 +68,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
20
68
fixing numerous edge cases related to helpers and `@data` variables.
21
69
22
70
### Fixed
23
-
-`../` expressions inside `{{else}}` blocks of `{{#if}}`, `{{#unless}}`, `{{#with}}`, and sections invoking `blockHelperMissing` resolved to the wrong context level.
24
-
- A missing helper called via multi-segment path in a subexpression or `@data` variable failed to invoke `helperMissing`.
25
-
- A non-function context property used as a helper (e.g. `{{foo "arg"}}` where `foo` is not a closure) incorrectly called `helperMissing` rather than throwing a distinct error.
71
+
-`../` expressions inside `{{else}}` blocks of `{{#if}}`, `{{#unless}}`, `{{#with}}`,
72
+
and sections invoking `blockHelperMissing` resolved to the wrong context level.
73
+
- A missing helper called via a `@data` variable or multi-segment path in a subexpression failed to invoke `helperMissing`.
74
+
- A non-function context property used as a helper (e.g. `{{foo "arg"}}` where `foo` is not a closure)
75
+
incorrectly called `helperMissing` rather than throwing a distinct error.
26
76
- No error thrown when calling a missing helper via a multi-segment path with arguments (e.g. `{{foo.bar "arg"}}`).
27
77
- Closures in context data could not be used as block helpers (e.g. `{{#fn}}...{{/fn}}` where `fn` is a closure).
28
78
- Closures in context data or `@data` variables failed to be passed `HelperOptions` as the last argument in certain cases.
29
79
- Templates with hash arguments on complex paths (e.g. `{{foo.bar arg=val}}`) were not compiled correctly.
30
-
- Closures in context data were not invoked when accessed via a multi-segment path (e.g. `{{foo.bar}}`), or via a literal path (e.g. `{{"foo"}}`) in `knownHelpersOnly` mode.
80
+
- Closures in context data were not invoked when accessed via a multi-segment path (e.g. `{{foo.bar}}`),
81
+
or via a literal path (e.g. `{{"foo"}}`) in `knownHelpersOnly` mode.
31
82
-`@data` variables incorrectly took priority over helpers with the same name.
32
83
-`knownHelpersOnly` was not enforced for `@data` expressions or complex paths used with arguments.
33
84
@@ -245,6 +296,7 @@ Initial release after forking from LightnCandy 1.2.6.
Each `{{> partial}}` reference triggers the resolver on first use, and the result is cached for
103
+
the rest of that render. Only the partials that the page actually references are ever loaded.
104
+
105
+
> [!IMPORTANT]
106
+
> Precompiled templates must be regenerated whenever PHP Handlebars is updated, as the generated
107
+
> PHP code depends on the current version of the runtime. The build step above should be part of
108
+
> a deployment process so that precompiled output does not need to be committed to source control.
109
+
56
110
## Compile Options
57
111
58
112
You can alter the template compilation by passing an `Options` instance as the second argument to `compile` or `precompile`.
@@ -70,14 +124,23 @@ echo $template(['first' => 'John']); // Error: "last" not defined
70
124
71
125
### Available Options
72
126
127
+
*`compat`: Set to `true` to enable recursive field lookup. If a template variable is not found in the current scope,
128
+
it will automatically be looked up in parent scopes, matching Mustache's default behavior.
129
+
130
+
> [!NOTE]
131
+
> Recursive lookup has a runtime cost, so it is recommended that performance-sensitive
132
+
> operations should avoid `compat` mode and instead opt for explicit path references.
133
+
73
134
*`knownHelpers`: Associative array (`helperName => bool`) of helpers that will be registered at runtime.
74
-
The compiler uses this to emit direct helper calls instead of dynamic dispatch, which is faster and required when `knownHelpersOnly` is set.
75
-
Built-in helpers (`if`, `unless`, `each`, `with`, `lookup`, `log`) are pre-populated as `true` and may be excluded by setting them to `false`.
76
-
Setting `if` or `unless` to `false` also disables the inline ternary optimization and allows those helpers to be overridden at runtime.
135
+
The compiler uses this to emit direct helper calls instead of dynamic dispatch,
136
+
which is faster and required when `knownHelpersOnly` is set.
137
+
Built-in helpers (`if`, `unless`, `each`, `with`, `lookup`, `log`) are pre-populated as `true` and may be excluded
138
+
by setting them to `false`. Setting `if` or `unless` to `false` also disables the inline ternary optimization and
139
+
allows those helpers to be overridden at runtime.
77
140
78
141
*`knownHelpersOnly`: Restricts templates to only the helpers in `knownHelpers`, enabling further compile-time optimizations:
79
142
block sections and bare `{{identifier}}` expressions skip the runtime helper table and use a direct context lookup,
80
-
and any use of an unregistered helper throws a compile-time exception instead of falling back to dynamic dispatch.
143
+
and any use of an unknown helper throws a compile-time exception instead of falling back to dynamic dispatch.
81
144
82
145
*`noEscape`: Set to `true` to disable HTML escaping of output.
83
146
@@ -96,22 +159,23 @@ echo $template(['first' => 'John']); // Error: "last" not defined
96
159
*`explicitPartialContext`: Disables implicit context for partials.
97
160
When enabled, partials that are not passed a context value will execute against an empty object.
98
161
99
-
*`partials`: An associative array of custom partial template strings (`name => template`).
100
-
101
-
*`partialResolver`: A closure that will be called at compile time for any partial not found in the `partials` array,
102
-
and should return a template string for it.
103
-
104
162
## Runtime Options
105
163
106
164
`Handlebars::compile` returns a closure which can be invoked as `$template($context, $options)`.
107
165
The `$options` parameter takes an array of runtime options, accepting the following keys:
108
166
109
167
*`data`: An associative array of custom `@data` variables (e.g. `['version' => '1.0']` makes `@version` available in the template).
110
168
111
-
*`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.
169
+
*`helpers`: An `array<string, Closure>` of helpers to merge with the built-in helpers.
170
+
Can also be used to override a built-in helper by using the same name.
112
171
113
-
*`partials`: An `array<string, \Closure>` of partial closures precompiled with `Handlebars::compile`.
114
-
Useful when multiple templates share the same partials, and you want to avoid recompiling them for each template.
172
+
*`partials`: An `array<string, Closure>` of partials precompiled with `Handlebars::compile`.
173
+
Useful for eagerly providing a known set of partials (e.g. a shared layout).
174
+
175
+
*`partialResolver`: A `Closure(string $name): ?Closure` called lazily when a partial is referenced
176
+
but not found in the `partials` map. Should return a compiled partial closure, or `null` if the partial
177
+
does not exist. The resolved closure is cached for the remainder of the render, so each partial is loaded
*`hasPartial(string $name): bool`: Returns `true` if a partial with the given name is registered.
173
237
Useful alongside `registerPartial()` to implement lazy partial loading.
174
238
175
-
*`registerPartial(string $name, \Closure $partial): void`: Registers a compiled partial closure for the
176
-
remainder of the render. The closure must be produced by `Handlebars::compile`.
239
+
*`registerPartial(string $name, Closure $partial): void`: Registers a compiled partial closure for the
240
+
remainder of the render. The closure can be produced via `Handlebars::compile`, or by importing a
241
+
cached closure created with `Handlebars::precompile`.
177
242
178
243
> [!NOTE]
179
244
> `isset($options->fn)` and `isset($options->inverse)` return `true` if the helper was called as a block,
@@ -234,6 +299,16 @@ All syntax and language features from Handlebars.js 4.7.9 should work the same i
234
299
with the following exceptions:
235
300
236
301
* 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).
237
-
* The `data`and `compat`compilation options have not been implemented.
302
+
* The `data` compilation option has not been implemented.
238
303
* The [runtime options to control prototype access](https://handlebarsjs.com/api-reference/runtime-options.html#options-to-control-prototype-access),
239
304
along with the `lookupProperty()` helper option method have not been implemented, since they aren't relevant for PHP.
305
+
306
+
## Mustache Compatibility
307
+
308
+
Handlebars is largely compatible with Mustache syntax, with a few notable differences:
309
+
310
+
- Handlebars does not perform recursive field lookup by default.
311
+
The `compat` compile option must be set to enable this behavior.
312
+
- Alternative Mustache delimiters (e.g. `{{=<% %>=}}`) are not supported.
313
+
- Spaces are not allowed between the opening `{{` and a command character such as `#`, `/`, or `>`.
314
+
For example, `{{> partial}}` works but `{{ > partial}}` does not.
0 commit comments