mirror of
https://github.com/twigphp/Twig.git
synced 2026-08-17 12:11:33 +00:00
Support short-circuiting in null-safe operator chains
This commit is contained in:
+5
-1
@@ -894,7 +894,8 @@ The following operators don't fit into any of the other categories:
|
||||
{{ user.name }}
|
||||
|
||||
The null-safe operator (``?.``) works like the dot operator but returns
|
||||
``null`` instead of throwing an exception when the left operand is ``null``:
|
||||
``null`` instead of throwing an exception when the left operand is ``null``.
|
||||
If the operand is part of a chain, the rest of the chain is skipped:
|
||||
|
||||
.. code-block:: twig
|
||||
|
||||
@@ -904,6 +905,9 @@ The following operators don't fit into any of the other categories:
|
||||
{{ user?.address?.city }}
|
||||
{# can be chained for safe navigation through potentially null values #}
|
||||
|
||||
{{ user?.address.city }}
|
||||
{# returns null if user is null, the rest of the chain is skipped (address.city is not evaluated) #}
|
||||
|
||||
.. versionadded:: 3.23
|
||||
|
||||
The null-safe operator was added in Twig 3.23.
|
||||
|
||||
@@ -36,7 +36,7 @@ class GetAttrExpression extends AbstractExpression implements SupportDefinedTest
|
||||
trigger_deprecation('twig/twig', '3.15', \sprintf('Not passing a "%s" instance as the "arguments" argument of the "%s" constructor is deprecated ("%s" given).', ArrayExpression::class, static::class, $arguments::class));
|
||||
}
|
||||
|
||||
parent::__construct($nodes, ['type' => $type, 'ignore_strict_check' => false, 'optimizable' => !$nullSafe, 'null_safe' => $nullSafe], $lineno);
|
||||
parent::__construct($nodes, ['type' => $type, 'ignore_strict_check' => false, 'optimizable' => !$nullSafe, 'null_safe' => $nullSafe, 'is_short_circuited' => false, 'var_name' => null], $lineno);
|
||||
}
|
||||
|
||||
public function enableDefinedTest(): void
|
||||
@@ -50,7 +50,6 @@ class GetAttrExpression extends AbstractExpression implements SupportDefinedTest
|
||||
$env = $compiler->getEnvironment();
|
||||
$arrayAccessSandbox = false;
|
||||
$nullSafe = $this->getAttribute('null_safe');
|
||||
$objectVar = null;
|
||||
|
||||
// optimize array calls
|
||||
if (
|
||||
@@ -99,18 +98,32 @@ class GetAttrExpression extends AbstractExpression implements SupportDefinedTest
|
||||
$this->getNode('node')->setAttribute('ignore_strict_check', true);
|
||||
}
|
||||
|
||||
if ($nullSafe) {
|
||||
$objectVar = '$'.$compiler->getVarName();
|
||||
if (null === $nullSafeNode = $nullSafe ? $this : null) {
|
||||
$node = $this->getNode('node');
|
||||
while ($node instanceof self) {
|
||||
if ($node->getAttribute('null_safe')) {
|
||||
$nullSafeNode = $node;
|
||||
break;
|
||||
}
|
||||
$node = $node->getNode('node');
|
||||
}
|
||||
}
|
||||
|
||||
$isShortCircuited = false;
|
||||
if (null !== $nullSafeNode && !$nullSafeNode->isShortCircuited()) {
|
||||
$compiler
|
||||
->raw('((null === ('.$objectVar.' = ')
|
||||
->subcompile($this->getNode('node'))
|
||||
->raw('((null === ('.$nullSafeNode->getVarName($compiler).' = ')
|
||||
->subcompile($nullSafeNode->getNode('node'))
|
||||
->raw(')) ? null : ');
|
||||
|
||||
$nullSafeNode->markAsShortCircuited();
|
||||
$isShortCircuited = true;
|
||||
}
|
||||
|
||||
$compiler->raw('CoreExtension::getAttribute($this->env, $this->source, ');
|
||||
|
||||
if ($nullSafe) {
|
||||
$compiler->raw($objectVar);
|
||||
$compiler->raw($this->getVarName($compiler));
|
||||
} else {
|
||||
$compiler->subcompile($this->getNode('node'));
|
||||
}
|
||||
@@ -139,7 +152,7 @@ class GetAttrExpression extends AbstractExpression implements SupportDefinedTest
|
||||
$compiler->raw(')');
|
||||
}
|
||||
|
||||
if ($nullSafe) {
|
||||
if ($isShortCircuited) {
|
||||
$compiler->raw(')');
|
||||
}
|
||||
}
|
||||
@@ -153,4 +166,23 @@ class GetAttrExpression extends AbstractExpression implements SupportDefinedTest
|
||||
$this->changeIgnoreStrictCheck($node->getNode('node'));
|
||||
}
|
||||
}
|
||||
|
||||
private function markAsShortCircuited(): void
|
||||
{
|
||||
$this->setAttribute('is_short_circuited', true);
|
||||
}
|
||||
|
||||
private function isShortCircuited(): bool
|
||||
{
|
||||
return $this->getAttribute('is_short_circuited');
|
||||
}
|
||||
|
||||
private function getVarName(Compiler $compiler): string
|
||||
{
|
||||
if (null === $this->getAttribute('var_name')) {
|
||||
$this->setAttribute('var_name', $compiler->getVarName());
|
||||
}
|
||||
|
||||
return '$'.$this->getAttribute('var_name');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
|
||||
use Twig\Attribute\FirstClassTwigCallableReady;
|
||||
use Twig\Compiler;
|
||||
use Twig\Environment;
|
||||
use Twig\Error\RuntimeError;
|
||||
use Twig\Error\SyntaxError;
|
||||
use Twig\ExpressionParser\Prefix\UnaryOperatorExpressionParser;
|
||||
use Twig\Extension\AbstractExtension;
|
||||
@@ -350,6 +351,49 @@ class ExpressionParserTest extends TestCase
|
||||
['foo' => (object) ['bar' => false]],
|
||||
'',
|
||||
],
|
||||
// short-circuiting
|
||||
[
|
||||
'{{ foo?.bar.baz }}',
|
||||
['foo' => null],
|
||||
'',
|
||||
],
|
||||
[
|
||||
'{{ foo?.bar.baz?.qux.corge }}',
|
||||
['foo' => null],
|
||||
'',
|
||||
],
|
||||
[
|
||||
'{{ foo?.bar.baz?.qux.corge }}',
|
||||
['foo' => (object) ['bar' => (object) ['baz' => null]]],
|
||||
'',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getTestForInvalidNullSafeOperatorShortCircuiting
|
||||
*/
|
||||
public function testInvalidNullSafeOperatorShortCircuiting(string $template, array $data, string $expectedMessage)
|
||||
{
|
||||
$env = new Environment(new ArrayLoader(['template' => $template]), ['strict_variables' => true]);
|
||||
|
||||
$this->expectException(RuntimeError::class);
|
||||
$this->expectExceptionMessage($expectedMessage);
|
||||
|
||||
$env->render('template', $data);
|
||||
}
|
||||
|
||||
public static function getTestForInvalidNullSafeOperatorShortCircuiting()
|
||||
{
|
||||
yield [
|
||||
'{{ foo?.bar.baz }}',
|
||||
['foo' => (object) ['bar' => null]],
|
||||
'Impossible to access an attribute ("baz") on a null variable in "template" at line 1.',
|
||||
];
|
||||
yield [
|
||||
'{{ foo?.bar.baz?.qux.corge }}',
|
||||
['foo' => (object) ['bar' => (object) ['baz' => (object) ['qux' => null]]]],
|
||||
'Impossible to access an attribute ("corge") on a null variable in "template" at line 1.',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -51,6 +51,9 @@ class GetAttrTest extends NodeTestCase
|
||||
|
||||
$expr = new ContextVariable('foo', 1);
|
||||
$attr = new ConstantExpression('bar', 1);
|
||||
$attr2 = new ConstantExpression('baz', 1);
|
||||
$attr3 = new ConstantExpression('qux', 1);
|
||||
$attr4 = new ConstantExpression('corge', 1);
|
||||
$args = new ArrayExpression([], 1);
|
||||
|
||||
$node = new GetAttrExpression($expr, $attr, $args, Template::ANY_CALL, 1);
|
||||
@@ -59,6 +62,16 @@ class GetAttrTest extends NodeTestCase
|
||||
$node = new GetAttrExpression($expr, $attr, $args, Template::ANY_CALL, 1, true);
|
||||
$tests[] = [$node, '((null === ($_v%s = // line 1'."\n".'($context["foo"] ?? null))) ? null : '.self::createAttributeGetter().'$_v%s, "bar", [], "any", false, false, false, 1))', null, true];
|
||||
|
||||
$node = new GetAttrExpression($expr, $attr, $args, Template::ANY_CALL, 1, true);
|
||||
$node = new GetAttrExpression($node, $attr2, $args, Template::METHOD_CALL, 1);
|
||||
$tests[] = [$node, '((null === ($_v%s = // line 1'."\n".'($context["foo"] ?? null))) ? null : '.self::createAttributeGetter().self::createAttributeGetter().'$_v%s, "bar", [], "any", false, false, false, 1), "baz", [], "method", false, false, false, 1))', null, true];
|
||||
|
||||
$node = new GetAttrExpression($expr, $attr, $args, Template::ANY_CALL, 1, true);
|
||||
$node = new GetAttrExpression($node, $attr2, $args, Template::ANY_CALL, 1);
|
||||
$node = new GetAttrExpression($node, $attr3, $args, Template::METHOD_CALL, 1, true);
|
||||
$node = new GetAttrExpression($node, $attr4, $args, Template::ANY_CALL, 1);
|
||||
$tests[] = [$node, '((null === ($_v0 = ((null === ($_v1 = // line 1'."\n".'($context["foo"] ?? null))) ? null : '.self::createAttributeGetter().self::createAttributeGetter().'$_v1, "bar", [], "any", false, false, false, 1), "baz", [], "any", false, false, false, 1)))) ? null : '.self::createAttributeGetter().self::createAttributeGetter().'$_v0, "qux", [], "method", false, false, false, 1), "corge", [], "any", false, false, false, 1))', null];
|
||||
|
||||
$node = new GetAttrExpression($expr, $attr, $args, Template::ARRAY_CALL, 1);
|
||||
$tests[] = [$node, '(($_v%s = // line 1'."\n".
|
||||
'($context["foo"] ?? null)) && is_array($_v%s) || $_v%s instanceof ArrayAccess ? ($_v%s["bar"] ?? null) : null)', null, true, ];
|
||||
|
||||
Reference in New Issue
Block a user