Skip to content

Commit 5526b37

Browse files
committed
Release 1.1.0
1 parent fc84799 commit 5526b37

2 files changed

Lines changed: 123 additions & 50 deletions

File tree

CHANGELOG.md

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,34 @@ All notable changes to this project will be documented in this file.
33

44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
55

6+
## [1.1.0] Dynamic Partial Resolution - 2026-03-26
7+
8+
### Added
9+
- `HelperOptions::hasPartial()`: check whether a named partial is registered at runtime.
10+
- `HelperOptions::registerPartial()`: register a compiled partial closure from within a helper,
11+
enabling the same lazy-loading pattern as `Handlebars.registerPartial()` in Handlebars.js
12+
([#5](https://github.com/devtheorem/php-handlebars/issues/5), https://github.com/zordius/lightncandy/issues/296).
13+
14+
### Fixed
15+
- Nested `{{> @partial-block}}` calls from runtime partials.
16+
- Failover rendering for `{{> partial}}fallback{{/partial}}` blocks where the partial is also
17+
called conditionally earlier in the template.
18+
- `isset($options->fn)` and `isset($options->inverse)` now correctly return `true` for all block
19+
helper calls, even when the block is inverted or lacks an `{{else}}` clause.
20+
- Closures at complex paths without any arguments (e.g. `{{#obj.fn}}`) are no longer passed a
21+
`HelperOptions` argument (matching Handlebars.js behavior).
22+
- Inverted sections with literal block paths (e.g. `{{^"foo"}}`) now correctly route through
23+
`blockHelperMissing`.
24+
- With `knownHelpersOnly` enabled, inverted sections now correctly skip dispatch to unregistered runtime helpers.
25+
- `../` expressions inside an `{{else}}` body now correctly resolve to the block helper's scope
26+
when there is no enclosing block context.
27+
- `.length` lookup on block param variables and in `strict` mode.
28+
29+
630
## [1.0.1] Root SubExpression - 2026-03-24
731
### Fixed
832
- Support for sub-expressions that are `PathExpression` roots (e.g. `{{(my-helper foo).bar}}`).
9-
- Compilation of multi-segment `if`/`unless` conditions (https://github.com/devtheorem/php-handlebars/issues/15).
33+
- Compilation of multi-segment `if`/`unless` conditions ([#15](https://github.com/devtheorem/php-handlebars/issues/15)).
1034
- Helper argument handling in `strict` mode.
1135
- `assumeObjects` errors now align better with Handlebars.js.
1236

@@ -40,7 +64,7 @@ and uses almost 30% less memory. The code is also significantly simpler and easi
4064
renders, and removes limitations on what they can access and do
4165
(e.g. it resolves https://github.com/zordius/lightncandy/issues/342).
4266
- Exceptions thrown by custom helpers are no longer caught and re-thrown, so the original exception
43-
can now be caught in your own code for easier debugging (https://github.com/devtheorem/php-handlebars/issues/13).
67+
can now be caught in your own code for easier debugging ([#13](https://github.com/devtheorem/php-handlebars/issues/13)).
4468
- The `partialResolver` closure signature no longer receives an internal `Context` argument.
4569
Now only the partial name is passed.
4670
- `knownHelpersOnly` now works as in Handlebars.js, and an exception will be thrown if the template
@@ -53,8 +77,8 @@ and uses almost 30% less memory. The code is also significantly simpler and easi
5377
- `Options::$helperResolver`: use the `helperMissing` / `blockHelperMissing` runtime helpers instead.
5478

5579
### Fixed
56-
- Fatal error with deeply nested `else if` using custom helper (https://github.com/devtheorem/php-handlebars/issues/2).
57-
- Incorrect rendering of float values (https://github.com/devtheorem/php-handlebars/issues/11).
80+
- Fatal error with deeply nested `else if` using custom helper ([#2](https://github.com/devtheorem/php-handlebars/issues/2)).
81+
- Incorrect rendering of float values ([#11](https://github.com/devtheorem/php-handlebars/issues/11)).
5882
- Conditional `@partial-block` expressions.
5983
- Support for `@partial-block` in nested partials (https://github.com/zordius/lightncandy/issues/292).
6084
- Ability to precompile partials and pass them at runtime (https://github.com/zordius/lightncandy/issues/341).
@@ -158,6 +182,7 @@ Initial release after forking from LightnCandy 1.2.6.
158182
- HTML documentation.
159183
- Dozens of unnecessary feature flags.
160184

185+
[1.1.0]: https://github.com/devtheorem/php-handlebars/compare/v1.0.1...v1.1.0
161186
[1.0.1]: https://github.com/devtheorem/php-handlebars/compare/v1.0.0...v1.0.1
162187
[1.0.0]: https://github.com/devtheorem/php-handlebars/compare/v0.9.9...v1.0.0
163188
[0.9.9]: https://github.com/devtheorem/php-handlebars/compare/v0.9.8...v0.9.9

README.md

Lines changed: 94 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@ PHP Handlebars compiles and executes complex templates up to 40% faster than Lig
1010
| Library | Compile time | Runtime | Total time | Peak memory usage |
1111
|--------------------|--------------|---------|------------|-------------------|
1212
| LightnCandy 1.2.6 | 5.2 ms | 2.8 ms | 8.0 ms | 5.3 MB |
13-
| PHP Handlebars 1.0 | 3.5 ms | 1.6 ms | 5.1 ms | 3.6 MB |
13+
| PHP Handlebars 1.1 | 3.5 ms | 1.6 ms | 5.1 ms | 3.6 MB |
1414

1515
_Tested on PHP 8.5 with the JIT enabled. See the `benchmark` branch to run the same test._
1616

1717
## Features
1818

1919
* Supports all Handlebars syntax and language features, including expressions, subexpressions, helpers,
20-
partials, hooks, and `@data` variables.
20+
partials, hooks, `@data` variables, whitespace control, and `.length` on arrays.
2121
* Templates are parsed using [PHP Handlebars Parser](https://github.com/devtheorem/php-handlebars-parser),
2222
which implements the same lexical analysis and AST grammar specification as Handlebars.js.
2323
* Tested against the full [Handlebars.js spec](https://github.com/jbboehr/handlebars-spec).
@@ -70,67 +70,116 @@ echo $template(['first' => 'John']); // Error: "last" not defined
7070

7171
### Available Options
7272

73-
* `knownHelpers`: Associative array (`helperName => bool`) of helpers known to exist at template execution time.
74-
Passing this allows the compiler to optimize a number of cases.
75-
Builtin helpers are automatically included in this list and may be omitted by setting that value to `false`.
76-
* `knownHelpersOnly`: Enable to allow further optimizations based on the known helpers list.
77-
* `noEscape`: Enable to not HTML escape any content.
73+
* `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.
77+
78+
* `knownHelpersOnly`: Restricts templates to only the helpers in `knownHelpers`, enabling further compile-time optimizations:
79+
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.
81+
82+
* `noEscape`: Set to `true` to disable HTML escaping of output.
83+
7884
* `strict`: Run in strict mode. In this mode, templates will throw rather than silently ignore missing fields.
7985
This has the side effect of disabling inverse operations such as `{{^foo}}{{/foo}}`
8086
unless fields are explicitly included in the source object.
81-
* `assumeObjects`: Removes object existence checks when traversing paths.
82-
This is a subset of strict mode that generates optimized templates when the data inputs are known to be safe.
83-
* `preventIndent`: Prevent indented partial-call from indenting the entire partial output by the same amount.
87+
88+
* `assumeObjects`: A looser alternative to `strict` mode. A null intermediate in a path
89+
(e.g. `foo` is null when resolving `foo.bar`) throws an exception, but a missing terminal key returns null silently.
90+
91+
* `preventIndent`: Prevents an indented partial call from indenting the entire partial output by the same amount.
92+
8493
* `ignoreStandalone`: Disables standalone tag removal.
8594
When set, blocks and partials that are on their own line will not remove the whitespace on that line.
95+
8696
* `explicitPartialContext`: Disables implicit context for partials.
8797
When enabled, partials that are not passed a context value will execute against an empty object.
88-
* `partials`: Provide a `name => value` array of custom partial template strings.
89-
* `partialResolver`: A closure which will be called for any partial not in the `partials` array to return a template for it.
98+
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.
90103

91104
## Runtime Options
92105

93106
`Handlebars::compile` returns a closure which can be invoked as `$template($context, $options)`.
94107
The `$options` parameter takes an array of runtime options, accepting the following keys:
95108

96-
* `data`: An array to define custom `@variable` private variables.
97-
* `helpers`: An `array<string, \Closure>` containing custom helpers to add to the built-in helpers.
98-
* `partials`: An `array<string, \Closure>` containing partial functions precompiled with `Handlebars::compile`.
99-
This is useful if multiple templates sharing the same partials need to be compiled and rendered, and you don't want
100-
to recompile the same partials over and over for each template.
109+
* `data`: An associative array of initial `@data` variables (e.g. `['version' => '1.0']` makes `@version` available in the template).
110+
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.
112+
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.
101115

102116
## Custom Helpers
103117

104118
Helper functions will be passed any arguments provided to the helper in the template.
105119
If needed, a final `$options` parameter can be included which will be passed a `HelperOptions` instance.
106-
This object contains properties for accessing `hash` arguments, `data`, and the current `scope`, `name`,
107-
as well as `fn()` and `inverse()` methods to render the block and else contents, respectively.
108120

109121
For example, a custom `#equals` helper with JS equality semantics could be implemented as follows:
110122

111123
```php
112124
use DevTheorem\Handlebars\{Handlebars, HelperOptions};
113125

114126
$template = Handlebars::compile('{{#equals my_var false}}Equal to false{{else}}Not equal{{/equals}}');
115-
$options = [
116-
'helpers' => [
117-
'equals' => function (mixed $a, mixed $b, HelperOptions $options) {
118-
// In JS, null is not equal to blank string or false or zero,
119-
// and when both operands are strings no coercion is performed.
120-
$equal = ($a === null || $b === null || is_string($a) && is_string($b))
121-
? $a === $b
122-
: $a == $b;
123-
124-
return $equal ? $options->fn() : $options->inverse();
125-
},
126-
],
127+
$helpers = [
128+
'equals' => function (mixed $a, mixed $b, HelperOptions $options) {
129+
// In JS, null is not equal to blank string or false or zero,
130+
// and when both operands are strings no coercion is performed.
131+
$equal = ($a === null || $b === null || is_string($a) && is_string($b))
132+
? $a === $b
133+
: $a == $b;
134+
135+
return $equal ? $options->fn() : $options->inverse();
136+
},
127137
];
138+
$runtimeOptions = ['helpers' => $helpers];
128139

129-
echo $template(['my_var' => 0], $options); // Equal to false
130-
echo $template(['my_var' => 1], $options); // Not equal
131-
echo $template(['my_var' => null], $options); // Not equal
140+
echo $template(['my_var' => 0], $runtimeOptions); // Equal to false
141+
echo $template(['my_var' => 1], $runtimeOptions); // Not equal
142+
echo $template(['my_var' => null], $runtimeOptions); // Not equal
132143
```
133144

145+
### HelperOptions Properties
146+
147+
* `name` (readonly `string`): The helper name as it appeared in the template.
148+
Useful in `helperMissing`/`blockHelperMissing` hooks to identify which name was called.
149+
150+
* `hash` (readonly `array`): Key/value pairs passed as hash arguments in the template
151+
(e.g. `{{helper foo=1 bar="x"}}` produces `['foo' => 1, 'bar' => 'x']`).
152+
153+
* `blockParams` (readonly `int`): The number of block parameters declared by the helper call
154+
(e.g. `{{#helper as |a b|}}` produces `2`).
155+
156+
* `scope` (`mixed`): The current evaluation context (equivalent to `this` in a Handlebars.js helper).
157+
Can be reassigned inside a helper to change the context passed to `fn()`.
158+
159+
* `data` (`array`): The current `@data` frame. Contains `@`-prefixed private variables such as
160+
`root`, `index`, `key`, `first`, `last`, and `_parent`. Can be read or modified inside a helper.
161+
162+
### HelperOptions Methods
163+
164+
* `fn(mixed $context = <current scope>, mixed $data = null): string`: Renders the block body.
165+
Pass a new context as `$context` to change what the block renders against (equivalent to `options.fn(newContext)` in JS).
166+
Pass a `$data` array with a `'data'` key to inject additional `@`-prefixed variables into the block,
167+
and/or a `'blockParams'` key containing an array of values to expose as block parameters.
168+
169+
* `inverse(mixed $context = null, mixed $data = null): string`: Renders the `{{else}}` / inverse block.
170+
Returns an empty string if no inverse block was provided.
171+
Accepts the same optional `$context` and `$data` arguments as `fn()`.
172+
173+
* `hasPartial(string $name): bool`: Returns `true` if a partial with the given name is registered.
174+
Useful alongside `registerPartial()` to implement lazy partial loading.
175+
176+
* `registerPartial(string $name, \Closure $partial): void`: Registers a compiled partial closure for the
177+
remainder of the render. The closure must be produced by `Handlebars::compile`.
178+
179+
> [!NOTE]
180+
> `isset($options->fn)` and `isset($options->inverse)` return true if the helper was called as a block,
181+
> and `false` for inline helper calls.
182+
134183
## Hooks
135184

136185
If a custom helper named `helperMissing` is defined, it will be called when a mustache or a block-statement
@@ -147,19 +196,18 @@ use DevTheorem\Handlebars\{Handlebars, HelperOptions};
147196
$template = Handlebars::compile('{{foo 2 "value"}}
148197
{{#person}}{{firstName}} {{lastName}}{{/person}}');
149198

150-
$options = [
151-
'helpers' => [
152-
'helperMissing' => function (...$args) {
153-
$options = array_pop($args);
154-
return "Missing {$options->name}(" . implode(',', $args) . ')';
155-
},
156-
'blockHelperMissing' => function (mixed $context, HelperOptions $options) {
157-
return "'{$options->name}' not found. Printing block: {$options->fn($context)}";
158-
},
159-
],
199+
$helpers = [
200+
'helperMissing' => function (...$args) {
201+
$options = array_pop($args);
202+
return "Missing {$options->name}(" . implode(',', $args) . ')';
203+
},
204+
'blockHelperMissing' => function (mixed $context, HelperOptions $options) {
205+
return "'{$options->name}' not found. Printing block: {$options->fn($context)}";
206+
},
160207
];
161208

162-
echo $template(['person' => ['firstName' => 'John', 'lastName' => 'Doe']], $options);
209+
$data = ['person' => ['firstName' => 'John', 'lastName' => 'Doe']];
210+
echo $template($data, ['helpers' => $helpers]);
163211
```
164212
Output:
165213
> Missing foo(2,value)
@@ -176,7 +224,7 @@ using the `Handlebars::escapeExpression()` method to avoid potential security co
176224

177225
## Missing Features
178226

179-
All syntax and language features from Handlebars.js 4.7.8 should work the same in PHP Handlebars,
227+
All syntax and language features from Handlebars.js 4.7.9 should work the same in PHP Handlebars,
180228
with the following exceptions:
181229

182230
* 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).

0 commit comments

Comments
 (0)