Skip to content

Commit be8d130

Browse files
HypeMCfabpot
authored andcommitted
[ExpressionLanguage] Fix null-safe chaining
1 parent 4d64023 commit be8d130

File tree

2 files changed

+71
-2
lines changed

2 files changed

+71
-2
lines changed

Node/GetAttrNode.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ class GetAttrNode extends Node
2424
public const METHOD_CALL = 2;
2525
public const ARRAY_CALL = 3;
2626

27+
private bool $isShortCircuited = false;
28+
2729
public function __construct(Node $node, Node $attribute, ArrayNode $arguments, int $type)
2830
{
2931
parent::__construct(
@@ -70,9 +72,16 @@ public function evaluate(array $functions, array $values)
7072
switch ($this->attributes['type']) {
7173
case self::PROPERTY_CALL:
7274
$obj = $this->nodes['node']->evaluate($functions, $values);
75+
7376
if (null === $obj && $this->nodes['attribute']->isNullSafe) {
77+
$this->isShortCircuited = true;
78+
79+
return null;
80+
}
81+
if (null === $obj && $this->isShortCircuited()) {
7482
return null;
7583
}
84+
7685
if (!\is_object($obj)) {
7786
throw new \RuntimeException(sprintf('Unable to get property "%s" of non-object "%s".', $this->nodes['attribute']->dump(), $this->nodes['node']->dump()));
7887
}
@@ -83,9 +92,16 @@ public function evaluate(array $functions, array $values)
8392

8493
case self::METHOD_CALL:
8594
$obj = $this->nodes['node']->evaluate($functions, $values);
95+
8696
if (null === $obj && $this->nodes['attribute']->isNullSafe) {
97+
$this->isShortCircuited = true;
98+
99+
return null;
100+
}
101+
if (null === $obj && $this->isShortCircuited()) {
87102
return null;
88103
}
104+
89105
if (!\is_object($obj)) {
90106
throw new \RuntimeException(sprintf('Unable to call method "%s" of non-object "%s".', $this->nodes['attribute']->dump(), $this->nodes['node']->dump()));
91107
}
@@ -97,6 +113,11 @@ public function evaluate(array $functions, array $values)
97113

98114
case self::ARRAY_CALL:
99115
$array = $this->nodes['node']->evaluate($functions, $values);
116+
117+
if (null === $array && $this->isShortCircuited()) {
118+
return null;
119+
}
120+
100121
if (!\is_array($array) && !$array instanceof \ArrayAccess) {
101122
throw new \RuntimeException(sprintf('Unable to get an item of non-array "%s".', $this->nodes['node']->dump()));
102123
}
@@ -105,6 +126,13 @@ public function evaluate(array $functions, array $values)
105126
}
106127
}
107128

129+
private function isShortCircuited(): bool
130+
{
131+
return $this->isShortCircuited
132+
|| ($this->nodes['node'] instanceof self && $this->nodes['node']->isShortCircuited())
133+
;
134+
}
135+
108136
public function toArray()
109137
{
110138
switch ($this->attributes['type']) {

Tests/ExpressionLanguageTest.php

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -249,13 +249,13 @@ public function testNullSafeEvaluate($expression, $foo)
249249
/**
250250
* @dataProvider provideNullSafe
251251
*/
252-
public function testNullsafeCompile($expression, $foo)
252+
public function testNullSafeCompile($expression, $foo)
253253
{
254254
$expressionLanguage = new ExpressionLanguage();
255255
$this->assertNull(eval(sprintf('return %s;', $expressionLanguage->compile($expression, ['foo' => 'foo']))));
256256
}
257257

258-
public function provideNullsafe()
258+
public function provideNullSafe()
259259
{
260260
$foo = new class() extends \stdClass {
261261
public function bar()
@@ -272,6 +272,47 @@ public function bar()
272272
yield ['foo["bar"]?.baz()', ['bar' => null]];
273273
yield ['foo.bar()?.baz', $foo];
274274
yield ['foo.bar()?.baz()', $foo];
275+
276+
yield ['foo?.bar.baz', null];
277+
yield ['foo?.bar["baz"]', null];
278+
yield ['foo?.bar["baz"]["qux"]', null];
279+
yield ['foo?.bar["baz"]["qux"].quux', null];
280+
yield ['foo?.bar["baz"]["qux"].quux()', null];
281+
yield ['foo?.bar().baz', null];
282+
yield ['foo?.bar()["baz"]', null];
283+
yield ['foo?.bar()["baz"]["qux"]', null];
284+
yield ['foo?.bar()["baz"]["qux"].quux', null];
285+
yield ['foo?.bar()["baz"]["qux"].quux()', null];
286+
}
287+
288+
/**
289+
* @dataProvider provideInvalidNullSafe
290+
*/
291+
public function testNullSafeEvaluateFails($expression, $foo, $message)
292+
{
293+
$expressionLanguage = new ExpressionLanguage();
294+
295+
$this->expectException(\RuntimeException::class);
296+
$this->expectExceptionMessage($message);
297+
$expressionLanguage->evaluate($expression, ['foo' => $foo]);
298+
}
299+
300+
/**
301+
* @dataProvider provideInvalidNullSafe
302+
*/
303+
public function testNullSafeCompileFails($expression, $foo)
304+
{
305+
$expressionLanguage = new ExpressionLanguage();
306+
307+
$this->expectWarning();
308+
eval(sprintf('return %s;', $expressionLanguage->compile($expression, ['foo' => 'foo'])));
309+
}
310+
311+
public function provideInvalidNullSafe()
312+
{
313+
yield ['foo?.bar.baz', (object) ['bar' => null], 'Unable to get property "baz" of non-object "foo.bar".'];
314+
yield ['foo?.bar["baz"]', (object) ['bar' => null], 'Unable to get an item of non-array "foo.bar".'];
315+
yield ['foo?.bar["baz"].qux.quux', (object) ['bar' => ['baz' => null]], 'Unable to get property "qux" of non-object "foo.bar["baz"]".'];
275316
}
276317

277318
/**

0 commit comments

Comments
 (0)