Skip to content

Commit b958c52

Browse files
committed
[ExpressionLanguage] Fix null-safe chaining
1 parent 90164c1 commit b958c52

File tree

2 files changed

+64
-7
lines changed

2 files changed

+64
-7
lines changed

src/Symfony/Component/ExpressionLanguage/Node/GetAttrNode.php

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,13 @@ public function compile(Compiler $compiler)
6767

6868
public function evaluate(array $functions, array $values)
6969
{
70+
if ($this->isEvaluationShortCircuited($functions, $values)) {
71+
return null;
72+
}
73+
7074
switch ($this->attributes['type']) {
7175
case self::PROPERTY_CALL:
7276
$obj = $this->nodes['node']->evaluate($functions, $values);
73-
if (null === $obj && $this->nodes['attribute']->isNullSafe) {
74-
return null;
75-
}
7677
if (!\is_object($obj)) {
7778
throw new \RuntimeException(sprintf('Unable to get property "%s" of non-object "%s".', $this->nodes['attribute']->dump(), $this->nodes['node']->dump()));
7879
}
@@ -83,9 +84,6 @@ public function evaluate(array $functions, array $values)
8384

8485
case self::METHOD_CALL:
8586
$obj = $this->nodes['node']->evaluate($functions, $values);
86-
if (null === $obj && $this->nodes['attribute']->isNullSafe) {
87-
return null;
88-
}
8987
if (!\is_object($obj)) {
9088
throw new \RuntimeException(sprintf('Unable to call method "%s" of non-object "%s".', $this->nodes['attribute']->dump(), $this->nodes['node']->dump()));
9189
}
@@ -105,6 +103,24 @@ public function evaluate(array $functions, array $values)
105103
}
106104
}
107105

106+
private function isEvaluationShortCircuited(array $functions, array $values): bool
107+
{
108+
$node = $this;
109+
110+
do {
111+
if (
112+
$node->nodes['attribute'] instanceof ConstantNode
113+
&& $node->nodes['attribute']->isNullSafe
114+
&& null === $node->nodes['node']->evaluate($functions, $values)
115+
) {
116+
return true;
117+
}
118+
$node = $node->nodes['node'];
119+
} while ($node instanceof self);
120+
121+
return false;
122+
}
123+
108124
public function toArray()
109125
{
110126
switch ($this->attributes['type']) {

src/Symfony/Component/ExpressionLanguage/Tests/ExpressionLanguageTest.php

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ public function testNullsafeCompile($expression, $foo)
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)