@@ -11,6 +11,9 @@ enum Scope
1111 case Use;
1212}
1313
14+ /**
15+ * @phpstan-import-type Template from Handlebars
16+ */
1417class HelperOptions
1518{
1619 /**
@@ -21,57 +24,76 @@ class HelperOptions
2124 public function __construct (
2225 public mixed &$ scope ,
2326 public array &$ data ,
27+ private readonly RuntimeContext $ cx ,
2428 public readonly string $ name = '' ,
2529 public readonly array $ hash = [],
2630 public readonly int $ blockParams = 0 ,
27- private readonly ?RuntimeContext $ cx = null ,
2831 private readonly ?Closure $ cb = null ,
2932 private readonly ?Closure $ inv = null ,
3033 private readonly array $ outerBlockParams = [],
3134 ) {}
3235
3336 /**
34- * Allows isset($options->fn) and isset($options->inverse) to check whether the block exists.
37+ * Returns true if a partial with the given name is registered.
38+ */
39+ public function hasPartial (string $ name ): bool
40+ {
41+ return isset ($ this ->cx ->inlinePartials [$ name ]) || isset ($ this ->cx ->partials [$ name ]);
42+ }
43+
44+ /**
45+ * Registers a compiled partial closure under the given name for the remainder of the render.
46+ * Typically used alongside hasPartial() to implement lazy partial loading.
47+ * @param Template $partial
48+ */
49+ public function registerPartial (string $ name , \Closure $ partial ): void
50+ {
51+ $ this ->cx ->partials [$ name ] = $ partial ;
52+ }
53+
54+ /**
55+ * Supports isset($options->fn) and isset($options->inverse), both of which return true for
56+ * any block helper call and false for inline helper calls (matching Handlebars.js behavior).
3557 */
3658 public function __isset (string $ name ): bool
3759 {
38- if ($ name === 'fn ' ) {
39- return $ this ->cb !== null ;
40- } elseif ($ name === 'inverse ' ) {
41- return $ this ->inv !== null ;
60+ if ($ name === 'fn ' || $ name === 'inverse ' ) {
61+ return $ this ->cb !== null || $ this ->inv !== null ;
4262 }
4363 return false ;
4464 }
4565
4666 public function fn (mixed $ context = Scope::Use, mixed $ data = null ): string
4767 {
48- if ($ this ->cx === null ) {
49- throw new \Exception ('fn() is not supported for inline helpers ' );
50- } elseif ($ this ->cb === null ) {
68+ if ($ this ->cb === null ) {
69+ if ($ this ->inv === null ) {
70+ throw new \Exception ('fn() is not supported for inline helpers ' );
71+ }
5172 return '' ;
5273 }
5374 $ cx = $ this ->cx ;
5475 $ scope = $ this ->scope ;
5576
56- // Save partials so that any {{#* inline}} partials registered inside the block body
77+ // Save inlinePartials so that any {{#* inline}} partials registered inside the block body
5778 // don't leak out after fn() returns. The spec requires inline partials to be
5879 // block-scoped. PHP copy-on-write makes this assignment cheap when no inline partials are registered.
59- $ savedPartials = $ cx ->partials ;
80+ $ savedInlinePartials = $ cx ->inlinePartials ;
6081
6182 // Skip depths push for explicit same-context pass (equivalent to HBS.js options.fn(this))
6283 $ skipDepths = $ context === $ scope ;
6384 $ resolvedContext = $ skipDepths ? $ scope : ($ context === Scope::Use ? $ scope : $ context );
6485 $ ret = $ this ->callBlock ($ this ->cb , $ resolvedContext , !$ skipDepths , $ data );
6586
66- $ cx ->partials = $ savedPartials ;
87+ $ cx ->inlinePartials = $ savedInlinePartials ;
6788 return $ ret ;
6889 }
6990
7091 public function inverse (mixed $ context = null , mixed $ data = null ): string
7192 {
72- if ($ this ->cx === null ) {
73- throw new \Exception ('inverse() is not supported for inline helpers ' );
74- } elseif ($ this ->inv === null ) {
93+ if ($ this ->inv === null ) {
94+ if ($ this ->cb === null ) {
95+ throw new \Exception ('inverse() is not supported for inline helpers ' );
96+ }
7597 return '' ;
7698 }
7799 return $ this ->callBlock ($ this ->inv , $ context ?? $ this ->scope , $ context !== null , $ data );
@@ -81,7 +103,6 @@ public function inverse(mixed $context = null, mixed $data = null): string
81103 private function callBlock (\Closure $ closure , mixed $ context , bool $ pushDepths , ?array $ data ): string
82104 {
83105 $ cx = $ this ->cx ;
84- assert ($ cx !== null );
85106 $ savedFrame = null ;
86107 $ bpStack = null ;
87108
@@ -133,12 +154,11 @@ public function iterate(array $items): string
133154 return '' ;
134155 }
135156 $ cx = $ this ->cx ;
136- assert ($ cx !== null );
137157 $ cb = $ this ->cb ;
138158
139- // Push depths and save partials once for the entire loop.
159+ // Push depths and save inlinePartials once for the entire loop.
140160 $ cx ->depths [] = $ this ->scope ;
141- $ savedPartials = $ cx ->partials ;
161+ $ savedInlinePartials = $ cx ->inlinePartials ;
142162
143163 $ last = count ($ items ) - 1 ;
144164 $ ret = '' ;
@@ -166,7 +186,7 @@ public function iterate(array $items): string
166186
167187 $ cx ->frame = $ outerFrame ;
168188 array_pop ($ cx ->depths );
169- $ cx ->partials = $ savedPartials ;
189+ $ cx ->inlinePartials = $ savedInlinePartials ;
170190 return $ ret ;
171191 }
172192}
0 commit comments