Skip to content

Commit 59f5bc2

Browse files
committed
Release 1.0.0
1 parent 6069dc4 commit 59f5bc2

File tree

3 files changed

+101
-30
lines changed

3 files changed

+101
-30
lines changed

CHANGELOG.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,60 @@ 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.0.0] AST Compiler - 2026-03-22
7+
8+
Rewrote the parser and compiler to use an abstract syntax tree, based on the same lexical analysis
9+
and grammar specification as Handlebars.js. This eliminates a large class of edge cases and parsing
10+
bugs that the old regex-based approach failed to handle correctly.
11+
12+
This release is 35-40% faster than v0.9.9 and LightnCandy at compiling and executing complex templates,
13+
and uses almost 30% less memory. The code is also significantly simpler and easier to maintain.
14+
15+
### Added
16+
- Support for nested inline partials.
17+
- Support for closures in data and helper arguments.
18+
- `helperMissing` and `blockHelperMissing` hooks: handle calls to unknown helpers with the same API
19+
as in Handlebars.js, replacing the old `helperResolver` option.
20+
- `knownHelpers` compile option: tell the compiler which helpers will be available at runtime for
21+
more efficient execution (helper existence checks can be skipped).
22+
- `assumeObjects` compile option: a subset of `strict` mode that generates optimized templates when
23+
the data inputs are known to be safe.
24+
- Support for deprecated `{{person/firstname}}` path expressions for parity with Handlebars.js
25+
(avoid using this syntax in new code, though).
26+
27+
### Changed
28+
- Custom helpers must now be passed at runtime when invoking a template (via the `helpers` runtime
29+
option key), rather than via the `Options` object passed to `compile` or `precompile`. This is a
30+
significant optimization, since it eliminates the overhead of reading and tokenizing PHP files to
31+
extract helper functions. It also enables sharing helper closures across multiple templates and
32+
renders, and removes limitations on what they can access and do
33+
(e.g. it resolves https://github.com/zordius/lightncandy/issues/342).
34+
- Exceptions thrown by custom helpers are no longer caught and re-thrown, so the original exception
35+
can now be caught in your own code for easier debugging (https://github.com/devtheorem/php-handlebars/issues/13).
36+
- The `partialResolver` closure signature no longer receives an internal `Context` argument.
37+
Now only the partial name is passed.
38+
- `knownHelpersOnly` now works as in Handlebars.js, and an exception will be thrown if the template
39+
uses a helper which is not in the `knownHelpers` list.
40+
- Updated various error messages to align with those output by Handlebars.js.
41+
42+
### Removed
43+
- `Options::$helpers`: instead pass custom helpers when invoking a template, using the `helpers` key
44+
in the runtime options array (the second argument to the template closure).
45+
- `Options::$helperResolver`: use the `helperMissing` / `blockHelperMissing` runtime helpers instead.
46+
47+
### Fixed
48+
- Fatal error with deeply nested `else if` using custom helper (https://github.com/devtheorem/php-handlebars/issues/2).
49+
- Incorrect rendering of float values (https://github.com/devtheorem/php-handlebars/issues/11).
50+
- Conditional `@partial-block` expressions.
51+
- Support for `@partial-block` in nested partials (https://github.com/zordius/lightncandy/issues/292).
52+
- Ability to precompile partials and pass them at runtime (https://github.com/zordius/lightncandy/issues/341).
53+
- Fatal error when a string parameter to a partial includes curly braces (https://github.com/zordius/lightncandy/issues/316).
54+
- Behavior when modifying root context in a custom helper (https://github.com/zordius/lightncandy/issues/350).
55+
- Escaping of block params and partial names.
56+
- Inline partials defined inside a `{{#with}}` or other block leaking out of that block's scope after it closes.
57+
- Numerous other bugs related to scoping, block params, inverted block helpers, section iteration, and depth-relative paths.
58+
59+
660
## [0.9.9] Stringable Conditions - 2025-10-15
761
### Added
862
- Allow `Stringable` variables in `if` statements ([#8](https://github.com/devtheorem/php-handlebars/pull/8)).
@@ -96,6 +150,7 @@ Initial release after forking from LightnCandy 1.2.6.
96150
- HTML documentation.
97151
- Dozens of unnecessary feature flags.
98152

153+
[1.0.0]: https://github.com/devtheorem/php-handlebars/compare/v0.9.9...v1.0.0
99154
[0.9.9]: https://github.com/devtheorem/php-handlebars/compare/v0.9.8...v0.9.9
100155
[0.9.8]: https://github.com/devtheorem/php-handlebars/compare/v0.9.7...v0.9.8
101156
[0.9.7]: https://github.com/devtheorem/php-handlebars/compare/v0.9.6...v0.9.7

README.md

Lines changed: 40 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,26 @@
11
# PHP Handlebars
22

3-
A blazing fast, spec-compliant PHP implementation of [Handlebars](https://handlebarsjs.com/).
3+
A blazing fast, spec-compliant PHP implementation of [Handlebars](https://handlebarsjs.com).
44

5-
Originally based on [LightnCandy](https://github.com/zordius/lightncandy), but rewritten to focus on
6-
more robust Handlebars.js compatibility without the need for excessive feature flags.
5+
Originally based on [LightnCandy](https://github.com/zordius/lightncandy), but rewritten to enable
6+
full Handlebars.js compatibility without excessive feature flags or performance tradeoffs.
7+
8+
PHP Handlebars compiles and executes complex templates up to 40% faster than LightnCandy:
9+
10+
| Library | Compile time | Runtime | Total time | Peak memory usage |
11+
|--------------------|--------------|---------|------------|-------------------|
12+
| 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 |
14+
15+
_Tested on PHP 8.5 with the JIT enabled. See the `benchmark` branch to run the same test._
716

817
## Features
918

10-
* Compile templates to pure PHP code.
19+
* Supports all Handlebars syntax and language features, including expressions, subexpressions, helpers,
20+
partials, hooks, and `@data` variables.
1121
* Templates are parsed using [PHP Handlebars Parser](https://github.com/devtheorem/php-handlebars-parser),
12-
which implements the same lexical analysis and grammar specification as Handlebars.js.
13-
* Tested against the [Handlebars.js spec](https://github.com/jbboehr/handlebars-spec).
22+
which implements the same lexical analysis and AST grammar specification as Handlebars.js.
23+
* Tested against the full [Handlebars.js spec](https://github.com/jbboehr/handlebars-spec).
1424

1525
## Installation
1626
```
@@ -55,7 +65,7 @@ $template = Handlebars::compile('Hi {{first}} {{last}}!', new Options(
5565
strict: true,
5666
));
5767

58-
echo $template(['first' => 'John']); // Error: Runtime: last does not exist
68+
echo $template(['first' => 'John']); // Error: "last" not defined
5969
```
6070

6171
### Available Options
@@ -75,15 +85,26 @@ echo $template(['first' => 'John']); // Error: Runtime: last does not exist
7585
When set, blocks and partials that are on their own line will not remove the whitespace on that line.
7686
* `explicitPartialContext`: Disables implicit context for partials.
7787
When enabled, partials that are not passed a context value will execute against an empty object.
78-
* `partials`: Provide a `name => value` array of custom partial templates.
88+
* `partials`: Provide a `name => value` array of custom partial template strings.
7989
* `partialResolver`: A closure which will be called for any partial not in the `partials` array to return a template for it.
8090

91+
## Runtime Options
92+
93+
`Handlebars::compile` returns a closure which can be invoked as `$template($context, $options)`.
94+
The `$options` parameter takes an array of runtime options, accepting the following keys:
95+
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.
101+
81102
## Custom Helpers
82103

83104
Helper functions will be passed any arguments provided to the helper in the template.
84105
If needed, a final `$options` parameter can be included which will be passed a `HelperOptions` instance.
85-
This object contains properties for accessing `hash` arguments, `data`, and the current `scope`, as well as
86-
`fn()` and `inverse()` methods to render the block and else contents, respectively.
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.
87108

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

@@ -94,17 +115,13 @@ $template = Handlebars::compile('{{#equals my_var false}}Equal to false{{else}}N
94115
$options = [
95116
'helpers' => [
96117
'equals' => function (mixed $a, mixed $b, HelperOptions $options) {
97-
$jsEquals = function (mixed $a, mixed $b): bool {
98-
if ($a === null || $b === null || is_string($a) && is_string($b)) {
99-
// In JS, null is not equal to blank string or false or zero,
100-
// and when both operands are strings no coercion is performed.
101-
return $a === $b;
102-
}
103-
104-
return $a == $b;
105-
};
106-
107-
return $jsEquals($a, $b) ? $options->fn() : $options->inverse();
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();
108125
},
109126
],
110127
];
@@ -164,3 +181,5 @@ with the following exceptions:
164181

165182
* 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).
166183
* The `data` and `compat` compilation options have not been implemented.
184+
* The [runtime options to control prototype access](https://handlebarsjs.com/api-reference/runtime-options.html#options-to-control-prototype-access),
185+
along with the `lookupProperty()` helper option method have not been implemented, since they aren't relevant for PHP.

tests/RegressionTest.php

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -161,16 +161,13 @@ public static function helperProvider(): array
161161
};
162162

163163
$equals = function (mixed $a, mixed $b, HelperOptions $options) {
164-
$jsEquals = function (mixed $a, mixed $b): bool {
165-
if ($a === null || $b === null) {
166-
// in JS, null is not equal to blank string or false or zero
167-
return $a === $b;
168-
}
164+
// In JS, null is not equal to blank string or false or zero,
165+
// and when both operands are strings no coercion is performed.
166+
$equal = ($a === null || $b === null || is_string($a) && is_string($b))
167+
? $a === $b
168+
: $a == $b;
169169

170-
return $a == $b;
171-
};
172-
173-
return $jsEquals($a, $b) ? $options->fn() : $options->inverse();
170+
return $equal ? $options->fn() : $options->inverse();
174171
};
175172

176173
return [

0 commit comments

Comments
 (0)