Skip to content

Commit 4e4d23b

Browse files
committed
Introduce operator classes to describe operators provided by extensions instead of arrays
1 parent 4f3770e commit 4e4d23b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+1766
-232
lines changed

.github/workflows/ci.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ jobs:
1919
strategy:
2020
matrix:
2121
php-version:
22-
- '8.0'
2322
- '8.1'
2423
- '8.2'
2524
- '8.3'
@@ -68,7 +67,6 @@ jobs:
6867
strategy:
6968
matrix:
7069
php-version:
71-
- '8.0'
7270
- '8.1'
7371
- '8.2'
7472
- '8.3'

CHANGELOG

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# 3.19.0 (2025-XX-XX)
22

3+
* Bump minimum PHP version to 8.1
4+
* Introduce operator classes to describe operators provided by extensions instead of arrays
35
* Fix `constant()` behavior when used with `??`
46
* Add the `invoke` filter
57
* Make `{}` optional for the `types` tag

doc/advanced.rst

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -775,26 +775,9 @@ responsible for parsing the tag and compiling it to PHP.
775775
Operators
776776
~~~~~~~~~
777777

778-
The ``getOperators()`` methods lets you add new operators. Here is how to add
779-
the ``!``, ``||``, and ``&&`` operators::
780-
781-
class CustomTwigExtension extends \Twig\Extension\AbstractExtension
782-
{
783-
public function getOperators()
784-
{
785-
return [
786-
[
787-
'!' => ['precedence' => 50, 'class' => \Twig\Node\Expression\Unary\NotUnary::class],
788-
],
789-
[
790-
'||' => ['precedence' => 10, 'class' => \Twig\Node\Expression\Binary\OrBinary::class, 'associativity' => \Twig\ExpressionParser::OPERATOR_LEFT],
791-
'&&' => ['precedence' => 15, 'class' => \Twig\Node\Expression\Binary\AndBinary::class, 'associativity' => \Twig\ExpressionParser::OPERATOR_LEFT],
792-
],
793-
];
794-
}
795-
796-
// ...
797-
}
778+
The ``getOperators()`` methods lets you add new operators. To implement a new
779+
one, have a look at the default operators provided by
780+
``Twig\Extension\CoreExtension``.
798781

799782
Tests
800783
~~~~~

doc/deprecated.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,3 +411,26 @@ Operators
411411
{# or #}
412412

413413
{{ (not 1) * 2 }} {# this is equivalent to what Twig 4.x will do without the parentheses #}
414+
415+
* Operators are now instances of ``Twig\Operator\OperatorInterface`` instead of
416+
arrays. The ``ExtensionInterface::getOperators()`` method should now return an
417+
array of ``Twig\Operator\OperatorInterface`` instances.
418+
419+
Before:
420+
421+
public function getOperators(): array {
422+
return [
423+
'not' => [
424+
'precedence' => 10,
425+
'class' => NotUnaryOperator::class,
426+
],
427+
];
428+
}
429+
430+
After:
431+
432+
public function getOperators(): Operators {
433+
return new Operators([
434+
new NotUnaryOperator(),
435+
]);
436+
}

src/Environment.php

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,10 @@
2727
use Twig\Loader\ArrayLoader;
2828
use Twig\Loader\ChainLoader;
2929
use Twig\Loader\LoaderInterface;
30-
use Twig\Node\Expression\Binary\AbstractBinary;
31-
use Twig\Node\Expression\Unary\AbstractUnary;
3230
use Twig\Node\ModuleNode;
3331
use Twig\Node\Node;
3432
use Twig\NodeVisitor\NodeVisitorInterface;
33+
use Twig\Operator\Operators;
3534
use Twig\Runtime\EscaperRuntime;
3635
use Twig\RuntimeLoader\FactoryRuntimeLoader;
3736
use Twig\RuntimeLoader\RuntimeLoaderInterface;
@@ -862,22 +861,10 @@ public function mergeGlobals(array $context): array
862861

863862
/**
864863
* @internal
865-
*
866-
* @return array<string, array{precedence: int, precedence_change?: OperatorPrecedenceChange, class: class-string<AbstractUnary>}>
867-
*/
868-
public function getUnaryOperators(): array
869-
{
870-
return $this->extensionSet->getUnaryOperators();
871-
}
872-
873-
/**
874-
* @internal
875-
*
876-
* @return array<string, array{precedence: int, precedence_change?: OperatorPrecedenceChange, class: class-string<AbstractBinary>, associativity: ExpressionParser::OPERATOR_*}>
877864
*/
878-
public function getBinaryOperators(): array
865+
public function getOperators(): Operators
879866
{
880-
return $this->extensionSet->getBinaryOperators();
867+
return $this->extensionSet->getOperators();
881868
}
882869

883870
private function updateOptionsHash(): void

src/ExpressionParser.php

Lines changed: 39 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,13 @@
1818
use Twig\Node\Expression\AbstractExpression;
1919
use Twig\Node\Expression\ArrayExpression;
2020
use Twig\Node\Expression\ArrowFunctionExpression;
21-
use Twig\Node\Expression\Binary\AbstractBinary;
2221
use Twig\Node\Expression\Binary\ConcatBinary;
2322
use Twig\Node\Expression\ConstantExpression;
2423
use Twig\Node\Expression\GetAttrExpression;
2524
use Twig\Node\Expression\MacroReferenceExpression;
2625
use Twig\Node\Expression\NameExpression;
2726
use Twig\Node\Expression\Ternary\ConditionalTernary;
2827
use Twig\Node\Expression\TestExpression;
29-
use Twig\Node\Expression\Unary\AbstractUnary;
3028
use Twig\Node\Expression\Unary\NegUnary;
3129
use Twig\Node\Expression\Unary\NotUnary;
3230
use Twig\Node\Expression\Unary\PosUnary;
@@ -37,6 +35,9 @@
3735
use Twig\Node\Expression\Variable\TemplateVariable;
3836
use Twig\Node\Node;
3937
use Twig\Node\Nodes;
38+
use Twig\Operator\OperatorArity;
39+
use Twig\Operator\OperatorAssociativity;
40+
use Twig\Operator\Operators;
4041

4142
/**
4243
* Parses expressions.
@@ -50,41 +51,33 @@
5051
*/
5152
class ExpressionParser
5253
{
54+
// deprecated, to be removed in 4.0
5355
public const OPERATOR_LEFT = 1;
5456
public const OPERATOR_RIGHT = 2;
5557

56-
/** @var array<string, array{precedence: int, precedence_change?: OperatorPrecedenceChange, class: class-string<AbstractUnary>}> */
57-
private $unaryOperators;
58-
/** @var array<string, array{precedence: int, precedence_change?: OperatorPrecedenceChange, class: class-string<AbstractBinary>, associativity: self::OPERATOR_*}> */
59-
private $binaryOperators;
58+
private Operators $operators;
6059
private $readyNodes = [];
61-
private array $precedenceChanges = [];
60+
private \WeakMap $precedenceChanges;
6261
private bool $deprecationCheck = true;
6362

6463
public function __construct(
6564
private Parser $parser,
6665
private Environment $env,
6766
) {
68-
$this->unaryOperators = $env->getUnaryOperators();
69-
$this->binaryOperators = $env->getBinaryOperators();
70-
71-
$ops = [];
72-
foreach ($this->unaryOperators as $n => $c) {
73-
$ops[] = $c + ['name' => $n, 'type' => 'unary'];
74-
}
75-
foreach ($this->binaryOperators as $n => $c) {
76-
$ops[] = $c + ['name' => $n, 'type' => 'binary'];
77-
}
78-
foreach ($ops as $config) {
79-
if (!isset($config['precedence_change'])) {
67+
$this->operators = $env->getOperators();
68+
$this->precedenceChanges = new \WeakMap();
69+
foreach ($this->operators as $op) {
70+
if (!$op->getPrecedenceChange()) {
8071
continue;
8172
}
82-
$name = $config['type'].'_'.$config['name'];
83-
$min = min($config['precedence_change']->getNewPrecedence(), $config['precedence']);
84-
$max = max($config['precedence_change']->getNewPrecedence(), $config['precedence']);
85-
foreach ($ops as $c) {
86-
if ($c['precedence'] > $min && $c['precedence'] < $max) {
87-
$this->precedenceChanges[$c['type'].'_'.$c['name']][] = $name;
73+
$min = min($op->getPrecedenceChange()->getNewPrecedence(), $op->getPrecedence());
74+
$max = max($op->getPrecedenceChange()->getNewPrecedence(), $op->getPrecedence());
75+
foreach ($this->operators as $o) {
76+
if ($o->getPrecedence() > $min && $o->getPrecedence() < $max) {
77+
if (!isset($this->precedenceChanges[$o])) {
78+
$this->precedenceChanges[$o] = [];
79+
}
80+
$this->precedenceChanges[$o][] = $op;
8881
}
8982
}
9083
}
@@ -102,28 +95,27 @@ public function parseExpression($precedence = 0)
10295

10396
$expr = $this->getPrimary();
10497
$token = $this->parser->getCurrentToken();
105-
while ($this->isBinary($token) && $this->binaryOperators[$token->getValue()]['precedence'] >= $precedence) {
106-
$op = $this->binaryOperators[$token->getValue()];
98+
while ($token->test(Token::OPERATOR_TYPE) && ($op = $this->operators->getBinary($token->getValue())) && $op->getPrecedence() >= $precedence) {
10799
$this->parser->getStream()->next();
108100

109101
if ('is not' === $token->getValue()) {
110102
$expr = $this->parseNotTestExpression($expr);
111103
} elseif ('is' === $token->getValue()) {
112104
$expr = $this->parseTestExpression($expr);
113-
} elseif (isset($op['callable'])) {
114-
$expr = $op['callable']($this->parser, $expr);
105+
} elseif (null !== $op->getCallable()) {
106+
$expr = $op->getCallable()($this->parser, $expr);
115107
} else {
116108
$previous = $this->setDeprecationCheck(true);
117109
try {
118-
$expr1 = $this->parseExpression(self::OPERATOR_LEFT === $op['associativity'] ? $op['precedence'] + 1 : $op['precedence']);
110+
$expr1 = $this->parseExpression(OperatorAssociativity::Left === $op->getAssociativity() ? $op->getPrecedence() + 1 : $op->getPrecedence());
119111
} finally {
120112
$this->setDeprecationCheck($previous);
121113
}
122-
$class = $op['class'];
114+
$class = $op->getNodeClass();
123115
$expr = new $class($expr, $expr1, $token->getLine());
124116
}
125117

126-
$expr->setAttribute('operator', 'binary_'.$token->getValue());
118+
$expr->setAttribute('operator', $op);
127119

128120
$this->triggerPrecedenceDeprecations($expr);
129121

@@ -144,30 +136,29 @@ private function triggerPrecedenceDeprecations(AbstractExpression $expr): void
144136
return;
145137
}
146138

147-
if (str_starts_with($unaryOp = $expr->getAttribute('operator'), 'unary')) {
139+
if (OperatorArity::Unary === $expr->getAttribute('operator')->getArity()) {
148140
if ($expr->hasExplicitParentheses()) {
149141
return;
150142
}
151-
$target = explode('_', $unaryOp)[1];
143+
$operator = $expr->getAttribute('operator');
152144
/** @var AbstractExpression $node */
153145
$node = $expr->getNode('node');
154-
foreach ($this->precedenceChanges as $operatorName => $changes) {
155-
if (!\in_array($unaryOp, $changes)) {
146+
foreach ($this->precedenceChanges as $op => $changes) {
147+
if (!\in_array($operator, $changes, true)) {
156148
continue;
157149
}
158-
if ($node->hasAttribute('operator') && $operatorName === $node->getAttribute('operator')) {
159-
$change = $this->unaryOperators[$target]['precedence_change'];
160-
trigger_deprecation($change->getPackage(), $change->getVersion(), \sprintf('Add explicit parentheses around the "%s" unary operator to avoid behavior change in the next major version as its precedence will change in "%s" at line %d.', $target, $this->parser->getStream()->getSourceContext()->getName(), $node->getTemplateLine()));
150+
if ($node->hasAttribute('operator') && $op === $node->getAttribute('operator')) {
151+
$change = $operator->getPrecedenceChange();
152+
trigger_deprecation($change->getPackage(), $change->getVersion(), \sprintf('Add explicit parentheses around the "%s" unary operator to avoid behavior change in the next major version as its precedence will change in "%s" at line %d.', $operator->getOperator(), $this->parser->getStream()->getSourceContext()->getName(), $node->getTemplateLine()));
161153
}
162154
}
163155
} else {
164-
foreach ($this->precedenceChanges[$expr->getAttribute('operator')] as $operatorName) {
156+
foreach ($this->precedenceChanges[$expr->getAttribute('operator')] as $operator) {
165157
foreach ($expr as $node) {
166158
/** @var AbstractExpression $node */
167-
if ($node->hasAttribute('operator') && $operatorName === $node->getAttribute('operator') && !$node->hasExplicitParentheses()) {
168-
$op = explode('_', $operatorName)[1];
169-
$change = $this->binaryOperators[$op]['precedence_change'];
170-
trigger_deprecation($change->getPackage(), $change->getVersion(), \sprintf('Add explicit parentheses around the "%s" binary operator to avoid behavior change in the next major version as its precedence will change in "%s" at line %d.', $op, $this->parser->getStream()->getSourceContext()->getName(), $node->getTemplateLine()));
159+
if ($node->hasAttribute('operator') && $operator === $node->getAttribute('operator') && !$node->hasExplicitParentheses()) {
160+
$change = $operator->getPrecedenceChange();
161+
trigger_deprecation($change->getPackage(), $change->getVersion(), \sprintf('Add explicit parentheses around the "%s" binary operator to avoid behavior change in the next major version as its precedence will change in "%s" at line %d.', $operator->getOperator(), $this->parser->getStream()->getSourceContext()->getName(), $node->getTemplateLine()));
171162
}
172163
}
173164
}
@@ -236,14 +227,13 @@ private function getPrimary(): AbstractExpression
236227
{
237228
$token = $this->parser->getCurrentToken();
238229

239-
if ($this->isUnary($token)) {
240-
$operator = $this->unaryOperators[$token->getValue()];
230+
if ($token->test(Token::OPERATOR_TYPE) && $operator = $this->operators->getUnary($token->getValue())) {
241231
$this->parser->getStream()->next();
242-
$expr = $this->parseExpression($operator['precedence']);
243-
$class = $operator['class'];
232+
$expr = $this->parseExpression($operator->getPrecedence());
233+
$class = $operator->getNodeClass();
244234

245235
$expr = new $class($expr, $token->getLine());
246-
$expr->setAttribute('operator', 'unary_'.$token->getValue());
236+
$expr->setAttribute('operator', $operator);
247237

248238
if ($this->deprecationCheck) {
249239
$this->triggerPrecedenceDeprecations($expr);
@@ -284,16 +274,6 @@ private function parseConditionalExpression($expr): AbstractExpression
284274
return $expr;
285275
}
286276

287-
private function isUnary(Token $token): bool
288-
{
289-
return $token->test(Token::OPERATOR_TYPE) && isset($this->unaryOperators[$token->getValue()]);
290-
}
291-
292-
private function isBinary(Token $token): bool
293-
{
294-
return $token->test(Token::OPERATOR_TYPE) && isset($this->binaryOperators[$token->getValue()]);
295-
}
296-
297277
public function parsePrimaryExpression()
298278
{
299279
$token = $this->parser->getCurrentToken();

src/Extension/AbstractExtension.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public function getFunctions()
4040

4141
public function getOperators()
4242
{
43-
return [[], []];
43+
return [];
4444
}
4545

4646
public function getLastModified(): int

0 commit comments

Comments
 (0)