Skip to content

Commit 55ac158

Browse files
committed
Switch to objects (experimental)
1 parent 74b4ca7 commit 55ac158

11 files changed

Lines changed: 297 additions & 143 deletions

php_handlebars_template.php

Lines changed: 33 additions & 33 deletions
Large diffs are not rendered by default.

phpunit.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
4+
bootstrap="tests/bootstrap.php">
5+
<testsuites>
6+
<testsuite name="default">
7+
<directory>tests</directory>
8+
</testsuite>
9+
</testsuites>
10+
</phpunit>

src/Compiler.php

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ public function compile(Program $program, Context $context): string
7979
public function composePHPRender(string $code): string
8080
{
8181
$partials = implode(",\n", $this->context->partialCode);
82-
$closure = self::templateClosure($code, $partials, "\n \$in = &\$cx->data['root'];");
82+
$closure = self::templateClosure($code, $partials, "\n \$in = &\$cx->data->root;");
8383
return "use " . Runtime::class . " as LR;\nreturn $closure;";
8484
}
8585

@@ -590,7 +590,9 @@ private function PathExpression(PathExpression $expression): string
590590
// sub-expression as the base and use the string tail as the remaining key accesses.
591591
$hasSubExprHead = $expression->head instanceof SubExpression;
592592
if ($hasSubExprHead) {
593-
$base = '(' . $this->SubExpression($expression->head) . ')';
593+
// Normalize the sub-expression result so PHP array returns become ArrayContext and
594+
// can be accessed via ->{'key'} property syntax.
595+
$base = 'LR::normalizeContext(' . $this->SubExpression($expression->head) . ')';
594596
$stringParts = $expression->tail;
595597
} else {
596598
$base = $this->buildBasePath($data, $depth);
@@ -818,7 +820,7 @@ private function getSimpleHelperName(PathExpression|Literal $path): ?string
818820
private function buildBasePath(bool $data, int $depth): string
819821
{
820822
if ($data) {
821-
return '$cx->data' . str_repeat("['_parent']", $depth);
823+
return '$cx->data' . str_repeat('->_parent', $depth);
822824
}
823825
return $depth > 0 ? "\$cx->depths[count(\$cx->depths)-$depth]" : '$in';
824826
}
@@ -848,15 +850,15 @@ private static function buildCallChain(string $fn, string $base, array $parts, ?
848850
}
849851

850852
/**
851-
* Build a chained array-access string for the given path parts.
852-
* e.g. ['foo', 'bar'] → "['foo']['bar']"
853+
* Build a chained object-property-access string for the given path parts.
854+
* e.g. ['foo', 'bar'] → "->{'foo'}->{'bar'}"
853855
* @param string[] $parts
854856
*/
855857
private static function buildKeyAccess(array $parts): string
856858
{
857859
$n = '';
858860
foreach ($parts as $part) {
859-
$n .= '[' . self::quote($part) . ']';
861+
$n .= '->{' . self::quote($part) . '}';
860862
}
861863
return $n;
862864
}
@@ -908,10 +910,11 @@ private function compileModeAwareLookup(string $base, array $parts, string $orig
908910
return $base;
909911
}
910912
if ($this->context->options->assumeObjects || ($this->context->options->strict && $this->compilingHelperArgs)) {
911-
// Use nullCheck chain for assumeObjects and helper arguments in strict mode.
912-
// This mirrors HBS.js: both paths use bare nameLookup, so only a null intermediate throws
913-
// (JS TypeError), while a missing key on a valid object returns null silently (JS undefined).
914-
return self::buildCallChain('nullCheck', $base, $parts);
913+
// Bare access without ??: a null intermediate triggers a PHP warning that the null-property
914+
// handler converts to an ErrorException (PHP 8), or a native TypeError (PHP 9).
915+
// This mirrors HBS.js assumeObjects: missing keys on valid objects return null silently
916+
// (stdClass property access), while null intermediates throw.
917+
return $base . self::buildKeyAccess($parts);
915918
}
916919
if ($this->context->options->strict) {
917920
return self::buildCallChain('strictLookup', $base, $parts, self::quote($original));

src/Handlebars.php

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,13 @@ public static function template(string $templateSpec): \Closure
4646

4747
/**
4848
* Creates a child @data frame inheriting fields from the given frame.
49-
* Use this in block helpers before passing a data array to fn() or inverse(),
49+
* Use this in block helpers before passing a data object to fn() or inverse(),
5050
* equivalent to Handlebars.createFrame() in Handlebars.js.
51-
* @param array<mixed> $data
52-
* @return array<mixed>
5351
*/
54-
public static function createFrame(array $data): array
52+
public static function createFrame(\stdClass $data): \stdClass
5553
{
56-
$frame = $data;
57-
$frame['_parent'] = $data;
54+
$frame = clone $data;
55+
$frame->_parent = $data;
5856
return $frame;
5957
}
6058

src/HelperOptions.php

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,12 @@ enum Scope
1717
class HelperOptions
1818
{
1919
/**
20-
* @param array<mixed> $data
2120
* @param array<mixed> $hash
2221
* @param array<mixed> $outerBlockParams outer block param stack, passed as trailing elements of the stack
2322
*/
2423
public function __construct(
2524
public mixed &$scope,
26-
public array &$data,
25+
public \stdClass &$data,
2726
private readonly RuntimeContext $cx,
2827
public readonly string $name = '',
2928
public readonly array $hash = [],
@@ -96,18 +95,37 @@ private function invokeBlock(\Closure $closure, mixed $context, mixed $data): st
9695
// Skip depths push when the caller explicitly passes the current scope (equivalent to
9796
// HBS.js options.fn(this) / options.inverse(this)), since the scope level isn't changing.
9897
$pushDepths = $context !== $scope;
99-
$resolvedContext = $pushDepths ? ($context === Scope::Use ? $scope : $context) : $scope;
98+
$resolvedContext = $pushDepths
99+
? ($context === Scope::Use ? $scope : Runtime::normalizeContext($context))
100+
: $scope;
100101
$outerFrame = null;
101102
$bpStack = null;
102103

103-
if (isset($data['data'])) {
104+
// Accept legacy array data arg for backward compatibility.
105+
if (is_array($data)) {
106+
$data = (object) $data;
107+
}
108+
109+
if (isset($data->data)) {
104110
$outerFrame = $cx->data;
105-
$cx->data = $data['data'];
111+
if (is_array($data->data)) {
112+
$frame = new DataFrame();
113+
foreach ($data->data as $k => $v) {
114+
$frame->$k = $v;
115+
}
116+
$cx->data = $frame;
117+
} else {
118+
$cx->data = $data->data;
119+
}
106120
}
107121

108-
if (isset($data['blockParams'])) {
122+
if (isset($data->blockParams)) {
109123
// Build block params stack: current level prepended to outer stack.
110-
$bpStack = [$data['blockParams'], ...$this->outerBlockParams];
124+
// Normalize each value so block param property access uses stdClass syntax.
125+
$bpStack = [
126+
array_map(Runtime::normalizeContext(...), $data->blockParams),
127+
...$this->outerBlockParams,
128+
];
111129
}
112130

113131
if ($pushDepths) {
@@ -160,18 +178,19 @@ public function iterate(array $items): string
160178
// so the next iteration's assignment is an in-place mutation, not a copy.
161179
$bpStack = [[null, null], ...$this->outerBlockParams];
162180
$data = Handlebars::createFrame($outerFrame);
163-
$data['first'] = true;
181+
$data->first = true;
182+
$cx->data = $data;
164183

165184
foreach ($items as $index => $value) {
166-
$data['key'] = $index;
167-
$data['index'] = $i;
168-
$data['last'] = $i === $last;
169-
$cx->data = $data;
185+
$data->key = $index;
186+
$data->index = $i;
187+
$data->last = $i === $last;
170188

171-
$bpStack[0][0] = $value;
189+
$normalized = Runtime::normalizeContext($value);
190+
$bpStack[0][0] = $normalized;
172191
$bpStack[0][1] = $index;
173-
$ret .= $cb($cx, $value, $bpStack);
174-
$data['first'] = false;
192+
$ret .= $cb($cx, $normalized, $bpStack);
193+
$data->first = false;
175194
$i++;
176195
}
177196

0 commit comments

Comments
 (0)