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:
jf-cbd
2026-04-20 14:47:44 +02:00
committed by GitHub
parent f439490bfc
commit 805087a01b
171 changed files with 5629 additions and 1446 deletions

View File

@@ -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)

View File

@@ -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": [

View File

@@ -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"
}
}

View File

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

View File

@@ -27,4 +27,9 @@ abstract class AbstractExpressionParser implements ExpressionParserInterface
{
return [];
}
public function getOperatorTokens(): array
{
return [$this->getName(), ...$this->getAliases()];
}
}

View File

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

View File

@@ -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()];
}
}

View File

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

View File

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

View File

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

View File

@@ -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);
/*

View File

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

View File

@@ -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);
}

View File

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

View File

@@ -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(']');
}

View File

@@ -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());
}

View File

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

View File

@@ -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)
{

View File

@@ -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) {

View File

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

View File

@@ -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()));

View File

@@ -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));
}

View File

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

View File

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