mirror of
https://github.com/twigphp/Twig.git
synced 2026-08-30 03:57:21 +00:00
Merge branch '3.x' into 4.x
* 3.x: Tweak null-safe operator implementation Add null-safe operator Update u.rst to clarify truncate method's third argument behavior
This commit is contained in:
@@ -23,9 +23,13 @@ $output = fopen(dirname(__DIR__).'/doc/operators_precedence.rst', 'w');
|
||||
$twig = new Environment(new ArrayLoader([]));
|
||||
$descriptionLength = 11;
|
||||
$expressionParsers = [];
|
||||
$seen = new SplObjectStorage();
|
||||
foreach ($twig->getExpressionParsers() as $expressionParser) {
|
||||
$expressionParsers[] = $expressionParser;
|
||||
$descriptionLength = max($descriptionLength, $expressionParser instanceof ExpressionParserDescriptionInterface ? strlen($expressionParser->getDescription()) : '');
|
||||
if (!$seen->offsetExists($expressionParser)) {
|
||||
$expressionParsers[] = $expressionParser;
|
||||
$seen->offsetSet($expressionParser, true);
|
||||
$descriptionLength = max($descriptionLength, $expressionParser instanceof ExpressionParserDescriptionInterface ? strlen($expressionParser->getDescription()) : '');
|
||||
}
|
||||
}
|
||||
|
||||
fwrite($output, "\n+------------+------------------+---------+---------------+".str_repeat('-', $descriptionLength + 2)."+\n");
|
||||
@@ -46,9 +50,13 @@ foreach ($expressionParsers as $expressionParser) {
|
||||
if ($previousPrecedence !== $precedence) {
|
||||
$previous = null;
|
||||
}
|
||||
$operatorName = '``'.$expressionParser->getName().'``';
|
||||
if ($expressionParser->getAliases()) {
|
||||
$operatorName .= ', ``'.implode('``, ``', $expressionParser->getAliases()).'``';
|
||||
}
|
||||
fwrite($output, rtrim(sprintf("\n| %-10s | %-16s | %-7s | %-13s | %-{$descriptionLength}s |\n",
|
||||
(!$previous || $previousPrecedence !== $precedence ? $precedence : '').($expressionParser->getPrecedenceChange() ? ' => '.$expressionParser->getPrecedenceChange()->getNewPrecedence() : ''),
|
||||
'``'.$expressionParser->getName().'``',
|
||||
$operatorName,
|
||||
!$previous || ExpressionParserType::getType($previous) !== ExpressionParserType::getType($expressionParser) ? ExpressionParserType::getType($expressionParser)->value : '',
|
||||
!$previous || $previousAssociativity !== $associativity ? $associativity : '',
|
||||
$expressionParser instanceof ExpressionParserDescriptionInterface ? $expressionParser->getDescription() : '',
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@ Truncating a string:
|
||||
{{ 'Lorem ipsum'|u.truncate(8, '...') }}
|
||||
Lorem...
|
||||
|
||||
The ``truncate`` method also accepts a third argument to preserve whole words:
|
||||
By default, ``truncate`` cuts text at the exact length. Pass ``false`` as the third argument to preserve whole words:
|
||||
|
||||
.. code-block:: twig
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
+------------+------------------+---------+---------------+-------------------------------------------------------------------+
|
||||
| | ``(`` | infix | Left | Twig function call |
|
||||
+------------+------------------+---------+---------------+-------------------------------------------------------------------+
|
||||
| | ``.`` | | | Get an attribute on a variable |
|
||||
| | ``.``, ``?.`` | | | Get an attribute on a variable |
|
||||
+------------+------------------+---------+---------------+-------------------------------------------------------------------+
|
||||
| | ``[`` | | | Array access |
|
||||
+------------+------------------+---------+---------------+-------------------------------------------------------------------+
|
||||
@@ -86,9 +86,7 @@
|
||||
+------------+------------------+---------+---------------+-------------------------------------------------------------------+
|
||||
| 10 | ``or`` | infix | Left | |
|
||||
+------------+------------------+---------+---------------+-------------------------------------------------------------------+
|
||||
| 5 | ``?:`` | infix | Right | Elvis operator (a ?: b) |
|
||||
+------------+------------------+---------+---------------+-------------------------------------------------------------------+
|
||||
| | ``?:`` | | | Elvis operator (a ?: b) |
|
||||
| 5 | ``?:``, ``? :`` | infix | Right | Elvis operator (a ?: b) |
|
||||
+------------+------------------+---------+---------------+-------------------------------------------------------------------+
|
||||
| | ``??`` | | | Null coalescing operator (a ?? b) |
|
||||
+------------+------------------+---------+---------------+-------------------------------------------------------------------+
|
||||
|
||||
+26
-3
@@ -851,7 +851,7 @@ The following operators don't fit into any of the other categories:
|
||||
|
||||
.. _dot_operator:
|
||||
|
||||
* ``.``, ``[]``: Gets an attribute of a variable.
|
||||
* ``.``, ``?.``, ``[]``: Gets an attribute of a variable.
|
||||
|
||||
The (``.``) operator abstracts getting an attribute of a variable (methods,
|
||||
properties or constants of a PHP object, or items of a PHP array):
|
||||
@@ -860,8 +860,23 @@ The following operators don't fit into any of the other categories:
|
||||
|
||||
{{ user.name }}
|
||||
|
||||
After the ``.``, you can use any expression by wrapping it with parenthesis
|
||||
``()``.
|
||||
The null-safe operator (``?.``) works like the dot operator but returns
|
||||
``null`` instead of throwing an exception when the left operand is ``null``:
|
||||
|
||||
.. code-block:: twig
|
||||
|
||||
{{ user?.name }}
|
||||
{# returns null if user is null, otherwise returns user.name #}
|
||||
|
||||
{{ user?.address?.city }}
|
||||
{# can be chained for safe navigation through potentially null values #}
|
||||
|
||||
.. versionadded:: 3.23
|
||||
|
||||
The null-safe operator was added in Twig 3.23.
|
||||
|
||||
After the ``.`` or ``?.``, you can use any expression by wrapping it with
|
||||
parenthesis ``()``.
|
||||
|
||||
One use case is when the attribute contains special characters (like ``-``
|
||||
that would be interpreted as the minus operator):
|
||||
@@ -870,6 +885,7 @@ The following operators don't fit into any of the other categories:
|
||||
|
||||
{# equivalent to the non-working user.first-name #}
|
||||
{{ user.('first-name') }}
|
||||
{{ user?.('first-name') }}
|
||||
|
||||
Another use case is when the attribute is "dynamic" (defined via a variable):
|
||||
|
||||
@@ -877,6 +893,7 @@ The following operators don't fit into any of the other categories:
|
||||
|
||||
{{ user.(name) }}
|
||||
{{ user.('get' ~ name) }}
|
||||
{{ user?.(name) }}
|
||||
|
||||
Before Twig 3.15, use the :doc:`attribute <functions/attribute>` function
|
||||
instead for the two previous use cases.
|
||||
@@ -912,6 +929,12 @@ The following operators don't fit into any of the other categories:
|
||||
* if not, and if ``strict_variables`` is ``false``, return ``null``;
|
||||
* if not, throw an exception.
|
||||
|
||||
To resolve ``user?.name`` to a PHP call, Twig checks if ``user`` is
|
||||
``null`` first:
|
||||
|
||||
* if ``user`` is ``null``, return ``null``;
|
||||
* otherwise, use the same algorithm as for ``user.name``.
|
||||
|
||||
To resolve ``user['name']`` to a PHP call, Twig uses the following algorithm
|
||||
at runtime:
|
||||
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
"script_names" function
|
||||
--TEMPLATE--
|
||||
{{ script_names('UNKNOWN')|length }}
|
||||
{{ script_names() is iterable }}
|
||||
{{ script_names('fr') is iterable }}
|
||||
{{ script_names()|length > 200 ? 'more than 200' : 'less than 200' }}
|
||||
{{ script_names('fr')|length > 200 ? 'more than 200' : 'less than 200' }}
|
||||
{{ script_names()['Marc'] }}
|
||||
{{ script_names('fr')['Marc'] }}
|
||||
--DATA--
|
||||
return [];
|
||||
--EXPECT--
|
||||
0
|
||||
1
|
||||
1
|
||||
more than 200
|
||||
more than 200
|
||||
Marchen
|
||||
Marchen
|
||||
|
||||
@@ -37,6 +37,7 @@ final class DotExpressionParser extends AbstractExpressionParser implements Infi
|
||||
|
||||
public function parse(Parser $parser, AbstractExpression $expr, Token $token): AbstractExpression
|
||||
{
|
||||
$nullSafe = '?.' === $token->getValue();
|
||||
$stream = $parser->getStream();
|
||||
$token = $stream->getCurrent();
|
||||
$lineno = $token->getLine();
|
||||
@@ -55,7 +56,7 @@ final class DotExpressionParser extends AbstractExpressionParser implements Infi
|
||||
) {
|
||||
$attribute = new ConstantExpression($token->getValue(), $token->getLine());
|
||||
} 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 GetAttrExpression($expr, $attribute, $arguments, $type, $lineno);
|
||||
return new GetAttrExpression($expr, $attribute, $arguments, $type, $lineno, $nullSafe);
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
@@ -82,6 +83,11 @@ final class DotExpressionParser extends AbstractExpressionParser implements Infi
|
||||
return '.';
|
||||
}
|
||||
|
||||
public function getAliases(): array
|
||||
{
|
||||
return ['?.'];
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Get an attribute on a variable';
|
||||
|
||||
@@ -21,14 +21,14 @@ class GetAttrExpression extends AbstractExpression implements SupportDefinedTest
|
||||
{
|
||||
use SupportDefinedTestTrait;
|
||||
|
||||
public function __construct(AbstractExpression $node, AbstractExpression $attribute, ArrayExpression|ContextVariable|null $arguments, string $type, int $lineno)
|
||||
public function __construct(AbstractExpression $node, AbstractExpression $attribute, ArrayExpression|ContextVariable|null $arguments, string $type, int $lineno, bool $nullSafe = false)
|
||||
{
|
||||
$nodes = ['node' => $node, 'attribute' => $attribute];
|
||||
if (null !== $arguments) {
|
||||
$nodes['arguments'] = $arguments;
|
||||
}
|
||||
|
||||
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
|
||||
@@ -41,6 +41,8 @@ class GetAttrExpression extends AbstractExpression implements SupportDefinedTest
|
||||
{
|
||||
$env = $compiler->getEnvironment();
|
||||
$arrayAccessSandbox = false;
|
||||
$nullSafe = $this->getAttribute('null_safe');
|
||||
$objectVar = null;
|
||||
|
||||
// optimize array calls
|
||||
if (
|
||||
@@ -85,14 +87,27 @@ class GetAttrExpression extends AbstractExpression implements SupportDefinedTest
|
||||
;
|
||||
}
|
||||
|
||||
$compiler->raw('CoreExtension::getAttribute($this->env, $this->source, ');
|
||||
|
||||
if ($this->getAttribute('ignore_strict_check')) {
|
||||
$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
|
||||
->subcompile($this->getNode('node'))
|
||||
->raw(', ')
|
||||
->subcompile($this->getNode('attribute'))
|
||||
;
|
||||
@@ -125,6 +140,10 @@ class GetAttrExpression extends AbstractExpression implements SupportDefinedTest
|
||||
if ($arrayAccessSandbox) {
|
||||
$compiler->raw(')');
|
||||
}
|
||||
|
||||
if ($nullSafe) {
|
||||
$compiler->raw(')');
|
||||
}
|
||||
}
|
||||
|
||||
private function changeIgnoreStrictCheck(self $node): void
|
||||
|
||||
@@ -273,6 +273,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()
|
||||
{
|
||||
$env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]);
|
||||
|
||||
@@ -5,4 +5,4 @@ Twig does not support using . for concatenation
|
||||
--DATA--
|
||||
return []
|
||||
--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($args, $node->getNode('arguments'));
|
||||
$this->assertEquals(Template::ARRAY_CALL, $node->getAttribute('type'));
|
||||
$this->assertFalse($node->getAttribute('null_safe'));
|
||||
}
|
||||
|
||||
public static function provideTests(): iterable
|
||||
@@ -51,9 +52,13 @@ class GetAttrTest extends NodeTestCase
|
||||
$expr = new ContextVariable('foo', 1);
|
||||
$attr = new ConstantExpression('bar', 1);
|
||||
$args = new ArrayExpression([], 1);
|
||||
|
||||
$node = new GetAttrExpression($expr, $attr, $args, Template::ANY_CALL, 1);
|
||||
$tests[] = [$node, \sprintf('%s%s, "bar", arguments: [], lineno: 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);
|
||||
$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