mirror of
https://github.com/twigphp/Twig.git
synced 2026-09-19 05:46:41 +00:00
Add null-safe operator
This commit is contained in:
committed by
Fabien Potencier
parent
2bddfddb7d
commit
263c04fd1f
@@ -2,15 +2,15 @@
|
|||||||
"script_names" function
|
"script_names" function
|
||||||
--TEMPLATE--
|
--TEMPLATE--
|
||||||
{{ script_names('UNKNOWN')|length }}
|
{{ script_names('UNKNOWN')|length }}
|
||||||
{{ script_names() is iterable }}
|
{{ script_names()|length > 200 ? 'more than 200' : 'less than 200' }}
|
||||||
{{ script_names('fr') is iterable }}
|
{{ script_names('fr')|length > 200 ? 'more than 200' : 'less than 200' }}
|
||||||
{{ script_names()['Marc'] }}
|
{{ script_names()['Marc'] }}
|
||||||
{{ script_names('fr')['Marc'] }}
|
{{ script_names('fr')['Marc'] }}
|
||||||
--DATA--
|
--DATA--
|
||||||
return [];
|
return [];
|
||||||
--EXPECT--
|
--EXPECT--
|
||||||
0
|
0
|
||||||
1
|
more than 200
|
||||||
1
|
more than 200
|
||||||
Marchen
|
Marchen
|
||||||
Marchen
|
Marchen
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ final class DotExpressionParser extends AbstractExpressionParser implements Infi
|
|||||||
|
|
||||||
public function parse(Parser $parser, AbstractExpression $expr, Token $token): AbstractExpression
|
public function parse(Parser $parser, AbstractExpression $expr, Token $token): AbstractExpression
|
||||||
{
|
{
|
||||||
|
$nullSafe = '?.' === $token->getValue();
|
||||||
$stream = $parser->getStream();
|
$stream = $parser->getStream();
|
||||||
$token = $stream->getCurrent();
|
$token = $stream->getCurrent();
|
||||||
$lineno = $token->getLine();
|
$lineno = $token->getLine();
|
||||||
@@ -55,7 +56,7 @@ final class DotExpressionParser extends AbstractExpressionParser implements Infi
|
|||||||
) {
|
) {
|
||||||
$attribute = new ConstantExpression($token->getValue(), $token->getLine());
|
$attribute = new ConstantExpression($token->getValue(), $token->getLine());
|
||||||
} else {
|
} else {
|
||||||
throw new SyntaxError(\sprintf('Expected name or number, got value "%s" of type %s.', $token->getValue(), $token->toEnglish()), $token->getLine(), $stream->getSourceContext());
|
throw new SyntaxError(\sprintf('Expected name or number, got value "%s" of type "%s".', $token->getValue(), $token->toEnglish()), $token->getLine(), $stream->getSourceContext());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +75,7 @@ final class DotExpressionParser extends AbstractExpressionParser implements Infi
|
|||||||
return new MacroReferenceExpression(new TemplateVariable($expr->getAttribute('name'), $expr->getTemplateLine()), 'macro_'.$attribute->getAttribute('value'), $arguments, $expr->getTemplateLine());
|
return new MacroReferenceExpression(new TemplateVariable($expr->getAttribute('name'), $expr->getTemplateLine()), 'macro_'.$attribute->getAttribute('value'), $arguments, $expr->getTemplateLine());
|
||||||
}
|
}
|
||||||
|
|
||||||
return new GetAttrExpression($expr, $attribute, $arguments, $type, $lineno);
|
return new GetAttrExpression($expr, $attribute, $arguments, $type, $lineno, $nullSafe);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getName(): string
|
public function getName(): string
|
||||||
@@ -82,6 +83,11 @@ final class DotExpressionParser extends AbstractExpressionParser implements Infi
|
|||||||
return '.';
|
return '.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getAliases(): array
|
||||||
|
{
|
||||||
|
return ['?.'];
|
||||||
|
}
|
||||||
|
|
||||||
public function getDescription(): string
|
public function getDescription(): string
|
||||||
{
|
{
|
||||||
return 'Get an attribute on a variable';
|
return 'Get an attribute on a variable';
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
* This file is part of Twig.
|
* This file is part of Twig.
|
||||||
*
|
*
|
||||||
* (c) Fabien Potencier
|
* (c) Fabien Potencier
|
||||||
* (c) Armin Ronacher
|
|
||||||
*
|
*
|
||||||
* For the full copyright and license information, please view the LICENSE
|
* For the full copyright and license information, please view the LICENSE
|
||||||
* file that was distributed with this source code.
|
* file that was distributed with this source code.
|
||||||
@@ -25,7 +24,7 @@ class GetAttrExpression extends AbstractExpression implements SupportDefinedTest
|
|||||||
/**
|
/**
|
||||||
* @param ArrayExpression|NameExpression|null $arguments
|
* @param ArrayExpression|NameExpression|null $arguments
|
||||||
*/
|
*/
|
||||||
public function __construct(AbstractExpression $node, AbstractExpression $attribute, ?AbstractExpression $arguments, string $type, int $lineno)
|
public function __construct(AbstractExpression $node, AbstractExpression $attribute, ?AbstractExpression $arguments, string $type, int $lineno, bool $nullSafe = false)
|
||||||
{
|
{
|
||||||
$nodes = ['node' => $node, 'attribute' => $attribute];
|
$nodes = ['node' => $node, 'attribute' => $attribute];
|
||||||
if (null !== $arguments) {
|
if (null !== $arguments) {
|
||||||
@@ -36,7 +35,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));
|
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' => true], $lineno);
|
parent::__construct($nodes, ['type' => $type, 'ignore_strict_check' => false, 'optimizable' => !$nullSafe, 'null_safe' => $nullSafe], $lineno);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function enableDefinedTest(): void
|
public function enableDefinedTest(): void
|
||||||
@@ -49,6 +48,8 @@ class GetAttrExpression extends AbstractExpression implements SupportDefinedTest
|
|||||||
{
|
{
|
||||||
$env = $compiler->getEnvironment();
|
$env = $compiler->getEnvironment();
|
||||||
$arrayAccessSandbox = false;
|
$arrayAccessSandbox = false;
|
||||||
|
$nullSafe = $this->getAttribute('null_safe');
|
||||||
|
$objectVar = null;
|
||||||
|
|
||||||
// optimize array calls
|
// optimize array calls
|
||||||
if (
|
if (
|
||||||
@@ -93,14 +94,27 @@ class GetAttrExpression extends AbstractExpression implements SupportDefinedTest
|
|||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
$compiler->raw('CoreExtension::getAttribute($this->env, $this->source, ');
|
|
||||||
|
|
||||||
if ($this->getAttribute('ignore_strict_check')) {
|
if ($this->getAttribute('ignore_strict_check')) {
|
||||||
$this->getNode('node')->setAttribute('ignore_strict_check', true);
|
$this->getNode('node')->setAttribute('ignore_strict_check', true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($nullSafe) {
|
||||||
|
$objectVar = '$'.$compiler->getVarName();
|
||||||
|
$compiler
|
||||||
|
->raw('((null === ('.$objectVar.' = ')
|
||||||
|
->subcompile($this->getNode('node'))
|
||||||
|
->raw(')) ? null : ');
|
||||||
|
}
|
||||||
|
|
||||||
|
$compiler->raw('CoreExtension::getAttribute($this->env, $this->source, ');
|
||||||
|
|
||||||
|
if ($nullSafe) {
|
||||||
|
$compiler->raw($objectVar);
|
||||||
|
} else {
|
||||||
|
$compiler->subcompile($this->getNode('node'));
|
||||||
|
}
|
||||||
|
|
||||||
$compiler
|
$compiler
|
||||||
->subcompile($this->getNode('node'))
|
|
||||||
->raw(', ')
|
->raw(', ')
|
||||||
->subcompile($this->getNode('attribute'))
|
->subcompile($this->getNode('attribute'))
|
||||||
;
|
;
|
||||||
@@ -123,6 +137,10 @@ class GetAttrExpression extends AbstractExpression implements SupportDefinedTest
|
|||||||
if ($arrayAccessSandbox) {
|
if ($arrayAccessSandbox) {
|
||||||
$compiler->raw(')');
|
$compiler->raw(')');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($nullSafe) {
|
||||||
|
$compiler->raw(')');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function changeIgnoreStrictCheck(self $node): void
|
private function changeIgnoreStrictCheck(self $node): void
|
||||||
|
|||||||
@@ -288,6 +288,62 @@ class ExpressionParserTest extends TestCase
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider getTestsForNullSafeOperator
|
||||||
|
*/
|
||||||
|
public function testNullSafeOperator($template, $data, $expected)
|
||||||
|
{
|
||||||
|
$env = new Environment(new ArrayLoader(['template' => $template]));
|
||||||
|
|
||||||
|
$this->assertSame($expected, $env->render('template', $data));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getTestsForNullSafeOperator()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
'{{ foo?.bar }}',
|
||||||
|
['foo' => (object) ['bar' => 'baz']],
|
||||||
|
'baz',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'{{ foo?.bar }}',
|
||||||
|
['foo' => null],
|
||||||
|
'',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'{{ foo?.bar?.baz }}',
|
||||||
|
['foo' => (object) ['bar' => (object) ['baz' => 'qux']]],
|
||||||
|
'qux',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'{{ foo?.bar?.baz }}',
|
||||||
|
['foo' => (object) ['bar' => null]],
|
||||||
|
'',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'{{ foo?.bar?.baz }}',
|
||||||
|
['foo' => null],
|
||||||
|
'',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'{{ foo?.bar?.baz ?? "qux" }}',
|
||||||
|
['foo' => null],
|
||||||
|
'qux',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'{{ foo?.bar ?? "qux" }}',
|
||||||
|
['foo' => (object) ['bar' => 0]],
|
||||||
|
'0',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'{{ foo?.bar ?? "qux" }}',
|
||||||
|
['foo' => (object) ['bar' => false]],
|
||||||
|
'',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
public function testMacroDefinitionDoesNotSupportNonNameVariableName()
|
public function testMacroDefinitionDoesNotSupportNonNameVariableName()
|
||||||
{
|
{
|
||||||
$env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]);
|
$env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]);
|
||||||
|
|||||||
@@ -5,4 +5,4 @@ Twig does not support using . for concatenation
|
|||||||
--DATA--
|
--DATA--
|
||||||
return []
|
return []
|
||||||
--EXCEPTION--
|
--EXCEPTION--
|
||||||
Twig\Error\SyntaxError: Expected name or number, got value "b" of type string in "index.twig" at line 2.
|
Twig\Error\SyntaxError: Expected name or number, got value "b" of type "string" in "index.twig" at line 2.
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ class GetAttrTest extends NodeTestCase
|
|||||||
$this->assertEquals($attr, $node->getNode('attribute'));
|
$this->assertEquals($attr, $node->getNode('attribute'));
|
||||||
$this->assertEquals($args, $node->getNode('arguments'));
|
$this->assertEquals($args, $node->getNode('arguments'));
|
||||||
$this->assertEquals(Template::ARRAY_CALL, $node->getAttribute('type'));
|
$this->assertEquals(Template::ARRAY_CALL, $node->getAttribute('type'));
|
||||||
|
$this->assertFalse($node->getAttribute('null_safe'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function provideTests(): iterable
|
public static function provideTests(): iterable
|
||||||
@@ -51,9 +52,13 @@ class GetAttrTest extends NodeTestCase
|
|||||||
$expr = new ContextVariable('foo', 1);
|
$expr = new ContextVariable('foo', 1);
|
||||||
$attr = new ConstantExpression('bar', 1);
|
$attr = new ConstantExpression('bar', 1);
|
||||||
$args = new ArrayExpression([], 1);
|
$args = new ArrayExpression([], 1);
|
||||||
|
|
||||||
$node = new GetAttrExpression($expr, $attr, $args, Template::ANY_CALL, 1);
|
$node = new GetAttrExpression($expr, $attr, $args, Template::ANY_CALL, 1);
|
||||||
$tests[] = [$node, \sprintf('%s%s, "bar", [], "any", false, false, false, 1)', self::createAttributeGetter(), self::createVariableGetter('foo', 1))];
|
$tests[] = [$node, \sprintf('%s%s, "bar", [], "any", false, false, false, 1)', self::createAttributeGetter(), self::createVariableGetter('foo', 1))];
|
||||||
|
|
||||||
|
$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::ARRAY_CALL, 1);
|
$node = new GetAttrExpression($expr, $attr, $args, Template::ARRAY_CALL, 1);
|
||||||
$tests[] = [$node, '(($_v%s = // line 1'."\n".
|
$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, ];
|
'($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