N°8910 - Upgrade Symfony packages (#811)

This commit is contained in:
Benjamin Dalsass
2026-02-23 06:54:26 +01:00
committed by GitHub
parent b91e6c384a
commit 4853ca444e
224 changed files with 4758 additions and 1778 deletions

View File

@@ -1,3 +1,34 @@
# 3.23.0 (2026-01-23)
* Add `=` assignment operator (allows to set variables in expression or to replace the short-form of the set tag)
* Add sequence, mapping, and object destructuring
* Add `?.` null-safe operator
* Add `===` and `!==` operators (equivalent to the `same as` and `not same as` tests)
* Fix opcache preload warning for unlinked anonymous class
* Fix spread operator behavior
# 3.22.2 (2025-12-14)
* Fix "cycle" with non-countable ArrayAccess + Traversable objects
* Use "getShareDir" as an indicator of Symfony version in Symfony bundle
* Fix escaper compatibility with PHP 8.5
# 3.22.1 (2025-11-16)
* Add support for Symfony 8
# 3.22.0 (2025-10-29)
* Add support for two words test in guard tag
* Add `Environment::registerUndefinedTestCallback()`
* Fix compatibility with Symfony 8
* Fix accessing arrays with stringable objects as key
* Avoid errors when failing to guess the template info for an error
* Fix expression parser compatibility layer
* Fix compiling 'index' with repr (not string) in EmbedNode
* Update configuration keys + allow extra keys for CommonMark extensions
* Allow usage of other Markdown converters than CommonMark in LeagueMarkdown
# 3.21.1 (2025-05-03)
* Fix ExtensionSet usage of BinaryOperatorExpressionParser
@@ -44,6 +75,7 @@
# 3.18.0 (2024-12-29)
* Support for invoking closures
* Fix unary operator precedence change
* Ignore `SyntaxError` exceptions from undefined handlers when using the `guard` tag
* Add a way to stream template rendering (`TemplateWrapper::stream()` and `TemplateWrapper::streamBlock()`)
@@ -52,7 +84,6 @@
* Fix the null coalescing operator when the test returns null
* Fix the Elvis operator when used as '? :' instead of '?:'
* Support for invoking closures
# 3.17.0 (2024-12-10)

View File

@@ -1,25 +0,0 @@
parameters:
ignoreErrors:
- # The method is dynamically generated by the CheckSecurityNode
message: '#^Call to an undefined method Twig\\Template\:\:checkSecurity\(\)\.$#'
identifier: method.notFound
count: 1
path: src/Extension/CoreExtension.php
- # 2 parameters will be required
message: '#^Method Twig\\Node\\IncludeNode\:\:addGetTemplate\(\) invoked with 2 parameters, 1 required\.$#'
identifier: arguments.count
count: 1
path: src/Node/IncludeNode.php
- # int|string will be supported in 4.x
message: '#^PHPDoc tag @param for parameter $name with type int|string is not subtype of native type string\.$#'
identifier: parameter.phpDocType
count: 5
path: src/Node/Node.php
- # Adding 0 to the string representation of a number is valid and what we want here
message: '#^Binary operation "\+" between 0 and string results in an error\.$#'
identifier: binaryOp.invalid
count: 1
path: src/Lexer.php

View File

@@ -1,9 +0,0 @@
includes:
- phpstan-baseline.neon
parameters:
level: 3
paths:
- src
excludePaths:
- src/Test

View File

@@ -0,0 +1,15 @@
{
"subtrees": {
"twig-extra-bundle": "extra/twig-extra-bundle",
"cache-extra": "extra/cache-extra",
"cssinliner-extra": "extra/cssinliner-extra",
"html-extra": "extra/html-extra",
"inky-extra": "extra/inky-extra",
"intl-extra": "extra/intl-extra",
"markdown-extra": "extra/markdown-extra",
"string-extra": "extra/string-extra"
},
"defaults": {
"git_constraint": "<1.8.2"
}
}

View File

@@ -31,15 +31,15 @@ use Twig\TwigFilter;
final class AsTwigFilter
{
/**
* @param non-empty-string $name The name of the filter in Twig.
* @param bool|null $needsCharset Whether the filter needs the charset passed as the first argument.
* @param bool|null $needsEnvironment Whether the filter needs the environment passed as the first argument, or after the charset.
* @param bool|null $needsContext Whether the filter needs the context array passed as the first argument, or after the charset and the environment.
* @param string[]|null $isSafe List of formats in which you want the raw output to be printed unescaped.
* @param string|array|null $isSafeCallback Function called at compilation time to determine if the filter is safe.
* @param string|null $preEscape Some filters may need to work on input that is already escaped or safe
* @param string[]|null $preservesSafety Preserves the safety of the value that the filter is applied to.
* @param DeprecatedCallableInfo|null $deprecationInfo Information about the deprecation
* @param non-empty-string $name The name of the filter in Twig
* @param bool|null $needsCharset Whether the filter needs the charset passed as the first argument
* @param bool|null $needsEnvironment Whether the filter needs the environment passed as the first argument, or after the charset
* @param bool|null $needsContext Whether the filter needs the context array passed as the first argument, or after the charset and the environment
* @param string[]|null $isSafe List of formats in which you want the raw output to be printed unescaped
* @param string|array|null $isSafeCallback Function called at compilation time to determine if the filter is safe
* @param string|null $preEscape Some filters may need to work on input that is already escaped or safe
* @param string[]|null $preservesSafety Preserves the safety of the value that the filter is applied to
* @param DeprecatedCallableInfo|null $deprecationInfo Information about the deprecation
*/
public function __construct(
public string $name,

View File

@@ -31,13 +31,13 @@ use Twig\TwigFunction;
final class AsTwigFunction
{
/**
* @param non-empty-string $name The name of the function in Twig.
* @param bool|null $needsCharset Whether the function needs the charset passed as the first argument.
* @param bool|null $needsEnvironment Whether the function needs the environment passed as the first argument, or after the charset.
* @param bool|null $needsContext Whether the function needs the context array passed as the first argument, or after the charset and the environment.
* @param string[]|null $isSafe List of formats in which you want the raw output to be printed unescaped.
* @param string|array|null $isSafeCallback Function called at compilation time to determine if the function is safe.
* @param DeprecatedCallableInfo|null $deprecationInfo Information about the deprecation
* @param non-empty-string $name The name of the function in Twig
* @param bool|null $needsCharset Whether the function needs the charset passed as the first argument
* @param bool|null $needsEnvironment Whether the function needs the environment passed as the first argument, or after the charset
* @param bool|null $needsContext Whether the function needs the context array passed as the first argument, or after the charset and the environment
* @param string[]|null $isSafe List of formats in which you want the raw output to be printed unescaped
* @param string|array|null $isSafeCallback Function called at compilation time to determine if the function is safe
* @param DeprecatedCallableInfo|null $deprecationInfo Information about the deprecation
*/
public function __construct(
public string $name,

View File

@@ -31,11 +31,11 @@ use Twig\TwigTest;
final class AsTwigTest
{
/**
* @param non-empty-string $name The name of the test in Twig.
* @param bool|null $needsCharset Whether the test needs the charset passed as the first argument.
* @param bool|null $needsEnvironment Whether the test needs the environment passed as the first argument, or after the charset.
* @param bool|null $needsContext Whether the test needs the context array passed as the first argument, or after the charset and the environment.
* @param DeprecatedCallableInfo|null $deprecationInfo Information about the deprecation
* @param non-empty-string $name The name of the test in Twig
* @param bool|null $needsCharset Whether the test needs the charset passed as the first argument
* @param bool|null $needsEnvironment Whether the test needs the environment passed as the first argument, or after the charset
* @param bool|null $needsContext Whether the test needs the context array passed as the first argument, or after the charset and the environment
* @param DeprecatedCallableInfo|null $deprecationInfo Information about the deprecation
*/
public function __construct(
public string $name,

View File

@@ -43,11 +43,11 @@ use Twig\TokenParser\TokenParserInterface;
*/
class Environment
{
public const VERSION = '3.21.1';
public const VERSION_ID = 32101;
public const VERSION = '3.23.0';
public const VERSION_ID = 32300;
public const MAJOR_VERSION = 3;
public const MINOR_VERSION = 21;
public const RELEASE_VERSION = 1;
public const MINOR_VERSION = 23;
public const RELEASE_VERSION = 0;
public const EXTRA_VERSION = '';
private $charset;
@@ -827,6 +827,14 @@ class Environment
return $this->extensionSet->getTest($name);
}
/**
* @param callable(string): (TwigTest|false) $callable
*/
public function registerUndefinedTestCallback(callable $callable): void
{
$this->extensionSet->registerUndefinedTestCallback($callable);
}
/**
* @return void
*/

View File

@@ -148,6 +148,10 @@ class Error extends \Exception
}
}
if (null === $template) {
return; // Impossible to guess the info as the template was not found in the backtrace
}
$r = new \ReflectionObject($template);
$file = $r->getFileName();
@@ -158,7 +162,7 @@ class Error extends \Exception
while ($e = array_pop($exceptions)) {
$traces = $e->getTrace();
array_unshift($traces, ['file' => $e instanceof Error ? $e->phpFile : $e->getFile(), 'line' => $e instanceof Error ? $e->phpLine : $e->getLine()]);
array_unshift($traces, ['file' => $e instanceof self ? $e->phpFile : $e->getFile(), 'line' => $e instanceof self ? $e->phpLine : $e->getLine()]);
while ($trace = array_shift($traces)) {
if (!isset($trace['file']) || !isset($trace['line']) || $file != $trace['file']) {
continue;

View File

@@ -214,7 +214,7 @@ class ExpressionParser
{
trigger_deprecation('twig/twig', '3.19', \sprintf('The "%s()" method is deprecated, use "Twig\ExpressionParser\Infix\ArgumentsTrait::parseNamedArguments()" instead.', __METHOD__));
$parsePrimary = new \ReflectionMethod($this->parser, 'parsePrimary');
$parsePrimaryExpression = new \ReflectionMethod($this->parser, 'parsePrimaryExpression');
$namedArguments = false;
$definition = false;
@@ -263,7 +263,7 @@ class ExpressionParser
$name = $value->getAttribute('name');
if ($definition) {
$value = $parsePrimary->invoke($this->parser);
$value = $parsePrimaryExpression->invoke($this->parser);
if (!$this->checkConstantExpression($value)) {
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());

View File

@@ -13,6 +13,7 @@ namespace Twig\ExpressionParser\Infix;
use Twig\Error\SyntaxError;
use Twig\Node\Expression\ArrayExpression;
use Twig\Node\Expression\Binary\SetBinary;
use Twig\Node\Expression\Unary\SpreadUnary;
use Twig\Node\Expression\Variable\ContextVariable;
use Twig\Node\Expression\Variable\LocalVariable;
@@ -58,7 +59,10 @@ trait ArgumentsTrait
}
$name = null;
if (($token = $stream->nextIf(Token::OPERATOR_TYPE, '=')) || ($token = $stream->nextIf(Token::PUNCTUATION_TYPE, ':'))) {
if ($value instanceof SetBinary) {
$name = $value->getNode('left')->getAttribute('name');
$value = $value->getNode('right');
} elseif (($token = $stream->nextIf(Token::OPERATOR_TYPE, '=')) || ($token = $stream->nextIf(Token::PUNCTUATION_TYPE, ':'))) {
if (!$value instanceof ContextVariable) {
throw new SyntaxError(\sprintf('A parameter name must be a string, "%s" given.', $value::class), $token->getLine(), $stream->getSourceContext());
}

View File

@@ -0,0 +1,66 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\ExpressionParser\Infix;
use Twig\Error\SyntaxError;
use Twig\ExpressionParser\InfixAssociativity;
use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Expression\ArrayExpression;
use Twig\Node\Expression\Binary\AbstractBinary;
use Twig\Node\Expression\Binary\ObjectDestructuringSetBinary;
use Twig\Node\Expression\Binary\SequenceDestructuringSetBinary;
use Twig\Node\Expression\Binary\SetBinary;
use Twig\Node\Expression\Variable\ContextVariable;
use Twig\Parser;
use Twig\Token;
/**
* @internal
*/
class AssignmentExpressionParser extends BinaryOperatorExpressionParser
{
public function __construct(
string $name,
) {
parent::__construct(SetBinary::class, $name, 0, InfixAssociativity::Right);
}
/**
* @return AbstractBinary
*/
public function parse(Parser $parser, AbstractExpression $left, Token $token): AbstractExpression
{
if (!$left instanceof ContextVariable && !$left instanceof ArrayExpression) {
throw new SyntaxError(\sprintf('Cannot assign to "%s", only variables can be assigned.', $left::class), $token->getLine(), $parser->getStream()->getSourceContext());
}
$right = $parser->parseExpression(InfixAssociativity::Left === $this->getAssociativity() ? $this->getPrecedence() + 1 : $this->getPrecedence());
$right = match ($this->getName()) {
'=' => $right,
default => throw new \LogicException(\sprintf('Unknown operator: %s.', $this->getName())),
};
if ($left instanceof ArrayExpression) {
if ($left->isSequence()) {
return new SequenceDestructuringSetBinary($left, $right, $token->getLine());
} else {
return new ObjectDestructuringSetBinary($left, $right, $token->getLine());
}
} else {
return new SetBinary($left, $right, $token->getLine());
}
}
public function getDescription(): string
{
return 'Assignment operator';
}
}

View File

@@ -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';

View File

@@ -11,12 +11,16 @@
namespace Twig\ExpressionParser;
use Twig\Error\SyntaxError;
use Twig\Node\Expression\AbstractExpression;
use Twig\Parser;
use Twig\Token;
interface InfixExpressionParserInterface extends ExpressionParserInterface
{
/**
* @throws SyntaxError
*/
public function parse(Parser $parser, AbstractExpression $left, Token $token): AbstractExpression;
public function getAssociativity(): InfixAssociativity;

View File

@@ -20,6 +20,7 @@ use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Expression\ArrayExpression;
use Twig\Node\Expression\Binary\ConcatBinary;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\EmptyExpression;
use Twig\Node\Expression\Variable\ContextVariable;
use Twig\Parser;
use Twig\Token;
@@ -100,10 +101,6 @@ final class LiteralExpressionParser extends AbstractExpressionParser implements
return new ContextVariable($token->getValue(), $token->getLine());
}
if ('=' === $token->getValue() && ('==' === $stream->look(-1)->getValue() || '!=' === $stream->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(), $stream->getSourceContext());
}
// no break
default:
throw new SyntaxError(\sprintf('Unexpected token "%s" of value "%s".', $token->toEnglish(), $token->getValue()), $token->getLine(), $stream->getSourceContext());
@@ -174,7 +171,12 @@ final class LiteralExpressionParser extends AbstractExpressionParser implements
}
$first = false;
$node->addElement($parser->parseExpression());
// Check for empty slots (comma with no expression)
if ($stream->test(Token::PUNCTUATION_TYPE, ',')) {
$node->addElement(new EmptyExpression($stream->getCurrent()->getLine()));
} else {
$node->addElement($parser->parseExpression());
}
}
$stream->expect(Token::PUNCTUATION_TYPE, ']', 'An opened sequence is not properly closed');

View File

@@ -33,6 +33,7 @@ final class UnaryOperatorExpressionParser extends AbstractExpressionParser imple
private ?PrecedenceChange $precedenceChange = null,
private ?string $description = null,
private array $aliases = [],
private ?int $operandPrecedence = null,
) {
}
@@ -41,7 +42,7 @@ final class UnaryOperatorExpressionParser extends AbstractExpressionParser imple
*/
public function parse(Parser $parser, Token $token): AbstractExpression
{
return new ($this->nodeClass)($parser->parseExpression($this->precedence), $token->getLine());
return new ($this->nodeClass)($parser->parseExpression($this->operandPrecedence ?? $this->precedence), $token->getLine());
}
public function getName(): string

View File

@@ -11,11 +11,15 @@
namespace Twig\ExpressionParser;
use Twig\Error\SyntaxError;
use Twig\Node\Expression\AbstractExpression;
use Twig\Parser;
use Twig\Token;
interface PrefixExpressionParserInterface extends ExpressionParserInterface
{
/**
* @throws SyntaxError
*/
public function parse(Parser $parser, Token $token): AbstractExpression;
}

View File

@@ -104,7 +104,7 @@ final class AttributeExtension extends AbstractExtension
]);
if ($callable->getMinimalNumberOfRequiredArguments() > $method->getNumberOfParameters()) {
throw new \LogicException(sprintf('"%s::%s()" needs at least %d arguments to be used AsTwigFilter, but only %d defined.', $reflectionClass->getName(), $method->getName(), $callable->getMinimalNumberOfRequiredArguments(), $method->getNumberOfParameters()));
throw new \LogicException(\sprintf('"%s::%s()" needs at least %d arguments to be used AsTwigFilter, but only %d defined.', $reflectionClass->getName(), $method->getName(), $callable->getMinimalNumberOfRequiredArguments(), $method->getNumberOfParameters()));
}
$filters[$attribute->name] = $callable;
@@ -125,14 +125,13 @@ final class AttributeExtension extends AbstractExtension
]);
if ($callable->getMinimalNumberOfRequiredArguments() > $method->getNumberOfParameters()) {
throw new \LogicException(sprintf('"%s::%s()" needs at least %d arguments to be used AsTwigFunction, but only %d defined.', $reflectionClass->getName(), $method->getName(), $callable->getMinimalNumberOfRequiredArguments(), $method->getNumberOfParameters()));
throw new \LogicException(\sprintf('"%s::%s()" needs at least %d arguments to be used AsTwigFunction, but only %d defined.', $reflectionClass->getName(), $method->getName(), $callable->getMinimalNumberOfRequiredArguments(), $method->getNumberOfParameters()));
}
$functions[$attribute->name] = $callable;
}
foreach ($method->getAttributes(AsTwigTest::class) as $reflectionAttribute) {
/** @var AsTwigTest $attribute */
$attribute = $reflectionAttribute->newInstance();
@@ -145,7 +144,7 @@ final class AttributeExtension extends AbstractExtension
]);
if ($callable->getMinimalNumberOfRequiredArguments() > $method->getNumberOfParameters()) {
throw new \LogicException(sprintf('"%s::%s()" needs at least %d arguments to be used AsTwigTest, but only %d defined.', $reflectionClass->getName(), $method->getName(), $callable->getMinimalNumberOfRequiredArguments(), $method->getNumberOfParameters()));
throw new \LogicException(\sprintf('"%s::%s()" needs at least %d arguments to be used AsTwigTest, but only %d defined.', $reflectionClass->getName(), $method->getName(), $callable->getMinimalNumberOfRequiredArguments(), $method->getNumberOfParameters()));
}
$tests[$attribute->name] = $callable;

View File

@@ -17,6 +17,7 @@ use Twig\Error\LoaderError;
use Twig\Error\RuntimeError;
use Twig\Error\SyntaxError;
use Twig\ExpressionParser\Infix\ArrowExpressionParser;
use Twig\ExpressionParser\Infix\AssignmentExpressionParser;
use Twig\ExpressionParser\Infix\BinaryOperatorExpressionParser;
use Twig\ExpressionParser\Infix\ConditionalTernaryExpressionParser;
use Twig\ExpressionParser\Infix\DotExpressionParser;
@@ -55,10 +56,12 @@ use Twig\Node\Expression\Binary\ModBinary;
use Twig\Node\Expression\Binary\MulBinary;
use Twig\Node\Expression\Binary\NotEqualBinary;
use Twig\Node\Expression\Binary\NotInBinary;
use Twig\Node\Expression\Binary\NotSameAsBinary;
use Twig\Node\Expression\Binary\NullCoalesceBinary;
use Twig\Node\Expression\Binary\OrBinary;
use Twig\Node\Expression\Binary\PowerBinary;
use Twig\Node\Expression\Binary\RangeBinary;
use Twig\Node\Expression\Binary\SameAsBinary;
use Twig\Node\Expression\Binary\SpaceshipBinary;
use Twig\Node\Expression\Binary\StartsWithBinary;
use Twig\Node\Expression\Binary\SubBinary;
@@ -333,7 +336,7 @@ final class CoreExtension extends AbstractExtension
return [
// unary operators
new UnaryOperatorExpressionParser(NotUnary::class, 'not', 50, new PrecedenceChange('twig/twig', '3.15', 70)),
new UnaryOperatorExpressionParser(SpreadUnary::class, '...', 512, description: 'Spread operator'),
new UnaryOperatorExpressionParser(SpreadUnary::class, '...', 512, description: 'Spread operator', operandPrecedence: 0),
new UnaryOperatorExpressionParser(NegUnary::class, '-', 500),
new UnaryOperatorExpressionParser(PosUnary::class, '+', 500),
@@ -360,6 +363,8 @@ final class CoreExtension extends AbstractExtension
new BinaryOperatorExpressionParser(EndsWithBinary::class, 'ends with', 20),
new BinaryOperatorExpressionParser(HasSomeBinary::class, 'has some', 20),
new BinaryOperatorExpressionParser(HasEveryBinary::class, 'has every', 20),
new BinaryOperatorExpressionParser(SameAsBinary::class, '===', 20),
new BinaryOperatorExpressionParser(NotSameAsBinary::class, '!==', 20),
new BinaryOperatorExpressionParser(RangeBinary::class, '..', 25),
new BinaryOperatorExpressionParser(AddBinary::class, '+', 30),
new BinaryOperatorExpressionParser(SubBinary::class, '-', 30),
@@ -373,6 +378,9 @@ final class CoreExtension extends AbstractExtension
// ternary operator
new ConditionalTernaryExpressionParser(),
// assignment operator
new AssignmentExpressionParser('='),
// Twig callables
new IsExpressionParser(),
new IsNotExpressionParser(),
@@ -417,10 +425,8 @@ final class CoreExtension extends AbstractExtension
trigger_deprecation('twig/twig', '3.12', 'Passing a non-countable sequence of values to "%s()" is deprecated.', __METHOD__);
return $values;
$values = self::toArray($values, false);
}
$values = self::toArray($values, false);
}
if (!$count = \count($values)) {
@@ -1694,7 +1700,7 @@ final class CoreExtension extends AbstractExtension
}
if (match (true) {
\is_array($object) => \array_key_exists($arrayItem, $object),
\is_array($object) => \array_key_exists($arrayItem = (string) $arrayItem, $object),
$object instanceof \ArrayAccess => $object->offsetExists($arrayItem),
default => false,
}) {
@@ -1715,9 +1721,13 @@ final class CoreExtension extends AbstractExtension
}
if ($object instanceof \ArrayAccess) {
$message = \sprintf('Key "%s" in object with ArrayAccess of class "%s" does not exist.', $arrayItem, $object::class);
if (\is_object($arrayItem) || \is_array($arrayItem)) {
$message = \sprintf('Key of type "%s" does not exist in ArrayAccess-able object of class "%s".', get_debug_type($arrayItem), get_debug_type($object));
} else {
$message = \sprintf('Key "%s" does not exist in ArrayAccess-able object of class "%s".', $arrayItem, get_debug_type($object));
}
} elseif (\is_object($object)) {
$message = \sprintf('Impossible to access a key "%s" on an object of class "%s" that does not implement ArrayAccess interface.', $item, $object::class);
$message = \sprintf('Impossible to access a key "%s" on an object of class "%s" that does not implement ArrayAccess interface.', $item, get_debug_type($object));
} elseif (\is_array($object)) {
if (!$object) {
$message = \sprintf('Key "%s" does not exist as the sequence/mapping is empty.', $arrayItem);
@@ -1880,7 +1890,7 @@ final class CoreExtension extends AbstractExtension
return;
}
throw new RuntimeError(\sprintf('Neither the property "%1$s" nor one of the methods "%1$s()", "get%1$s()"/"is%1$s()"/"has%1$s()" or "__call()" exist and have public access in class "%2$s".', $item, $class), $lineno, $source);
throw new RuntimeError(\sprintf('Neither the property "%1$s" nor one of the methods "%1$s()", "get%1$s()", "is%1$s()", "has%1$s()" or "__call()" exist and have public access in class "%2$s".', $item, $class), $lineno, $source);
}
if ($sandboxed) {

View File

@@ -11,8 +11,11 @@
namespace Twig\Extension;
use Twig\ExpressionParser;
use Twig\ExpressionParser\ExpressionParserInterface;
use Twig\ExpressionParser\PrecedenceChange;
use Twig\Node\Expression\Binary\AbstractBinary;
use Twig\Node\Expression\Unary\AbstractUnary;
use Twig\NodeVisitor\NodeVisitorInterface;
use Twig\TokenParser\TokenParserInterface;
use Twig\TwigFilter;

View File

@@ -27,6 +27,10 @@ use Twig\Node\Expression\AbstractExpression;
use Twig\NodeVisitor\NodeVisitorInterface;
use Twig\TokenParser\TokenParserInterface;
// Help opcache.preload discover always-needed symbols
// @see https://github.com/php/php-src/issues/10131
class_exists(BinaryOperatorExpressionParser::class);
/**
* @author Fabien Potencier <fabien@symfony.com>
*
@@ -59,6 +63,8 @@ final class ExtensionSet
private $functionCallbacks = [];
/** @var array<callable(string): (TwigFilter|false)> */
private $filterCallbacks = [];
/** @var array<callable(string): (TwigTest|false)> */
private $testCallbacks = [];
/** @var array<callable(string): (TokenParserInterface|false)> */
private $parserCallbacks = [];
private $lastModified = 0;
@@ -410,9 +416,23 @@ final class ExtensionSet
}
}
foreach ($this->testCallbacks as $callback) {
if (false !== $test = $callback($name)) {
return $test;
}
}
return null;
}
/**
* @param callable(string): (TwigTest|false) $callable
*/
public function registerUndefinedTestCallback(callable $callable): void
{
$this->testCallbacks[] = $callable;
}
public function getExpressionParsers(): ExpressionParsers
{
if (!$this->initialized) {

View File

@@ -525,7 +525,7 @@ class Lexer
private function getOperatorRegex(): string
{
$expressionParsers = ['='];
$expressionParsers = [];
foreach ($this->env->getExpressionParsers() as $expressionParser) {
$expressionParsers = array_merge($expressionParsers, [$expressionParser->getName()], $expressionParser->getAliases());
}

View File

@@ -41,7 +41,7 @@ class EmbedNode extends IncludeNode
->raw(', ')
->repr($this->getTemplateLine())
->raw(', ')
->string($this->getAttribute('index'))
->repr($this->getAttribute('index'))
->raw(')')
;
if ($this->getAttribute('ignore_missing')) {

View File

@@ -12,6 +12,7 @@
namespace Twig\Node\Expression;
use Twig\Compiler;
use Twig\Error\SyntaxError;
use Twig\Node\Expression\Unary\SpreadUnary;
use Twig\Node\Expression\Unary\StringCastUnary;
use Twig\Node\Expression\Variable\ContextVariable;
@@ -60,6 +61,31 @@ class ArrayExpression extends AbstractExpression implements SupportDefinedTestIn
return false;
}
/**
* Checks if the array is a sequence (keys are sequential integers starting from 0).
*
* @internal
*/
public function isSequence(): bool
{
foreach ($this->getKeyValuePairs() as $i => $pair) {
$key = $pair['key'];
if ($key instanceof TempNameExpression) {
$keyValue = $key->getAttribute('name');
} elseif ($key instanceof ConstantExpression) {
$keyValue = $key->getAttribute('value');
} else {
return false;
}
if ($keyValue !== $i) {
return false;
}
}
return true;
}
public function addElement(AbstractExpression $value, ?AbstractExpression $key = null): void
{
if (null === $key) {
@@ -77,6 +103,13 @@ class ArrayExpression extends AbstractExpression implements SupportDefinedTestIn
return;
}
// Check for empty expressions which are only allowed in destructuring
foreach ($this->getKeyValuePairs() as $pair) {
if ($pair['value'] instanceof EmptyExpression) {
throw new SyntaxError('Empty array elements are only allowed in destructuring assignments.', $pair['value']->getTemplateLine(), $this->getSourceContext());
}
}
$compiler->raw('[');
$isSequence = true;
foreach ($this->getKeyValuePairs() as $i => $pair) {

View File

@@ -13,8 +13,8 @@ namespace Twig\Node\Expression\Binary;
use Twig\Compiler;
use Twig\Error\SyntaxError;
use Twig\Node\Expression\ReturnBoolInterface;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\ReturnBoolInterface;
use Twig\Node\Node;
class MatchesBinary extends AbstractBinary implements ReturnBoolInterface

View File

@@ -0,0 +1,23 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\Node\Expression\Binary;
use Twig\Compiler;
use Twig\Node\Expression\ReturnBoolInterface;
class NotSameAsBinary extends AbstractBinary implements ReturnBoolInterface
{
public function operator(Compiler $compiler): Compiler
{
return $compiler->raw('!==');
}
}

View File

@@ -0,0 +1,71 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\Node\Expression\Binary;
use Twig\Compiler;
use Twig\Error\SyntaxError;
use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Expression\ArrayExpression;
use Twig\Node\Expression\Variable\ContextVariable;
use Twig\Node\Node;
/**
* @internal
*/
class ObjectDestructuringSetBinary extends AbstractBinary
{
private array $properties = [];
/**
* @param ArrayExpression $left The array expression containing object/mapping destructuring properties
* @param AbstractExpression $right The expression providing values for assignment
*/
public function __construct(Node $left, Node $right, int $lineno)
{
if (!$left instanceof ArrayExpression) {
throw new \LogicException('Left side must be ArrayExpression for object/mapping destructuring.');
}
foreach ($left->getKeyValuePairs() as $pair) {
if (!$pair['value'] instanceof ContextVariable) {
throw new SyntaxError(\sprintf('Cannot assign to "%s", only variables can be assigned in object/mapping destructuring.', $pair['value']::class), $lineno);
}
$this->properties[] = $pair['value']->getAttribute('name');
}
parent::__construct($left, $right, $lineno);
}
public function compile(Compiler $compiler): void
{
$compiler->addDebugInfo($this);
$compiler->raw('[');
foreach ($this->properties as $i => $property) {
if ($i) {
$compiler->raw(', ');
}
$compiler->raw('$context[')->repr($property)->raw(']');
}
$compiler->raw('] = [');
foreach ($this->properties as $i => $property) {
if ($i) {
$compiler->raw(', ');
}
$compiler->raw('CoreExtension::getAttribute($this->env, $this->source, ')->subcompile($this->getNode('right'))->raw(', ')->repr($property)->raw(', [], \\Twig\\Template::ANY_CALL, false, false, false, ')->repr($this->getNode('right')->getTemplateLine())->raw(')');
}
$compiler->raw(']');
}
public function operator(Compiler $compiler): Compiler
{
return $compiler->raw('=');
}
}

View File

@@ -0,0 +1,23 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\Node\Expression\Binary;
use Twig\Compiler;
use Twig\Node\Expression\ReturnBoolInterface;
class SameAsBinary extends AbstractBinary implements ReturnBoolInterface
{
public function operator(Compiler $compiler): Compiler
{
return $compiler->raw('===');
}
}

View File

@@ -0,0 +1,67 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\Node\Expression\Binary;
use Twig\Compiler;
use Twig\Error\SyntaxError;
use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Expression\ArrayExpression;
use Twig\Node\Expression\EmptyExpression;
use Twig\Node\Expression\Variable\ContextVariable;
use Twig\Node\Node;
/**
* @internal
*/
class SequenceDestructuringSetBinary extends AbstractBinary
{
private array $variables = [];
/**
* @param ArrayExpression $left The array expression containing variables to assign to
* @param AbstractExpression $right The expression providing values for assignment
*/
public function __construct(Node $left, Node $right, int $lineno)
{
foreach ($left->getKeyValuePairs() as $pair) {
if ($pair['value'] instanceof EmptyExpression) {
$this->variables[] = null;
} elseif ($pair['value'] instanceof ContextVariable) {
$this->variables[] = $pair['value']->getAttribute('name');
} else {
throw new SyntaxError(\sprintf('Cannot assign to "%s", only variables can be assigned in sequence destructuring.', $pair['value']::class), $lineno);
}
}
parent::__construct($left, $right, $lineno);
}
public function compile(Compiler $compiler): void
{
$compiler->addDebugInfo($this);
$compiler->raw('[');
foreach ($this->variables as $i => $name) {
if ($i) {
$compiler->raw(', ');
}
if (null !== $name) {
$compiler->raw('$context[')->repr($name)->raw(']');
}
}
$compiler->raw('] = array_pad(')->subcompile($this->getNode('right'))->raw(', ')->repr(\count($this->variables))->raw(', null)');
}
public function operator(Compiler $compiler): Compiler
{
return $compiler->raw('=');
}
}

View File

@@ -0,0 +1,44 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\Node\Expression\Binary;
use Twig\Compiler;
use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Expression\Variable\AssignContextVariable;
use Twig\Node\Expression\Variable\ContextVariable;
use Twig\Node\Node;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class SetBinary extends AbstractBinary
{
/**
* @param ContextVariable $left
* @param AbstractExpression $right
*/
public function __construct(Node $left, Node $right, int $lineno)
{
$name = $left->getAttribute('name');
if (!\is_string($name)) {
throw new \LogicException('The "name" attribute must be a string.');
}
$left = new AssignContextVariable($name, $left->getTemplateLine());
parent::__construct($left, $right, $lineno);
}
public function operator(Compiler $compiler): Compiler
{
return $compiler->raw('=');
}
}

View File

@@ -0,0 +1,33 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\Node\Expression;
use Twig\Compiler;
/**
* Represents an empty slot in an array.
*
* This is currently only used in destructuring contexts.
*
* @internal
*/
final class EmptyExpression extends AbstractExpression
{
public function __construct(int $lineno)
{
parent::__construct([], [], $lineno);
}
public function compile(Compiler $compiler): void
{
}
}

View File

@@ -1,5 +1,14 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\Node\Expression\FunctionNode;
use Twig\Compiler;

View File

@@ -1,5 +1,14 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\Node\Expression\FunctionNode;
use Twig\Compiler;

View File

@@ -25,7 +25,7 @@ class GetAttrExpression extends AbstractExpression implements SupportDefinedTest
/**
* @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];
if (null !== $arguments) {
@@ -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' => true], $lineno);
parent::__construct($nodes, ['type' => $type, 'ignore_strict_check' => false, 'optimizable' => !$nullSafe, 'null_safe' => $nullSafe], $lineno);
}
public function enableDefinedTest(): void
@@ -49,6 +49,8 @@ class GetAttrExpression extends AbstractExpression implements SupportDefinedTest
{
$env = $compiler->getEnvironment();
$arrayAccessSandbox = false;
$nullSafe = $this->getAttribute('null_safe');
$objectVar = null;
// optimize array calls
if (
@@ -93,14 +95,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'))
;
@@ -123,14 +138,18 @@ class GetAttrExpression extends AbstractExpression implements SupportDefinedTest
if ($arrayAccessSandbox) {
$compiler->raw(')');
}
if ($nullSafe) {
$compiler->raw(')');
}
}
private function changeIgnoreStrictCheck(GetAttrExpression $node): void
private function changeIgnoreStrictCheck(self $node): void
{
$node->setAttribute('optimizable', false);
$node->setAttribute('ignore_strict_check', true);
if ($node->getNode('node') instanceof GetAttrExpression) {
if ($node->getNode('node') instanceof self) {
$this->changeIgnoreStrictCheck($node->getNode('node'));
}
}

View File

@@ -15,16 +15,8 @@ use Twig\Attribute\FirstClassTwigCallableReady;
use Twig\Compiler;
use Twig\Error\SyntaxError;
use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Expression\ArrayExpression;
use Twig\Node\Expression\BlockReferenceExpression;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\FunctionExpression;
use Twig\Node\Expression\GetAttrExpression;
use Twig\Node\Expression\MacroReferenceExpression;
use Twig\Node\Expression\MethodCallExpression;
use Twig\Node\Expression\SupportDefinedTestInterface;
use Twig\Node\Expression\TestExpression;
use Twig\Node\Expression\Variable\ContextVariable;
use Twig\Node\Node;
use Twig\TwigTest;

View File

@@ -1,5 +1,14 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\Node;
use Twig\Attribute\YieldReady;

View File

@@ -76,6 +76,9 @@ class Parser
return \sprintf('__internal_parse_%d', $this->varNameSalt++);
}
/**
* @throws SyntaxError
*/
public function parse(TokenStream $stream, $test = null, bool $dropNeedle = false): ModuleNode
{
$vars = get_object_vars($this);
@@ -158,6 +161,9 @@ class Parser
}
}
/**
* @throws SyntaxError
*/
public function subparse($test, bool $dropNeedle = false): Node
{
$lineno = $this->getCurrentToken()->getLine();
@@ -494,11 +500,26 @@ class Parser
// try 2-words tests
$name = $name.' '.$this->getCurrentToken()->getValue();
if ($test = $this->env->getTest($name)) {
$this->stream->next();
try {
$test = $this->env->getTest($name);
} catch (SyntaxError $e) {
if (!$this->shouldIgnoreUnknownTwigCallables()) {
throw $e;
}
$test = null;
}
$this->stream->next();
} else {
$test = $this->env->getTest($name);
try {
$test = $this->env->getTest($name);
} catch (SyntaxError $e) {
if (!$this->shouldIgnoreUnknownTwigCallables()) {
throw $e;
}
$test = null;
}
}
if (!$test) {

View File

@@ -1,9 +1,9 @@
<?php
/*
* This file is part of the Symfony package.
* This file is part of Twig.
*
* (c) Fabien Potencier <fabien@symfony.com>
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.

View File

@@ -1,9 +1,9 @@
<?php
/*
* This file is part of the Symfony package.
* This file is part of Twig.
*
* (c) Fabien Potencier <fabien@symfony.com>
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.

View File

@@ -17,7 +17,7 @@ use Twig\Markup;
final class EscaperRuntime implements RuntimeExtensionInterface
{
/** @var array<string, callable(string $string, string $charset): string> */
/** @var array<string, callable(string, string): string> */
private $escapers = [];
/** @internal */
@@ -140,6 +140,10 @@ final class EscaperRuntime implements RuntimeExtensionInterface
case 'html':
// see https://www.php.net/htmlspecialchars
if ('UTF-8' === $charset) {
return htmlspecialchars($string, \ENT_QUOTES | \ENT_SUBSTITUTE, 'UTF-8');
}
// Using a static variable to avoid initializing the array
// each time the function is called. Moving the declaration on the
// top of the function slow downs other escaping strategies.
@@ -195,7 +199,7 @@ final class EscaperRuntime implements RuntimeExtensionInterface
* Escape sequences supported only by JavaScript, not JSON, are omitted.
* \" is also supported but omitted, because the resulting string is not HTML safe.
*/
static $shortMap = [
$short = match ($char) {
'\\' => '\\\\',
'/' => '\\/',
"\x08" => '\b',
@@ -203,10 +207,11 @@ final class EscaperRuntime implements RuntimeExtensionInterface
"\x0A" => '\n',
"\x0D" => '\r',
"\x09" => '\t',
];
default => false,
};
if (isset($shortMap[$char])) {
return $shortMap[$char];
if ($short) {
return $short;
}
$codepoint = mb_ord($char, 'UTF-8');
@@ -267,7 +272,7 @@ final class EscaperRuntime implements RuntimeExtensionInterface
* @license https://framework.zend.com/license/new-bsd New BSD License
*/
$chr = $matches[0];
$ord = \ord($chr);
$ord = \ord($chr[0]);
/*
* The following replaces characters undefined in HTML with the
@@ -288,18 +293,13 @@ final class EscaperRuntime implements RuntimeExtensionInterface
* entities that XML supports. Using HTML entities would result in this error:
* XML Parsing Error: undefined entity
*/
static $entityMap = [
return match ($ord) {
34 => '&quot;', /* quotation mark */
38 => '&amp;', /* ampersand */
60 => '&lt;', /* less-than sign */
62 => '&gt;', /* greater-than sign */
];
if (isset($entityMap[$ord])) {
return $entityMap[$ord];
}
return \sprintf('&#x%02X;', $ord);
default => \sprintf('&#x%02X;', $ord),
};
}
/*

View File

@@ -270,7 +270,7 @@ abstract class Template
/**
* @param string|TemplateWrapper|array<string|TemplateWrapper> $template
*/
protected function load(string|TemplateWrapper|array $template, int $line, int|null $index = null): self
protected function load(string|TemplateWrapper|array $template, int $line, ?int $index = null): self
{
try {
if (\is_array($template)) {
@@ -315,7 +315,7 @@ abstract class Template
*
* @deprecated since Twig 3.21 and will be removed in 4.0. Use Template::load() instead.
*/
protected function loadTemplate($template, $templateName = null, int|null $line = null, int|null $index = null): self|TemplateWrapper
protected function loadTemplate($template, $templateName = null, ?int $line = null, ?int $index = null): self|TemplateWrapper
{
trigger_deprecation('twig/twig', '3.21', 'The "%s" method is deprecated.', __METHOD__);

View File

@@ -32,9 +32,15 @@ final class GuardTokenParser extends AbstractTokenParser
$method = 'get'.$typeToken->getValue();
$nameToken = $stream->expect(Token::NAME_TYPE);
$name = $nameToken->getValue();
if ('test' === $typeToken->getValue() && $stream->test(Token::NAME_TYPE)) {
// try 2-words tests
$name .= ' '.$stream->getCurrent()->getValue();
$stream->next();
}
try {
$exists = null !== $this->parser->getEnvironment()->$method($nameToken->getValue());
$exists = null !== $this->parser->getEnvironment()->$method($name);
} catch (SyntaxError) {
$exists = false;
}