mirror of
https://github.com/Combodo/iTop.git
synced 2026-05-19 07:12:26 +02:00
N°9319 increase php min. version to 8.2 (#887)
* Update minimum PHP version to 8.2 * Fix previous wrong resolution of merge conflict
This commit is contained in:
@@ -1,3 +1,13 @@
|
||||
# 3.24.0 (2026-03-17)
|
||||
|
||||
* Deprecate not implementing the `getOperatorTokens()` method in `ExpressionParserInterface` implementations
|
||||
* Deprecate passing a non-`AbstractExpression` node to `Twig\Node\Expression\Binary\MatchesBinary` constructor
|
||||
* Deprecate passing a non-`AbstractExpression` node to `Parser::setParent()`
|
||||
* Add support for renaming variables in object destructuring (`{name: userName} = user`)
|
||||
* Add `html_attr_relaxed` escaping strategy that preserves :, @, [, and ] for front-end framework attribute names
|
||||
* Add support for short-circuiting in null-safe operator chains
|
||||
* Add the `html_attr` function and `html_attr_merge` as well as `html_attr_type` filters
|
||||
|
||||
# 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)
|
||||
|
||||
@@ -32,7 +32,8 @@
|
||||
"require-dev": {
|
||||
"symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0",
|
||||
"psr/container": "^1.0|^2.0",
|
||||
"phpstan/phpstan": "^2.0"
|
||||
"phpstan/phpstan": "^2.0@stable",
|
||||
"php-cs-fixer/shim": "^3.0@stable"
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -43,10 +43,10 @@ use Twig\TokenParser\TokenParserInterface;
|
||||
*/
|
||||
class Environment
|
||||
{
|
||||
public const VERSION = '3.23.0';
|
||||
public const VERSION_ID = 32300;
|
||||
public const VERSION = '3.24.0';
|
||||
public const VERSION_ID = 32400;
|
||||
public const MAJOR_VERSION = 3;
|
||||
public const MINOR_VERSION = 23;
|
||||
public const MINOR_VERSION = 24;
|
||||
public const RELEASE_VERSION = 0;
|
||||
public const EXTRA_VERSION = '';
|
||||
|
||||
|
||||
@@ -27,4 +27,9 @@ abstract class AbstractExpressionParser implements ExpressionParserInterface
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getOperatorTokens(): array
|
||||
{
|
||||
return [$this->getName(), ...$this->getAliases()];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,14 @@
|
||||
|
||||
namespace Twig\ExpressionParser;
|
||||
|
||||
/**
|
||||
* @method list<string> getOperatorTokens() Returns the operator token strings that this expression parser handles.
|
||||
* These are the strings that should be recognized as operator tokens by the Lexer,
|
||||
* and used to look up the parser in the registry.
|
||||
* For most parsers, this returns the name and aliases. Parsers that don't handle
|
||||
* operator tokens (like LiteralExpressionParser) should return an empty array.
|
||||
* This method will be added to the interface in Twig 4.0.
|
||||
*/
|
||||
interface ExpressionParserInterface
|
||||
{
|
||||
public function __toString(): string;
|
||||
|
||||
@@ -54,10 +54,9 @@ final class ExpressionParsers implements \IteratorAggregate
|
||||
// throw new \InvalidArgumentException(\sprintf('Precedence for "%s" must be between 0 and 512, got %d.', $parser->getName(), $parser->getPrecedence()));
|
||||
}
|
||||
$interface = $parser instanceof PrefixExpressionParserInterface ? PrefixExpressionParserInterface::class : InfixExpressionParserInterface::class;
|
||||
$this->parsersByName[$interface][$parser->getName()] = $parser;
|
||||
$this->parsersByClass[$parser::class] = $parser;
|
||||
foreach ($parser->getAliases() as $alias) {
|
||||
$this->parsersByName[$interface][$alias] = $parser;
|
||||
foreach (self::getOperatorTokensFor($parser) as $token) {
|
||||
$this->parsersByName[$interface][$token] = $parser;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,9 +89,22 @@ final class ExpressionParsers implements \IteratorAggregate
|
||||
|
||||
public function getIterator(): \Traversable
|
||||
{
|
||||
$seen = [];
|
||||
foreach ($this->parsersByName as $parsers) {
|
||||
// we don't yield the keys
|
||||
yield from $parsers;
|
||||
foreach ($parsers as $parser) {
|
||||
$id = spl_object_id($parser);
|
||||
if (!isset($seen[$id])) {
|
||||
$seen[$id] = true;
|
||||
yield $parser;
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($this->parsersByClass as $parser) {
|
||||
$id = spl_object_id($parser);
|
||||
if (!isset($seen[$id])) {
|
||||
$seen[$id] = true;
|
||||
yield $parser;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,4 +136,20 @@ final class ExpressionParsers implements \IteratorAggregate
|
||||
|
||||
return $this->precedenceChanges;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
public static function getOperatorTokensFor(ExpressionParserInterface $parser): array
|
||||
{
|
||||
if (method_exists($parser, 'getOperatorTokens')) {
|
||||
return $parser->getOperatorTokens();
|
||||
}
|
||||
|
||||
trigger_deprecation('twig/twig', '3.24', 'Not implementing the "getOperatorTokens()" method in "%s" is deprecated. This method will be part of the "%s" interface in 4.0.', $parser::class, ExpressionParserInterface::class);
|
||||
|
||||
return [$parser->getName(), ...$parser->getAliases()];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,12 +51,12 @@ class AssignmentExpressionParser extends BinaryOperatorExpressionParser
|
||||
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());
|
||||
|
||||
return new ObjectDestructuringSetBinary($left, $right, $token->getLine());
|
||||
}
|
||||
|
||||
return new SetBinary($left, $right, $token->getLine());
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
|
||||
@@ -17,6 +17,7 @@ use Twig\ExpressionParser\ExpressionParserDescriptionInterface;
|
||||
use Twig\ExpressionParser\PrefixExpressionParserInterface;
|
||||
use Twig\Node\Expression\AbstractExpression;
|
||||
use Twig\Node\Expression\ListExpression;
|
||||
use Twig\Node\Expression\Variable\AssignContextVariable;
|
||||
use Twig\Node\Expression\Variable\ContextVariable;
|
||||
use Twig\Parser;
|
||||
use Twig\Token;
|
||||
@@ -36,7 +37,7 @@ final class GroupingExpressionParser extends AbstractExpressionParser implements
|
||||
return $expr->setExplicitParentheses();
|
||||
}
|
||||
|
||||
return new ListExpression([$expr], $token->getLine());
|
||||
return new ListExpression([self::toAssignContextVariable($expr)], $token->getLine());
|
||||
}
|
||||
|
||||
// determine if we are parsing an arrow function arguments
|
||||
@@ -58,7 +59,16 @@ final class GroupingExpressionParser extends AbstractExpressionParser implements
|
||||
throw new SyntaxError('A list of variables must be followed by an arrow.', $stream->getCurrent()->getLine(), $stream->getSourceContext());
|
||||
}
|
||||
|
||||
return new ListExpression($names, $token->getLine());
|
||||
return new ListExpression(array_map(self::toAssignContextVariable(...), $names), $token->getLine());
|
||||
}
|
||||
|
||||
private static function toAssignContextVariable(AbstractExpression $expr): AssignContextVariable
|
||||
{
|
||||
if (!$expr instanceof ContextVariable) {
|
||||
throw new SyntaxError('A list must only contain variables.', $expr->getTemplateLine(), $expr->getSourceContext());
|
||||
}
|
||||
|
||||
return $expr instanceof AssignContextVariable ? $expr : new AssignContextVariable($expr->getAttribute('name'), $expr->getTemplateLine());
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
|
||||
@@ -30,8 +30,6 @@ use Twig\Token;
|
||||
*/
|
||||
final class LiteralExpressionParser extends AbstractExpressionParser implements PrefixExpressionParserInterface, ExpressionParserDescriptionInterface
|
||||
{
|
||||
private string $type = 'literal';
|
||||
|
||||
public function parse(Parser $parser, Token $token): AbstractExpression
|
||||
{
|
||||
$stream = $parser->getStream();
|
||||
@@ -41,41 +39,30 @@ final class LiteralExpressionParser extends AbstractExpressionParser implements
|
||||
switch ($token->getValue()) {
|
||||
case 'true':
|
||||
case 'TRUE':
|
||||
$this->type = 'constant';
|
||||
|
||||
return new ConstantExpression(true, $token->getLine());
|
||||
|
||||
case 'false':
|
||||
case 'FALSE':
|
||||
$this->type = 'constant';
|
||||
|
||||
return new ConstantExpression(false, $token->getLine());
|
||||
|
||||
case 'none':
|
||||
case 'NONE':
|
||||
case 'null':
|
||||
case 'NULL':
|
||||
$this->type = 'constant';
|
||||
|
||||
return new ConstantExpression(null, $token->getLine());
|
||||
|
||||
default:
|
||||
$this->type = 'variable';
|
||||
|
||||
return new ContextVariable($token->getValue(), $token->getLine());
|
||||
}
|
||||
|
||||
// no break
|
||||
case $token->test(Token::NUMBER_TYPE):
|
||||
$stream->next();
|
||||
$this->type = 'constant';
|
||||
|
||||
return new ConstantExpression($token->getValue(), $token->getLine());
|
||||
|
||||
case $token->test(Token::STRING_TYPE):
|
||||
case $token->test(Token::INTERPOLATION_START_TYPE):
|
||||
$this->type = 'string';
|
||||
|
||||
return $this->parseStringExpression($parser);
|
||||
|
||||
case $token->test(Token::PUNCTUATION_TYPE):
|
||||
@@ -96,7 +83,6 @@ final class LiteralExpressionParser extends AbstractExpressionParser implements
|
||||
if (preg_match(Lexer::REGEX_NAME, $token->getValue(), $matches) && $matches[0] == $token->getValue()) {
|
||||
// in this context, string operators are variable names
|
||||
$stream->next();
|
||||
$this->type = 'variable';
|
||||
|
||||
return new ContextVariable($token->getValue(), $token->getLine());
|
||||
}
|
||||
@@ -109,7 +95,12 @@ final class LiteralExpressionParser extends AbstractExpressionParser implements
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->type;
|
||||
return 'literal';
|
||||
}
|
||||
|
||||
public function getOperatorTokens(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
@@ -153,8 +144,6 @@ final class LiteralExpressionParser extends AbstractExpressionParser implements
|
||||
|
||||
private function parseSequenceExpression(Parser $parser)
|
||||
{
|
||||
$this->type = 'sequence';
|
||||
|
||||
$stream = $parser->getStream();
|
||||
$stream->expect(Token::OPERATOR_TYPE, '[', 'A sequence element was expected');
|
||||
|
||||
@@ -185,8 +174,6 @@ final class LiteralExpressionParser extends AbstractExpressionParser implements
|
||||
|
||||
private function parseMappingExpression(Parser $parser)
|
||||
{
|
||||
$this->type = 'mapping';
|
||||
|
||||
$stream = $parser->getStream();
|
||||
$stream->expect(Token::PUNCTUATION_TYPE, '{', 'A mapping element was expected');
|
||||
|
||||
|
||||
@@ -134,7 +134,7 @@ final class CoreExtension extends AbstractExtension
|
||||
|
||||
private $dateFormats = ['F j, Y H:i', '%d days'];
|
||||
private $numberFormat = [0, '.', ','];
|
||||
private $timezone = null;
|
||||
private $timezone;
|
||||
|
||||
/**
|
||||
* Sets the default format to be used by the date filter.
|
||||
@@ -1128,9 +1128,9 @@ final class CoreExtension extends AbstractExtension
|
||||
}
|
||||
if ((int) $bTrim == $bTrim) {
|
||||
return $a <=> (int) $bTrim;
|
||||
} else {
|
||||
return (float) $a <=> (float) $bTrim;
|
||||
}
|
||||
|
||||
return (float) $a <=> (float) $bTrim;
|
||||
}
|
||||
if (\is_string($a) && \is_int($b)) {
|
||||
$aTrim = trim($a, " \t\n\r\v\f");
|
||||
@@ -1139,9 +1139,9 @@ final class CoreExtension extends AbstractExtension
|
||||
}
|
||||
if ((int) $aTrim == $aTrim) {
|
||||
return (int) $aTrim <=> $b;
|
||||
} else {
|
||||
return (float) $aTrim <=> (float) $b;
|
||||
}
|
||||
|
||||
return (float) $aTrim <=> (float) $b;
|
||||
}
|
||||
|
||||
// float <=> string
|
||||
@@ -1179,7 +1179,7 @@ final class CoreExtension extends AbstractExtension
|
||||
*/
|
||||
public static function matches(string $regexp, ?string $str): int
|
||||
{
|
||||
set_error_handler(function ($t, $m) use ($regexp) {
|
||||
set_error_handler(static function ($t, $m) use ($regexp) {
|
||||
throw new RuntimeError(\sprintf('Regexp "%s" passed to "matches" is not valid', $regexp).substr($m, 12));
|
||||
});
|
||||
try {
|
||||
@@ -2148,7 +2148,7 @@ final class CoreExtension extends AbstractExtension
|
||||
*/
|
||||
public static function parseBlockFunction(Parser $parser, Node $fakeNode, $args, int $line): AbstractExpression
|
||||
{
|
||||
$fakeFunction = new TwigFunction('block', fn ($name, $template = null) => null);
|
||||
$fakeFunction = new TwigFunction('block', static fn ($name, $template = null) => null);
|
||||
$args = (new CallableArgumentsExtractor($fakeNode, $fakeFunction))->extractArguments($args);
|
||||
|
||||
return new BlockReferenceExpression($args[0], $args[1] ?? null, $line);
|
||||
@@ -2159,7 +2159,7 @@ final class CoreExtension extends AbstractExtension
|
||||
*/
|
||||
public static function parseAttributeFunction(Parser $parser, Node $fakeNode, $args, int $line): AbstractExpression
|
||||
{
|
||||
$fakeFunction = new TwigFunction('attribute', fn ($variable, $attribute, $arguments = null) => null);
|
||||
$fakeFunction = new TwigFunction('attribute', static fn ($variable, $attribute, $arguments = null) => null);
|
||||
$args = (new CallableArgumentsExtractor($fakeNode, $fakeFunction))->extractArguments($args);
|
||||
|
||||
/*
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
namespace Twig;
|
||||
|
||||
use Twig\Error\SyntaxError;
|
||||
use Twig\ExpressionParser\ExpressionParsers;
|
||||
|
||||
/**
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
@@ -527,7 +528,7 @@ class Lexer
|
||||
{
|
||||
$expressionParsers = [];
|
||||
foreach ($this->env->getExpressionParsers() as $expressionParser) {
|
||||
$expressionParsers = array_merge($expressionParsers, [$expressionParser->getName()], $expressionParser->getAliases());
|
||||
$expressionParsers = array_merge($expressionParsers, ExpressionParsers::getOperatorTokensFor($expressionParser));
|
||||
}
|
||||
|
||||
$expressionParsers = array_combine($expressionParsers, array_map('strlen', $expressionParsers));
|
||||
@@ -544,7 +545,7 @@ class Lexer
|
||||
|
||||
// an operator that begins with a character must not have a dot or pipe before
|
||||
if (ctype_alpha($expressionParser[0])) {
|
||||
$r = '(?<![\.\|])'.$r;
|
||||
$r = '(?<![\.\|]\s|.[\.\|])'.$r;
|
||||
}
|
||||
|
||||
// an operator with a space can be any amount of whitespaces
|
||||
|
||||
@@ -26,14 +26,14 @@ class ArrowFunctionExpression extends AbstractExpression
|
||||
{
|
||||
public function __construct(AbstractExpression $expr, Node $names, $lineno)
|
||||
{
|
||||
if (!$names instanceof ListExpression && !$names instanceof ContextVariable) {
|
||||
throw new SyntaxError('The arrow function argument must be a list of variables or a single variable.', $names->getTemplateLine(), $names->getSourceContext());
|
||||
}
|
||||
|
||||
if ($names instanceof ContextVariable) {
|
||||
$names = new ListExpression([new AssignContextVariable($names->getAttribute('name'), $names->getTemplateLine())], $lineno);
|
||||
}
|
||||
|
||||
if (!$names instanceof ListExpression) {
|
||||
throw new SyntaxError('The arrow function argument must be a list of variables or a single variable.', $names->getTemplateLine(), $names->getSourceContext());
|
||||
}
|
||||
|
||||
parent::__construct(['expr' => $expr, 'names' => $names], [], $lineno);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ namespace Twig\Node\Expression\Binary;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Error\SyntaxError;
|
||||
use Twig\Node\Expression\AbstractExpression;
|
||||
use Twig\Node\Expression\ConstantExpression;
|
||||
use Twig\Node\Expression\ReturnBoolInterface;
|
||||
use Twig\Node\Node;
|
||||
@@ -21,6 +22,13 @@ class MatchesBinary extends AbstractBinary implements ReturnBoolInterface
|
||||
{
|
||||
public function __construct(Node $left, Node $right, int $lineno)
|
||||
{
|
||||
if (!$left instanceof AbstractExpression) {
|
||||
trigger_deprecation('twig/twig', '3.24', 'Passing a "%s" instance to "%s()" first argument is deprecated, pass an "AbstractExpression" instance instead.', $left::class, __METHOD__);
|
||||
}
|
||||
if (!$right instanceof AbstractExpression) {
|
||||
trigger_deprecation('twig/twig', '3.24', 'Passing a "%s" instance to "%s()" second argument is deprecated, pass an "AbstractExpression" instance instead.', $right::class, __METHOD__);
|
||||
}
|
||||
|
||||
if ($right instanceof ConstantExpression) {
|
||||
$regexp = $right->getAttribute('value');
|
||||
set_error_handler(static fn ($t, $m) => throw new SyntaxError(\sprintf('Regexp "%s" passed to "matches" is not valid: %s.', $regexp, substr($m, 14)), $lineno));
|
||||
|
||||
@@ -23,7 +23,8 @@ use Twig\Node\Node;
|
||||
*/
|
||||
class ObjectDestructuringSetBinary extends AbstractBinary
|
||||
{
|
||||
private array $properties = [];
|
||||
/** @var list<array{property: string, variable: string}> */
|
||||
private array $mappings = [];
|
||||
|
||||
/**
|
||||
* @param ArrayExpression $left The array expression containing object/mapping destructuring properties
|
||||
@@ -38,7 +39,11 @@ class ObjectDestructuringSetBinary extends AbstractBinary
|
||||
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');
|
||||
|
||||
$this->mappings[] = [
|
||||
'property' => $pair['key']->getAttribute('value'),
|
||||
'variable' => $pair['value']->getAttribute('name'),
|
||||
];
|
||||
}
|
||||
|
||||
parent::__construct($left, $right, $lineno);
|
||||
@@ -48,18 +53,18 @@ class ObjectDestructuringSetBinary extends AbstractBinary
|
||||
{
|
||||
$compiler->addDebugInfo($this);
|
||||
$compiler->raw('[');
|
||||
foreach ($this->properties as $i => $property) {
|
||||
foreach ($this->mappings as $i => $mapping) {
|
||||
if ($i) {
|
||||
$compiler->raw(', ');
|
||||
}
|
||||
$compiler->raw('$context[')->repr($property)->raw(']');
|
||||
$compiler->raw('$context[')->repr($mapping['variable'])->raw(']');
|
||||
}
|
||||
$compiler->raw('] = [');
|
||||
foreach ($this->properties as $i => $property) {
|
||||
foreach ($this->mappings as $i => $mapping) {
|
||||
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('CoreExtension::getAttribute($this->env, $this->source, ')->subcompile($this->getNode('right'))->raw(', ')->repr($mapping['property'])->raw(', [], \\Twig\\Template::ANY_CALL, false, false, false, ')->repr($this->getNode('right')->getTemplateLine())->raw(')');
|
||||
}
|
||||
$compiler->raw(']');
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ use Twig\Util\ReflectionCallable;
|
||||
|
||||
abstract class CallExpression extends AbstractExpression
|
||||
{
|
||||
private $reflector = null;
|
||||
private $reflector;
|
||||
|
||||
/**
|
||||
* @return void
|
||||
@@ -213,9 +213,8 @@ abstract class CallExpression extends AbstractExpression
|
||||
} elseif ($callableParameter->isOptional()) {
|
||||
if (!$parameters) {
|
||||
break;
|
||||
} else {
|
||||
$missingArguments[] = $name;
|
||||
}
|
||||
$missingArguments[] = $name;
|
||||
} else {
|
||||
throw new SyntaxError(\sprintf('Value for argument "%s" is required for %s "%s".', $name, $callType, $callName), $this->getTemplateLine(), $this->getSourceContext());
|
||||
}
|
||||
|
||||
@@ -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' => !$nullSafe, 'null_safe' => $nullSafe], $lineno);
|
||||
parent::__construct($nodes, ['type' => $type, 'ignore_strict_check' => false, 'optimizable' => !$nullSafe, 'null_safe' => $nullSafe, 'is_short_circuited' => false, 'var_name' => null], $lineno);
|
||||
}
|
||||
|
||||
public function enableDefinedTest(): void
|
||||
@@ -50,7 +50,6 @@ class GetAttrExpression extends AbstractExpression implements SupportDefinedTest
|
||||
$env = $compiler->getEnvironment();
|
||||
$arrayAccessSandbox = false;
|
||||
$nullSafe = $this->getAttribute('null_safe');
|
||||
$objectVar = null;
|
||||
|
||||
// optimize array calls
|
||||
if (
|
||||
@@ -99,18 +98,32 @@ class GetAttrExpression extends AbstractExpression implements SupportDefinedTest
|
||||
$this->getNode('node')->setAttribute('ignore_strict_check', true);
|
||||
}
|
||||
|
||||
if ($nullSafe) {
|
||||
$objectVar = '$'.$compiler->getVarName();
|
||||
if (null === $nullSafeNode = $nullSafe ? $this : null) {
|
||||
$node = $this->getNode('node');
|
||||
while ($node instanceof self) {
|
||||
if ($node->getAttribute('null_safe')) {
|
||||
$nullSafeNode = $node;
|
||||
break;
|
||||
}
|
||||
$node = $node->getNode('node');
|
||||
}
|
||||
}
|
||||
|
||||
$isShortCircuited = false;
|
||||
if (null !== $nullSafeNode && !$nullSafeNode->isShortCircuited()) {
|
||||
$compiler
|
||||
->raw('((null === ('.$objectVar.' = ')
|
||||
->subcompile($this->getNode('node'))
|
||||
->raw('((null === ('.$nullSafeNode->getVarName($compiler).' = ')
|
||||
->subcompile($nullSafeNode->getNode('node'))
|
||||
->raw(')) ? null : ');
|
||||
|
||||
$nullSafeNode->markAsShortCircuited();
|
||||
$isShortCircuited = true;
|
||||
}
|
||||
|
||||
$compiler->raw('CoreExtension::getAttribute($this->env, $this->source, ');
|
||||
|
||||
if ($nullSafe) {
|
||||
$compiler->raw($objectVar);
|
||||
$compiler->raw($this->getVarName($compiler));
|
||||
} else {
|
||||
$compiler->subcompile($this->getNode('node'));
|
||||
}
|
||||
@@ -139,7 +152,7 @@ class GetAttrExpression extends AbstractExpression implements SupportDefinedTest
|
||||
$compiler->raw(')');
|
||||
}
|
||||
|
||||
if ($nullSafe) {
|
||||
if ($isShortCircuited) {
|
||||
$compiler->raw(')');
|
||||
}
|
||||
}
|
||||
@@ -153,4 +166,23 @@ class GetAttrExpression extends AbstractExpression implements SupportDefinedTest
|
||||
$this->changeIgnoreStrictCheck($node->getNode('node'));
|
||||
}
|
||||
}
|
||||
|
||||
private function markAsShortCircuited(): void
|
||||
{
|
||||
$this->setAttribute('is_short_circuited', true);
|
||||
}
|
||||
|
||||
private function isShortCircuited(): bool
|
||||
{
|
||||
return $this->getAttribute('is_short_circuited');
|
||||
}
|
||||
|
||||
private function getVarName(Compiler $compiler): string
|
||||
{
|
||||
if (null === $this->getAttribute('var_name')) {
|
||||
$this->setAttribute('var_name', $compiler->getVarName());
|
||||
}
|
||||
|
||||
return '$'.$this->getAttribute('var_name');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,12 +12,12 @@
|
||||
namespace Twig\Node\Expression;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\Expression\Variable\ContextVariable;
|
||||
use Twig\Node\Expression\Variable\AssignContextVariable;
|
||||
|
||||
class ListExpression extends AbstractExpression
|
||||
{
|
||||
/**
|
||||
* @param array<ContextVariable> $items
|
||||
* @param array<AssignContextVariable> $items
|
||||
*/
|
||||
public function __construct(array $items, int $lineno)
|
||||
{
|
||||
|
||||
@@ -29,6 +29,16 @@ class MacroReferenceExpression extends AbstractExpression implements SupportDefi
|
||||
parent::__construct(['template' => $template, 'arguments' => $arguments], ['name' => $name], $lineno);
|
||||
}
|
||||
|
||||
public function __clone()
|
||||
{
|
||||
// The template node must not be deep-cloned because its name is
|
||||
// lazily generated during compilation and must stay in sync with
|
||||
// the AssignTemplateVariable that populates the $macros array.
|
||||
$template = $this->nodes['template'];
|
||||
parent::__clone();
|
||||
$this->nodes['template'] = $template;
|
||||
}
|
||||
|
||||
public function compile(Compiler $compiler): void
|
||||
{
|
||||
if ($this->definedTest) {
|
||||
|
||||
@@ -54,6 +54,11 @@ final class SafeAnalysisNodeVisitor implements NodeVisitorInterface
|
||||
|
||||
if (\in_array('html_attr', $bucket['value'], true)) {
|
||||
$bucket['value'][] = 'html';
|
||||
$bucket['value'][] = 'html_attr_relaxed';
|
||||
}
|
||||
|
||||
if (\in_array('html_attr_relaxed', $bucket['value'], true)) {
|
||||
$bucket['value'][] = 'html';
|
||||
}
|
||||
|
||||
return $bucket['value'];
|
||||
|
||||
@@ -416,6 +416,10 @@ class Parser
|
||||
trigger_deprecation('twig/twig', '3.12', 'Passing "null" to "%s()" is deprecated.', __METHOD__);
|
||||
}
|
||||
|
||||
if (null !== $parent && !$parent instanceof AbstractExpression) {
|
||||
trigger_deprecation('twig/twig', '3.24', 'Passing a "%s" instance to "%s()" is deprecated, pass an "AbstractExpression" instance instead.', $parent::class, __METHOD__);
|
||||
}
|
||||
|
||||
if (null !== $this->parent) {
|
||||
throw new SyntaxError('Multiple extends tags are forbidden.', $parent->getTemplateLine(), $parent->getSourceContext());
|
||||
}
|
||||
@@ -447,7 +451,7 @@ class Parser
|
||||
|
||||
if (!$function) {
|
||||
if ($this->shouldIgnoreUnknownTwigCallables()) {
|
||||
return new TwigFunction($name, fn () => '');
|
||||
return new TwigFunction($name, static fn () => '');
|
||||
}
|
||||
$e = new SyntaxError(\sprintf('Unknown "%s" function.', $name), $line, $this->stream->getSourceContext());
|
||||
$e->addSuggestions($name, array_keys($this->env->getFunctions()));
|
||||
@@ -476,7 +480,7 @@ class Parser
|
||||
}
|
||||
if (!$filter) {
|
||||
if ($this->shouldIgnoreUnknownTwigCallables()) {
|
||||
return new TwigFilter($name, fn () => '');
|
||||
return new TwigFilter($name, static fn () => '');
|
||||
}
|
||||
$e = new SyntaxError(\sprintf('Unknown "%s" filter.', $name), $line, $this->stream->getSourceContext());
|
||||
$e->addSuggestions($name, array_keys($this->env->getFilters()));
|
||||
@@ -524,7 +528,7 @@ class Parser
|
||||
|
||||
if (!$test) {
|
||||
if ($this->shouldIgnoreUnknownTwigCallables()) {
|
||||
return new TwigTest($name, fn () => '');
|
||||
return new TwigTest($name, static fn () => '');
|
||||
}
|
||||
$e = new SyntaxError(\sprintf('Unknown "%s" test.', $name), $line, $this->stream->getSourceContext());
|
||||
$e->addSuggestions($name, array_keys($this->env->getTests()));
|
||||
|
||||
@@ -124,7 +124,7 @@ final class EscaperRuntime implements RuntimeExtensionInterface
|
||||
}
|
||||
|
||||
$string = (string) $string;
|
||||
} elseif (\in_array($strategy, ['html', 'js', 'css', 'html_attr', 'url'], true)) {
|
||||
} elseif (\in_array($strategy, ['html', 'js', 'css', 'html_attr', 'html_attr_relaxed', 'url'], true)) {
|
||||
// we return the input as is (which can be of any type)
|
||||
return $string;
|
||||
}
|
||||
@@ -191,7 +191,7 @@ final class EscaperRuntime implements RuntimeExtensionInterface
|
||||
throw new RuntimeError('The string to escape is not a valid UTF-8 string.');
|
||||
}
|
||||
|
||||
$string = preg_replace_callback('#[^a-zA-Z0-9,\._]#Su', function ($matches) {
|
||||
$string = preg_replace_callback('#[^a-zA-Z0-9,\._]#Su', static function ($matches) {
|
||||
$char = $matches[0];
|
||||
|
||||
/*
|
||||
@@ -243,7 +243,7 @@ final class EscaperRuntime implements RuntimeExtensionInterface
|
||||
throw new RuntimeError('The string to escape is not a valid UTF-8 string.');
|
||||
}
|
||||
|
||||
$string = preg_replace_callback('#[^a-zA-Z0-9]#Su', function ($matches) {
|
||||
$string = preg_replace_callback('#[^a-zA-Z0-9]#Su', static function ($matches) {
|
||||
$char = $matches[0];
|
||||
|
||||
return \sprintf('\\%X ', 1 === \strlen($char) ? \ord($char) : mb_ord($char, 'UTF-8'));
|
||||
@@ -256,6 +256,7 @@ final class EscaperRuntime implements RuntimeExtensionInterface
|
||||
return $string;
|
||||
|
||||
case 'html_attr':
|
||||
case 'html_attr_relaxed':
|
||||
if ('UTF-8' !== $charset) {
|
||||
$string = $this->convertEncoding($string, 'UTF-8', $charset);
|
||||
}
|
||||
@@ -264,7 +265,12 @@ final class EscaperRuntime implements RuntimeExtensionInterface
|
||||
throw new RuntimeError('The string to escape is not a valid UTF-8 string.');
|
||||
}
|
||||
|
||||
$string = preg_replace_callback('#[^a-zA-Z0-9,\.\-_]#Su', function ($matches) {
|
||||
$regex = match ($strategy) {
|
||||
'html_attr' => '#[^a-zA-Z0-9,\.\-_]#Su',
|
||||
'html_attr_relaxed' => '#[^a-zA-Z0-9,\.\-_:@\[\]]#Su',
|
||||
};
|
||||
|
||||
$string = preg_replace_callback($regex, static function ($matches) {
|
||||
/**
|
||||
* This function is adapted from code coming from Zend Framework.
|
||||
*
|
||||
@@ -323,7 +329,7 @@ final class EscaperRuntime implements RuntimeExtensionInterface
|
||||
return $this->escapers[$strategy]($string, $charset);
|
||||
}
|
||||
|
||||
$validStrategies = implode('", "', array_merge(['html', 'js', 'url', 'css', 'html_attr'], array_keys($this->escapers)));
|
||||
$validStrategies = implode('", "', array_merge(['html', 'js', 'url', 'css', 'html_attr', 'html_attr_relaxed'], array_keys($this->escapers)));
|
||||
|
||||
throw new RuntimeError(\sprintf('Invalid escaping strategy "%s" (valid ones: "%s").', $strategy, $validStrategies));
|
||||
}
|
||||
|
||||
@@ -158,7 +158,7 @@ abstract class Template
|
||||
if ($this->env->isDebug()) {
|
||||
ob_start();
|
||||
} else {
|
||||
ob_start(function () { return ''; });
|
||||
ob_start(static function () { return ''; });
|
||||
}
|
||||
$this->displayParentBlock($name, $context, $blocks);
|
||||
|
||||
@@ -193,7 +193,7 @@ abstract class Template
|
||||
if ($this->env->isDebug()) {
|
||||
ob_start();
|
||||
} else {
|
||||
ob_start(function () { return ''; });
|
||||
ob_start(static function () { return ''; });
|
||||
}
|
||||
try {
|
||||
$this->displayBlock($name, $context, $blocks, $useBlocks);
|
||||
@@ -367,7 +367,7 @@ abstract class Template
|
||||
if ($this->env->isDebug()) {
|
||||
ob_start();
|
||||
} else {
|
||||
ob_start(function () { return ''; });
|
||||
ob_start(static function () { return ''; });
|
||||
}
|
||||
try {
|
||||
$this->display($context);
|
||||
|
||||
@@ -54,7 +54,7 @@ final class DeprecationCollector
|
||||
public function collect(\Traversable $iterator): array
|
||||
{
|
||||
$deprecations = [];
|
||||
set_error_handler(function ($type, $msg) use (&$deprecations) {
|
||||
set_error_handler(static function ($type, $msg) use (&$deprecations) {
|
||||
if (\E_USER_DEPRECATED === $type) {
|
||||
$deprecations[] = $msg;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user