Merge branch '3.x' into 4.x

* 3.x:
  Fix typo
  Tweak
  Rename array to sequence
  Rename hash to mapping
This commit is contained in:
Fabien Potencier
2024-06-22 19:42:47 +02:00
27 changed files with 146 additions and 115 deletions
+4 -4
View File
@@ -40,8 +40,8 @@ standards:
{{ foo ~ bar }}
{{ true ? true : false }}
* Put exactly one space after the ``:`` sign in hashes and ``,`` in
arrays and hashes:
* Put exactly one space after the ``:`` sign in mappings and ``,`` in sequences
and mappings:
.. code-block:: twig
@@ -80,8 +80,8 @@ standards:
{{ foo|default('foo') }}
{{ range(1..10) }}
* Do not put any spaces before and after the opening and the closing of arrays
and hashes:
* Do not put any spaces before and after the opening and the closing of
sequences and mappings:
.. code-block:: twig
+1 -1
View File
@@ -50,6 +50,6 @@ Arguments
---------
* ``mime``: The mime type
* ``parameters``: An array of parameters
* ``parameters``: A mapping of parameters
.. _RFC 2397: https://tools.ietf.org/html/rfc2397
+10 -4
View File
@@ -1,14 +1,20 @@
``keys``
========
The ``keys`` filter returns the keys of an array. It is useful when you want to
iterate over the keys of an array:
The ``keys`` filter returns the keys of a sequence or a mapping. It is useful
when you want to iterate over the keys of a sequence or a mapping:
.. code-block:: twig
{% for key in array|keys %}
...
{% for key in [1, 2, 3, 4]|keys %}
{{ key }}
{% endfor %}
{# outputs: 1 2 3 4 #}
{% for key in {a: 'a_value', b: 'b_value'}|keys %}
{{ key }}
{% endfor %}
{# outputs: a b #}
.. note::
+7 -9
View File
@@ -1,7 +1,9 @@
``merge``
=========
The ``merge`` filter merges an array with another array:
The ``merge`` filter merges sequences and mappings:
For sequences, new values are added at the end of the existing ones:
.. code-block:: twig
@@ -11,9 +13,9 @@ The ``merge`` filter merges an array with another array:
{# values now contains [1, 2, 'apple', 'orange'] #}
New values are added at the end of the existing ones.
The ``merge`` filter also works on hashes:
For mappings, the merging process occurs on the keys; if the key does not
already exist, it is added but if the key already exists, its value is
overridden:
.. code-block:: twig
@@ -23,13 +25,9 @@ The ``merge`` filter also works on hashes:
{# items now contains {'apple': 'fruit', 'orange': 'fruit', 'peugeot': 'car', 'renault': 'car'} #}
For hashes, the merging process occurs on the keys: if the key does not
already exist, it is added but if the key already exists, its value is
overridden.
.. tip::
If you want to ensure that some values are defined in an array (by given
If you want to ensure that some values are defined in a mapping (by given
default values), reverse the two elements in the call:
.. code-block:: twig
+1 -1
View File
@@ -17,7 +17,7 @@ format is free-form):
Arguments
---------
* ``from``: The placeholder values as a hash
* ``from``: The placeholder values as a mapping
.. seealso::
+2 -2
View File
@@ -1,7 +1,7 @@
``sort``
========
The ``sort`` filter sorts an array:
The ``sort`` filter sorts sequences and mappings:
.. code-block:: twig
@@ -15,7 +15,7 @@ The ``sort`` filter sorts an array:
association. It supports Traversable objects by transforming
those to arrays.
You can pass an arrow function to sort the array:
You can pass an arrow function to configure the sorting:
.. code-block:: html+twig
+1 -1
View File
@@ -11,7 +11,7 @@ of strings:
You can also pass a ``limit`` argument:
* If ``limit`` is positive, the returned array will contain a maximum of
* If ``limit`` is positive, the returned sequence will contain a maximum of
limit elements with the last element containing the rest of string;
* If ``limit`` is negative, all components except the last -limit are
+2 -2
View File
@@ -1,8 +1,8 @@
``url_encode``
==============
The ``url_encode`` filter percent encodes a given string as URL segment
or an array as query string:
The ``url_encode`` filter percent encodes a given string as URL segment or a
mapping as query string:
.. code-block:: twig
+1 -1
View File
@@ -1,7 +1,7 @@
``cycle``
=========
The ``cycle`` function cycles on an array of values:
The ``cycle`` function cycles on a sequence or mapping:
.. code-block:: twig
+4 -4
View File
@@ -1,8 +1,8 @@
``for``
=======
Loop over each item in a sequence. For example, to display a list of users
provided in a variable called ``users``:
Loop over each item in a sequence or a mapping. For example, to display a list
of users provided in a variable called ``users``:
.. code-block:: html+twig
@@ -15,8 +15,8 @@ provided in a variable called ``users``:
.. note::
A sequence can be either an array or an object implementing the
``Traversable`` interface.
A sequence or a mapping can be either an array or an object implementing
the ``Traversable`` interface.
If you do need to iterate over a sequence of numbers, you can use the ``..``
operator:
+5 -3
View File
@@ -12,7 +12,7 @@ In the simplest form you can use it to test if an expression evaluates to
<p>Our website is in maintenance mode. Please, come back later.</p>
{% endif %}
You can also test if an array is not empty:
You can also test if a sequence or a mapping is not empty:
.. code-block:: html+twig
@@ -71,8 +71,10 @@ use more complex ``expressions`` there too:
INF (Infinity) true
whitespace-only string true
string "0" or '0' false
empty array false
empty sequence false
empty mapping false
null false
non-empty array true
non-empty sequence true
non-empty mapping true
object true
====================== ====================
+3 -3
View File
@@ -13,8 +13,8 @@ scope are not visible outside of the scope:
foo is not visible here any longer
Instead of defining variables at the beginning of the scope, you can pass a
hash of variables you want to define in the ``with`` tag; the previous example
is equivalent to the following one:
mapping of variables you want to define in the ``with`` tag; the previous
example is equivalent to the following one:
.. code-block:: twig
@@ -23,7 +23,7 @@ is equivalent to the following one:
{% endwith %}
foo is not visible here any longer
{# it works with any expression that resolves to a hash #}
{# it works with any expression that resolves to a mapping #}
{% set vars = {foo: 42} %}
{% with vars %}
...
+12 -12
View File
@@ -102,7 +102,7 @@ If a variable or attribute does not exist, the behavior depends on the
For convenience's sake ``foo.bar`` does the following things on the PHP
layer:
* check if ``foo`` is an array and ``bar`` a valid element;
* check if ``foo`` is a sequence or a mapping and ``bar`` a valid element;
* if not, and if ``foo`` is an object, check that ``bar`` is a valid property;
* if not, and if ``foo`` is an object, check that ``bar`` is a valid method
(even if ``bar`` is the constructor - use ``__construct()`` instead);
@@ -115,7 +115,7 @@ If a variable or attribute does not exist, the behavior depends on the
Twig also supports a specific syntax for accessing items on PHP arrays,
``foo['bar']``:
* check if ``foo`` is an array and ``bar`` a valid element;
* check if ``foo`` is a sequence or a mapping and ``bar`` a valid element;
* if not, and if ``strict_variables`` is ``false``, return ``null``;
* if not, throw an exception.
@@ -531,10 +531,10 @@ exist:
writing the number down. If a dot is present the number is a float,
otherwise an integer.
* ``["foo", "bar"]``: Arrays are defined by a sequence of expressions
* ``["foo", "bar"]``: Sequences are defined by a sequence of expressions
separated by a comma (``,``) and wrapped with squared brackets (``[]``).
* ``{"foo": "bar"}``: Hashes are defined by a list of keys and values
* ``{"foo": "bar"}``: Mappings are defined by a list of keys and values
separated by a comma (``,``) and wrapped with curly braces (``{}``):
.. code-block:: twig
@@ -542,7 +542,7 @@ exist:
{# keys as string #}
{'foo': 'foo', 'bar': 'bar'}
{# keys as names (equivalent to the previous hash) #}
{# keys as names (equivalent to the previous mapping) #}
{foo: 'foo', bar: 'bar'}
{# keys as integer #}
@@ -563,7 +563,7 @@ exist:
* ``null``: ``null`` represents no specific value. This is the value returned
when a variable does not exist. ``none`` is an alias for ``null``.
Arrays and hashes can be nested:
Sequences and mappings can be nested:
.. code-block:: twig
@@ -698,8 +698,8 @@ operand is contained in the right:
.. tip::
You can use this filter to perform a containment test on strings, arrays,
or objects implementing the ``Traversable`` interface.
You can use this filter to perform a containment test on strings,
sequences, mappings, or objects implementing the ``Traversable`` interface.
To perform a negative test, use the ``not in`` operator:
@@ -785,8 +785,8 @@ The following operators don't fit into any of the other categories:
{# returns the value of foo if it is defined and not null, 'no' otherwise #}
{{ foo ?? 'no' }}
* ``...``: The spread operator can be used to expand arrays or hashes (it cannot
be used to expand the arguments of a function call):
* ``...``: The spread operator can be used to expand sequences or mappings (it
cannot be used to expand the arguments of a function call):
.. code-block:: twig
@@ -819,7 +819,7 @@ Operator Score of precedence Description
``ends with``, ``has some``,
``has every``
``..`` 25 Range of values
``+``, ``-`` 30 Addition and substraction on numbers
``+``, ``-`` 30 Addition and subtraction on numbers
``~`` 40 String concatenation
``not`` 50 Negates a statement
``*``, ``/``, ``//``, ``%`` 60 Arithmetic operations on numbers
@@ -827,7 +827,7 @@ Operator Score of precedence Description
``**`` 200 Raises a number to the power of another
``??`` 300 Default value when a variable is null
``+``, ``-`` 500 Unary operations on numbers
``|``,``[]``,``.`` - Filters, array, hash, and attribute access
``|``,``[]``,``.`` - Filters, sequence, mapping, and attribute access
============================= =================================== =====================================================
Without using any parentheses, the operator precedence rules are used to
+2 -2
View File
@@ -1,8 +1,8 @@
``empty``
=========
``empty`` checks if a variable is an empty string, an empty array, an empty
hash, exactly ``false``, or exactly ``null``.
``empty`` checks if a variable is an empty string, an empty sequence, an empty
mapping, exactly ``false``, or exactly ``null``.
For objects that implement the ``Countable`` interface, ``empty`` will check the
return value of the ``count()`` method.
+29 -12
View File
@@ -278,9 +278,9 @@ class ExpressionParser
// no break
default:
if ($token->test(/* Token::PUNCTUATION_TYPE */ 9, '[')) {
$node = $this->parseArrayExpression();
$node = $this->parseSequenceExpression();
} elseif ($token->test(/* Token::PUNCTUATION_TYPE */ 9, '{')) {
$node = $this->parseHashExpression();
$node = $this->parseMappingExpression();
} elseif ($token->test(/* Token::OPERATOR_TYPE */ 8, '=') && ('==' === $this->parser->getStream()->look(-1)->getValue() || '!=' === $this->parser->getStream()->look(-1)->getValue())) {
throw new SyntaxError(\sprintf('Unexpected operator of value "%s". Did you try to use "===" or "!==" for strict comparison? Use "is same as(value)" instead.', $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext());
} else {
@@ -320,15 +320,22 @@ class ExpressionParser
}
public function parseArrayExpression()
{
trigger_deprecation('twig/twig', '3.11', 'Calling "%s()" is deprecated, use "parseSequenceExpression()" instead.', __METHOD__);
return $this->parseSequenceExpression();
}
public function parseSequenceExpression()
{
$stream = $this->parser->getStream();
$stream->expect(/* Token::PUNCTUATION_TYPE */ 9, '[', 'An array element was expected');
$stream->expect(/* Token::PUNCTUATION_TYPE */ 9, '[', 'A sequence element was expected');
$node = new ArrayExpression([], $stream->getCurrent()->getLine());
$first = true;
while (!$stream->test(/* Token::PUNCTUATION_TYPE */ 9, ']')) {
if (!$first) {
$stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ',', 'An array element must be followed by a comma');
$stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ',', 'A sequence element must be followed by a comma');
// trailing ,?
if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, ']')) {
@@ -346,21 +353,31 @@ class ExpressionParser
$node->addElement($this->parseExpression());
}
}
$stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ']', 'An opened array is not properly closed');
$stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ']', 'An opened sequence is not properly closed');
return $node;
}
/**
* @deprecated since 3.11, use parseMappingExpression() instead
*/
public function parseHashExpression()
{
trigger_deprecation('twig/twig', '3.11', 'Calling "%s()" is deprecated, use "parseMappingExpression()" instead.', __METHOD__);
return $this->parseMappingExpression();
}
public function parseMappingExpression()
{
$stream = $this->parser->getStream();
$stream->expect(/* Token::PUNCTUATION_TYPE */ 9, '{', 'A hash element was expected');
$stream->expect(/* Token::PUNCTUATION_TYPE */ 9, '{', 'A mapping element was expected');
$node = new ArrayExpression([], $stream->getCurrent()->getLine());
$first = true;
while (!$stream->test(/* Token::PUNCTUATION_TYPE */ 9, '}')) {
if (!$first) {
$stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ',', 'A hash value must be followed by a comma');
$stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ',', 'A mapping value must be followed by a comma');
// trailing ,?
if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, '}')) {
@@ -377,7 +394,7 @@ class ExpressionParser
continue;
}
// a hash key can be:
// a mapping key can be:
//
// * a number -- 12
// * a string -- 'a'
@@ -399,15 +416,15 @@ class ExpressionParser
} else {
$current = $stream->getCurrent();
throw new SyntaxError(\sprintf('A hash key must be a quoted string, a number, a name, or an expression enclosed in parentheses (unexpected token "%s" of value "%s".', Token::typeToEnglish($current->getType()), $current->getValue()), $current->getLine(), $stream->getSourceContext());
throw new SyntaxError(\sprintf('A mapping key must be a quoted string, a number, a name, or an expression enclosed in parentheses (unexpected token "%s" of value "%s".', Token::typeToEnglish($current->getType()), $current->getValue()), $current->getLine(), $stream->getSourceContext());
}
$stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ':', 'A hash key must be followed by a colon (:)');
$stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ':', 'A mapping key must be followed by a colon (:)');
$value = $this->parseExpression();
$node->addElement($value, $key);
}
$stream->expect(/* Token::PUNCTUATION_TYPE */ 9, '}', 'An opened hash is not properly closed');
$stream->expect(/* Token::PUNCTUATION_TYPE */ 9, '}', 'An opened mapping is not properly closed');
return $node;
}
@@ -631,7 +648,7 @@ class ExpressionParser
$value = $this->parsePrimaryExpression();
if (!$this->checkConstantExpression($value)) {
throw new SyntaxError('A default value for an argument must be a constant (a boolean, a string, a number, or an array).', $token->getLine(), $stream->getSourceContext());
throw new SyntaxError('A default value for an argument must be a constant (a boolean, a string, a number, a sequence, or a mapping).', $token->getLine(), $stream->getSourceContext());
}
} else {
$value = $this->parseExpression(0, $allowArrow);
+12 -12
View File
@@ -329,7 +329,7 @@ final class CoreExtension extends AbstractExtension
}
if (!\count($values)) {
throw new RuntimeError('The "cycle" function does not work on empty arrays.');
throw new RuntimeError('The "cycle" function does not work on empty sequences/mappings.');
}
return $values[$position % \count($values)];
@@ -399,7 +399,7 @@ final class CoreExtension extends AbstractExtension
$values = self::toArray($values);
if (0 === \count($values)) {
throw new RuntimeError('The random function cannot pick from an empty array.');
throw new RuntimeError('The random function cannot pick from an empty sequence/mapping.');
}
return $values[array_rand($values, 1)];
@@ -532,7 +532,7 @@ final class CoreExtension extends AbstractExtension
public static function replace($str, $from): string
{
if (!is_iterable($from)) {
throw new RuntimeError(\sprintf('The "replace" filter expects an array or "Traversable" as replace values, got "%s".', get_debug_type($from)));
throw new RuntimeError(\sprintf('The "replace" filter expects a sequence/mapping or "Traversable" as replace values, got "%s".', get_debug_type($from)));
}
return strtr($str ?? '', self::toArray($from));
@@ -629,7 +629,7 @@ final class CoreExtension extends AbstractExtension
foreach ($arrays as $argNumber => $array) {
if (!is_iterable($array)) {
throw new RuntimeError(\sprintf('The merge filter only works with arrays or "Traversable", got "%s" for argument %d.', \gettype($array), $argNumber + 1));
throw new RuntimeError(\sprintf('The merge filter only works with sequences/mappings or "Traversable", got "%s" for argument %d.', \gettype($array), $argNumber + 1));
}
$result = array_merge($result, self::toArray($array));
@@ -903,7 +903,7 @@ final class CoreExtension extends AbstractExtension
if ($array instanceof \Traversable) {
$array = iterator_to_array($array);
} elseif (!\is_array($array)) {
throw new RuntimeError(\sprintf('The sort filter only works with arrays or "Traversable", got "%s".', \gettype($array)));
throw new RuntimeError(\sprintf('The sort filter only works with sequences/mappings or "Traversable", got "%s".', \gettype($array)));
}
if (null !== $arrow) {
@@ -1416,7 +1416,7 @@ final class CoreExtension extends AbstractExtension
public static function batch($items, $size, $fill = null, $preserveKeys = true): array
{
if (!is_iterable($items)) {
throw new RuntimeError(\sprintf('The "batch" filter expects an array or "Traversable", got "%s".', get_debug_type($items)));
throw new RuntimeError(\sprintf('The "batch" filter expects a sequence/mapping or "Traversable", got "%s".', get_debug_type($items)));
}
$size = ceil($size);
@@ -1483,9 +1483,9 @@ final class CoreExtension extends AbstractExtension
$message = \sprintf('Impossible to access a key "%s" on an object of class "%s" that does not implement ArrayAccess interface.', $item, $object::class);
} elseif (\is_array($object)) {
if (empty($object)) {
$message = \sprintf('Key "%s" does not exist as the array is empty.', $arrayItem);
$message = \sprintf('Key "%s" does not exist as the sequence/mapping is empty.', $arrayItem);
} else {
$message = \sprintf('Key "%s" for array with keys "%s" does not exist.', $arrayItem, implode(', ', array_keys($object)));
$message = \sprintf('Key "%s" for sequence/mapping with keys "%s" does not exist.', $arrayItem, implode(', ', array_keys($object)));
}
} elseif (/* Template::ARRAY_CALL */ 'array' === $type) {
if (null === $object) {
@@ -1515,7 +1515,7 @@ final class CoreExtension extends AbstractExtension
if (null === $object) {
$message = \sprintf('Impossible to invoke a method ("%s") on a null variable.', $item);
} elseif (\is_array($object)) {
$message = \sprintf('Impossible to invoke a method ("%s") on an array.', $item);
$message = \sprintf('Impossible to invoke a method ("%s") on a sequence/mapping.', $item);
} else {
$message = \sprintf('Impossible to invoke a method ("%s") on a %s variable ("%s").', $item, \gettype($object), $object);
}
@@ -1653,7 +1653,7 @@ final class CoreExtension extends AbstractExtension
if ($array instanceof \Traversable) {
$array = iterator_to_array($array);
} elseif (!\is_array($array)) {
throw new RuntimeError(\sprintf('The column filter only works with arrays or "Traversable", got "%s" as first argument.', \gettype($array)));
throw new RuntimeError(\sprintf('The column filter only works with sequences/mappings or "Traversable", got "%s" as first argument.', \gettype($array)));
}
return array_column($array, $name, $index);
@@ -1665,7 +1665,7 @@ final class CoreExtension extends AbstractExtension
public static function filter(Environment $env, $array, $arrow)
{
if (!is_iterable($array)) {
throw new RuntimeError(\sprintf('The "filter" filter expects an array or "Traversable", got "%s".', get_debug_type($array)));
throw new RuntimeError(\sprintf('The "filter" filter expects a sequence/mapping or "Traversable", got "%s".', get_debug_type($array)));
}
self::checkArrowInSandbox($env, $arrow, 'filter', 'filter');
@@ -1701,7 +1701,7 @@ final class CoreExtension extends AbstractExtension
self::checkArrowInSandbox($env, $arrow, 'reduce', 'filter');
if (!\is_array($array) && !$array instanceof \Traversable) {
throw new RuntimeError(\sprintf('The "reduce" filter only works with arrays or "Traversable", got "%s" as first argument.', \gettype($array)));
throw new RuntimeError(\sprintf('The "reduce" filter only works with sequences/mappings or "Traversable", got "%s" as first argument.', \gettype($array)));
}
$accumulator = $initial;
+1 -1
View File
@@ -49,7 +49,7 @@ class WithNode extends Node
->raw(";\n")
->write(\sprintf("if (!is_iterable(\$%s)) {\n", $varsName))
->indent()
->write("throw new RuntimeError('Variables passed to the \"with\" tag must be a hash.', ")
->write("throw new RuntimeError('Variables passed to the \"with\" tag must be a mapping.', ")
->repr($node->getTemplateLine())
->raw(", \$this->getSourceContext());\n")
->outdent()
+15 -15
View File
@@ -55,9 +55,9 @@ class ExpressionParserTest extends TestCase
}
/**
* @dataProvider getTestsForArray
* @dataProvider getTestsForSequence
*/
public function testArrayExpression($template, $expected)
public function testSequenceExpression($template, $expected)
{
$env = new Environment($this->createMock(LoaderInterface::class), ['cache' => false, 'autoescape' => false]);
$stream = $env->tokenize($source = new Source($template, ''));
@@ -68,9 +68,9 @@ class ExpressionParserTest extends TestCase
}
/**
* @dataProvider getFailingTestsForArray
* @dataProvider getFailingTestsForSequence
*/
public function testArraySyntaxError($template)
public function testSequenceSyntaxError($template)
{
$this->expectException(SyntaxError::class);
@@ -79,7 +79,7 @@ class ExpressionParserTest extends TestCase
$parser->parse($env->tokenize(new Source($template, 'index')));
}
public function getFailingTestsForArray()
public function getFailingTestsForSequence()
{
return [
['{{ [1, "a": "b"] }}'],
@@ -88,10 +88,10 @@ class ExpressionParserTest extends TestCase
];
}
public function getTestsForArray()
public function getTestsForSequence()
{
return [
// simple array
// simple sequence
['{{ [1, 2] }}', new ArrayExpression([
new ConstantExpression(0, 1),
new ConstantExpression(1, 1),
@@ -101,7 +101,7 @@ class ExpressionParserTest extends TestCase
], 1),
],
// array with trailing ,
// sequence with trailing ,
['{{ [1, 2, ] }}', new ArrayExpression([
new ConstantExpression(0, 1),
new ConstantExpression(1, 1),
@@ -111,7 +111,7 @@ class ExpressionParserTest extends TestCase
], 1),
],
// simple hash
// simple mapping
['{{ {"a": "b", "b": "c"} }}', new ArrayExpression([
new ConstantExpression('a', 1),
new ConstantExpression('b', 1),
@@ -121,7 +121,7 @@ class ExpressionParserTest extends TestCase
], 1),
],
// hash with trailing ,
// mapping with trailing ,
['{{ {"a": "b", "b": "c", } }}', new ArrayExpression([
new ConstantExpression('a', 1),
new ConstantExpression('b', 1),
@@ -131,7 +131,7 @@ class ExpressionParserTest extends TestCase
], 1),
],
// hash in an array
// mapping in a sequence
['{{ [1, {"a": "b", "b": "c"}] }}', new ArrayExpression([
new ConstantExpression(0, 1),
new ConstantExpression(1, 1),
@@ -147,7 +147,7 @@ class ExpressionParserTest extends TestCase
], 1),
],
// array in a hash
// sequence in a mapping
['{{ {"a": [1, 2], "b": "c"} }}', new ArrayExpression([
new ConstantExpression('a', 1),
new ArrayExpression([
@@ -168,7 +168,7 @@ class ExpressionParserTest extends TestCase
new NameExpression('b', 1),
], 1)],
// array with spread operator
// sequence with spread operator
['{{ [1, 2, ...foo] }}',
new ArrayExpression([
new ConstantExpression(0, 1),
@@ -181,7 +181,7 @@ class ExpressionParserTest extends TestCase
$this->createNameExpression('foo', ['spread' => true]),
], 1)],
// hash with spread operator
// mapping with spread operator
['{{ {"a": "b", "b": "c", ...otherLetters} }}',
new ArrayExpression([
new ConstantExpression('a', 1),
@@ -301,7 +301,7 @@ class ExpressionParserTest extends TestCase
public function testMacroDefinitionDoesNotSupportNonConstantDefaultValues($template)
{
$this->expectException(SyntaxError::class);
$this->expectExceptionMessage('A default value for an argument must be a constant (a boolean, a string, a number, or an array) in "index" at line 1');
$this->expectExceptionMessage('A default value for an argument must be a constant (a boolean, a string, a number, a sequence, or a mapping) in "index" at line 1');
$env = new Environment($this->createMock(LoaderInterface::class), ['cache' => false, 'autoescape' => false]);
$parser = new Parser($env);
@@ -9,4 +9,4 @@ Exception thrown from a child for an extension error
--DATA--
return []
--EXCEPTION--
Twig\Error\RuntimeError: The random function cannot pick from an empty array in "base.twig" at line 4.
Twig\Error\RuntimeError: The random function cannot pick from an empty sequence/mapping in "base.twig" at line 4.
@@ -9,4 +9,4 @@ Exception thrown from an include for an extension error
--DATA--
return []
--EXCEPTION--
Twig\Error\RuntimeError: The random function cannot pick from an empty array in "content.twig" at line 4.
Twig\Error\RuntimeError: The random function cannot pick from an empty sequence/mapping in "content.twig" at line 4.
@@ -1,5 +1,5 @@
--TEST--
Twig supports the spread operator on hashes
Twig supports the spread operator on mappings
--TEMPLATE--
{% for key, value in { firstName: 'Ryan', lastName: 'Weaver', favoriteFood: 'popcorn', ...{favoriteFood: 'pizza', sport: 'running'} } %}
{{ key }}: {{ value }}
@@ -5,4 +5,4 @@ Exception for invalid argument type in replace call
--DATA--
return ['stdClass' => new \stdClass()]
--EXCEPTION--
Twig\Error\RuntimeError: The "replace" filter expects an array or "Traversable" as replace values, got "stdClass" in "index.twig" at line 2.
Twig\Error\RuntimeError: The "replace" filter expects a sequence/mapping or "Traversable" as replace values, got "stdClass" in "index.twig" at line 2.
@@ -0,0 +1,8 @@
--TEST--
"cycle" function returns an error on empty mappings
--TEMPLATE--
{{ cycle({}, 0) }}
--DATA--
return []
--EXCEPTION--
Twig\Error\RuntimeError: The "cycle" function does not work on empty sequences/mappings in "index.twig" at line 2.
@@ -1,8 +1,8 @@
--TEST--
"cycle" function returns an error on empty arrays
"cycle" function returns an error on empty sequences
--TEMPLATE--
{{ cycle([], 0) }}
--DATA--
return []
--EXCEPTION--
Twig\Error\RuntimeError: The "cycle" function does not work on empty arrays in "index.twig" at line 2.
Twig\Error\RuntimeError: The "cycle" function does not work on empty sequences/mappings in "index.twig" at line 2.
@@ -1,8 +1,8 @@
--TEST--
Twig does not confuse strings with integers in getAttribute()
--TEMPLATE--
{{ hash['2e2'] }}
{{ mapping['2e2'] }}
--DATA--
return ['hash' => ['2e2' => 'works']]
return ['mapping' => ['2e2' => 'works']]
--EXPECT--
works
@@ -1,10 +1,10 @@
--TEST--
"with" tag with an expression that is not a hash
"with" tag with an expression that is not a mapping
--TEMPLATE--
{% with vars %}
{{ foo }}{{ bar }}
{% endwith %}
--DATA--
return ['vars' => 'no-hash']
return ['vars' => 'no-mapping']
--EXCEPTION--
Twig\Error\RuntimeError: Variables passed to the "with" tag must be a hash in "index.twig" at line 2.
Twig\Error\RuntimeError: Variables passed to the "with" tag must be a mapping in "index.twig" at line 2.
+15 -15
View File
@@ -66,17 +66,17 @@ class TemplateTest extends TestCase
return [
['{{ string["a"] }}', 'Impossible to access a key ("a") on a string variable ("foo") in "%s" at line 1.'],
['{{ null["a"] }}', 'Impossible to access a key ("a") on a null variable in "%s" at line 1.'],
['{{ empty_array["a"] }}', 'Key "a" does not exist as the array is empty in "%s" at line 1.'],
['{{ array["a"] }}', 'Key "a" for array with keys "foo" does not exist in "%s" at line 1.'],
['{{ empty_array["a"] }}', 'Key "a" does not exist as the sequence/mapping is empty in "%s" at line 1.'],
['{{ array["a"] }}', 'Key "a" for sequence/mapping with keys "foo" does not exist in "%s" at line 1.'],
['{{ array_access["a"] }}', 'Key "a" in object with ArrayAccess of class "Twig\Tests\TemplateArrayAccessObject" does not exist in "%s" at line 1.'],
['{{ string.a }}', 'Impossible to access an attribute ("a") on a string variable ("foo") in "%s" at line 1.'],
['{{ string.a() }}', 'Impossible to invoke a method ("a") on a string variable ("foo") in "%s" at line 1.'],
['{{ null.a }}', 'Impossible to access an attribute ("a") on a null variable in "%s" at line 1.'],
['{{ null.a() }}', 'Impossible to invoke a method ("a") on a null variable in "%s" at line 1.'],
['{{ array.a() }}', 'Impossible to invoke a method ("a") on an array in "%s" at line 1.'],
['{{ empty_array.a }}', 'Key "a" does not exist as the array is empty in "%s" at line 1.'],
['{{ array.a }}', 'Key "a" for array with keys "foo" does not exist in "%s" at line 1.'],
['{{ attribute(array, -10) }}', 'Key "-10" for array with keys "foo" does not exist in "%s" at line 1.'],
['{{ array.a() }}', 'Impossible to invoke a method ("a") on a sequence/mapping in "%s" at line 1.'],
['{{ empty_array.a }}', 'Key "a" does not exist as the sequence/mapping is empty in "%s" at line 1.'],
['{{ array.a }}', 'Key "a" for sequence/mapping with keys "foo" does not exist in "%s" at line 1.'],
['{{ attribute(array, -10) }}', 'Key "-10" for sequence/mapping with keys "foo" does not exist in "%s" at line 1.'],
['{{ array_access.a }}', 'Neither the property "a" nor one of the methods "a()", "geta()"/"isa()"/"hasa()" or "__call()" exist and have public access in class "Twig\Tests\TemplateArrayAccessObject" in "%s" at line 1.'],
['{% from _self import foo %}{% macro foo(obj) %}{{ obj.missing_method() }}{% endmacro %}{{ foo(array_access) }}', 'Neither the property "missing_method" nor one of the methods "missing_method()", "getmissing_method()"/"ismissing_method()"/"hasmissing_method()" or "__call()" exist and have public access in class "Twig\Tests\TemplateArrayAccessObject" in "%s" at line 1.'],
['{{ magic_exception.test }}', 'An exception has been thrown during the rendering of a template ("Hey! Don\'t try to isset me!") in "%s" at line 1.'],
@@ -175,14 +175,14 @@ class TemplateTest extends TestCase
$this->assertSame('IntegerButStringWithLeadingZeros', $array['01']);
$this->assertSame('EmptyString', $array[null]);
$this->assertSame('Zero', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, false), 'false is treated as 0 when accessing an array (equals PHP behavior)');
$this->assertSame('One', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, true), 'true is treated as 1 when accessing an array (equals PHP behavior)');
$this->assertSame('One', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, 1.5), 'float is casted to int when accessing an array (equals PHP behavior)');
$this->assertSame('One', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, '1'), '"1" is treated as integer 1 when accessing an array (equals PHP behavior)');
$this->assertSame('MinusOne', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, -1.5), 'negative float is casted to int when accessing an array (equals PHP behavior)');
$this->assertSame('FloatButString', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, '1.5'), '"1.5" is treated as-is when accessing an array (equals PHP behavior)');
$this->assertSame('IntegerButStringWithLeadingZeros', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, '01'), '"01" is treated as-is when accessing an array (equals PHP behavior)');
$this->assertSame('EmptyString', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, null), 'null is treated as "" when accessing an array (equals PHP behavior)');
$this->assertSame('Zero', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, false), 'false is treated as 0 when accessing a sequence/mapping (equals PHP behavior)');
$this->assertSame('One', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, true), 'true is treated as 1 when accessing a sequence/mapping (equals PHP behavior)');
$this->assertSame('One', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, 1.5), 'float is casted to int when accessing a sequence/mapping (equals PHP behavior)');
$this->assertSame('One', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, '1'), '"1" is treated as integer 1 when accessing a sequence/mapping (equals PHP behavior)');
$this->assertSame('MinusOne', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, -1.5), 'negative float is casted to int when accessing a sequence/mapping (equals PHP behavior)');
$this->assertSame('FloatButString', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, '1.5'), '"1.5" is treated as-is when accessing a sequence/mapping (equals PHP behavior)');
$this->assertSame('IntegerButStringWithLeadingZeros', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, '01'), '"01" is treated as-is when accessing a sequence/mapping (equals PHP behavior)');
$this->assertSame('EmptyString', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, null), 'null is treated as "" when accessing a sequence/mapping (equals PHP behavior)');
}
/**
@@ -377,7 +377,7 @@ class TemplateTest extends TestCase
$tests = array_merge($tests, [
[false, null, 42, 'a', [], $anyType, 'Impossible to access an attribute ("a") on a integer variable ("42") in "index.twig".'],
[false, null, 'string', 'a', [], $anyType, 'Impossible to access an attribute ("a") on a string variable ("string") in "index.twig".'],
[false, null, [], 'a', [], $anyType, 'Key "a" does not exist as the array is empty in "index.twig".'],
[false, null, [], 'a', [], $anyType, 'Key "a" does not exist as the sequence/mapping is empty in "index.twig".'],
]);
return $tests;