Tweak null-safe operator implementation

This commit is contained in:
Fabien Potencier
2026-01-17 15:08:58 +01:00
parent b1b2a0923f
commit 6904954165
5 changed files with 48 additions and 15 deletions
+1
View File
@@ -1,5 +1,6 @@
# 3.23.0 (2026-XX-XX)
* Add `?.` null-safe operator
* Add `===` and `!==` operators (equivalent to the `same as` and `not same as` tests)
# 3.22.2 (2025-12-14)
+16 -4
View File
@@ -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() : '',
@@ -83,9 +91,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->getName().'``',
$operatorName,
!$previous || ExpressionParserType::getType($previous) !== ExpressionParserType::getType($expressionParser) ? ExpressionParserType::getType($expressionParser)->value : '',
!$previous || $previousAssociativity !== $associativity ? $associativity : '',
$expressionParser instanceof ExpressionParserDescriptionInterface ? $expressionParser->getDescription() : '',
+4 -8
View File
@@ -8,7 +8,7 @@
+------------+------------------+---------+---------------+-------------------------------------------------------------------+
| | ``(`` | | | Twig function call |
+------------+------------------+---------+---------------+-------------------------------------------------------------------+
| | ``.`` | | | Get an attribute on a variable |
| | ``.``, ``?.`` | | | Get an attribute on a variable |
+------------+------------------+---------+---------------+-------------------------------------------------------------------+
| | ``[`` | | | Array access |
+------------+------------------+---------+---------------+-------------------------------------------------------------------+
@@ -88,9 +88,7 @@
+------------+------------------+---------+---------------+-------------------------------------------------------------------+
| 10 | ``or`` | infix | Left | |
+------------+------------------+---------+---------------+-------------------------------------------------------------------+
| 5 | ``?:`` | infix | Right | Elvis operator (a ?: b) |
+------------+------------------+---------+---------------+-------------------------------------------------------------------+
| | ``?:`` | | | Elvis operator (a ?: b) |
| 5 | ``?:``, ``? :`` | infix | Right | Elvis operator (a ?: b) |
+------------+------------------+---------+---------------+-------------------------------------------------------------------+
| 0 | ``(`` | prefix | n/a | Explicit group expression (a) |
+------------+------------------+---------+---------------+-------------------------------------------------------------------+
@@ -110,7 +108,7 @@ Here is the same table for Twig 4.0 with adjusted precedences:
+------------+------------------+---------+---------------+-------------------------------------------------------------------+
| | ``(`` | infix | Left | Twig function call |
+------------+------------------+---------+---------------+-------------------------------------------------------------------+
| | ``.`` | | | Get an attribute on a variable |
| | ``.``, ``?.`` | | | Get an attribute on a variable |
+------------+------------------+---------+---------------+-------------------------------------------------------------------+
| | ``[`` | | | Array access |
+------------+------------------+---------+---------------+-------------------------------------------------------------------+
@@ -192,9 +190,7 @@ Here is the same table for Twig 4.0 with adjusted precedences:
+------------+------------------+---------+---------------+-------------------------------------------------------------------+
| 5 | ``??`` | infix | Right | Null coalescing operator (a ?? b) |
+------------+------------------+---------+---------------+-------------------------------------------------------------------+
| | ``?:`` | | | Elvis operator (a ?: b) |
+------------+------------------+---------+---------------+-------------------------------------------------------------------+
| | ``?:`` | | | Elvis operator (a ?: b) |
| | ``?:``, ``? :`` | | | Elvis operator (a ?: b) |
+------------+------------------+---------+---------------+-------------------------------------------------------------------+
| 0 | ``(`` | prefix | n/a | Explicit group expression (a) |
+------------+------------------+---------+---------------+-------------------------------------------------------------------+
+26 -3
View File
@@ -865,7 +865,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):
@@ -874,8 +874,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):
@@ -884,6 +899,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):
@@ -891,6 +907,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.
@@ -926,6 +943,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:
@@ -4,6 +4,7 @@
* This file is part of Twig.
*
* (c) Fabien Potencier
* (c) Armin Ronacher
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.