Skip to content

Commit d362f59

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

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

+1735
-221
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: 25 additions & 48 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,13 +51,11 @@
5051
*/
5152
class ExpressionParser
5253
{
54+
// FIXME: deprecated, use OperatorAssociativity instead
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 = [];
6160
private array $precedenceChanges = [];
6261
private bool $deprecationCheck = true;
@@ -65,26 +64,16 @@ 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+
foreach ($this->operators as $name => $op) {
69+
if (!$op->getPrecedenceChange()) {
8070
continue;
8171
}
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;
72+
$min = min($op->getPrecedenceChange()->getNewPrecedence(), $op->getPrecedence());
73+
$max = max($op->getPrecedenceChange()->getNewPrecedence(), $op->getPrecedence());
74+
foreach ($this->operators as $n => $o) {
75+
if ($o->getPrecedence() > $min && $o->getPrecedence() < $max) {
76+
$this->precedenceChanges[$n][] = $name;
8877
}
8978
}
9079
}
@@ -102,28 +91,27 @@ public function parseExpression($precedence = 0)
10291

10392
$expr = $this->getPrimary();
10493
$token = $this->parser->getCurrentToken();
105-
while ($this->isBinary($token) && $this->binaryOperators[$token->getValue()]['precedence'] >= $precedence) {
106-
$op = $this->binaryOperators[$token->getValue()];
94+
while ($token->test(Token::OPERATOR_TYPE) && ($op = $this->operators->getBinary($token->getValue())) && $op->getPrecedence() >= $precedence) {
10795
$this->parser->getStream()->next();
10896

10997
if ('is not' === $token->getValue()) {
11098
$expr = $this->parseNotTestExpression($expr);
11199
} elseif ('is' === $token->getValue()) {
112100
$expr = $this->parseTestExpression($expr);
113-
} elseif (isset($op['callable'])) {
114-
$expr = $op['callable']($this->parser, $expr);
101+
} elseif (null !== $op->getCallable()) {
102+
$expr = $op->getCallable()($this->parser, $expr);
115103
} else {
116104
$previous = $this->setDeprecationCheck(true);
117105
try {
118-
$expr1 = $this->parseExpression(self::OPERATOR_LEFT === $op['associativity'] ? $op['precedence'] + 1 : $op['precedence']);
106+
$expr1 = $this->parseExpression(OperatorAssociativity::Left === $op->getAssociativity() ? $op->getPrecedence() + 1 : $op->getPrecedence());
119107
} finally {
120108
$this->setDeprecationCheck($previous);
121109
}
122-
$class = $op['class'];
110+
$class = $op->getNodeClass();
123111
$expr = new $class($expr, $expr1, $token->getLine());
124112
}
125113

126-
$expr->setAttribute('operator', 'binary_'.$token->getValue());
114+
$expr->setAttribute('operator', OperatorArity::Binary->value.'_'.$token->getValue());
127115

128116
$this->triggerPrecedenceDeprecations($expr);
129117

@@ -156,7 +144,7 @@ private function triggerPrecedenceDeprecations(AbstractExpression $expr): void
156144
continue;
157145
}
158146
if ($node->hasAttribute('operator') && $operatorName === $node->getAttribute('operator')) {
159-
$change = $this->unaryOperators[$target]['precedence_change'];
147+
$change = $this->operators->getUnary($target)->getPrecedenceChange();
160148
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()));
161149
}
162150
}
@@ -166,7 +154,7 @@ private function triggerPrecedenceDeprecations(AbstractExpression $expr): void
166154
/** @var AbstractExpression $node */
167155
if ($node->hasAttribute('operator') && $operatorName === $node->getAttribute('operator') && !$node->hasExplicitParentheses()) {
168156
$op = explode('_', $operatorName)[1];
169-
$change = $this->binaryOperators[$op]['precedence_change'];
157+
$change = $this->operators->getBinary($op)->getPrecedenceChange();
170158
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()));
171159
}
172160
}
@@ -236,14 +224,13 @@ private function getPrimary(): AbstractExpression
236224
{
237225
$token = $this->parser->getCurrentToken();
238226

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

245232
$expr = new $class($expr, $token->getLine());
246-
$expr->setAttribute('operator', 'unary_'.$token->getValue());
233+
$expr->setAttribute('operator', OperatorArity::Unary->value.'_'.$token->getValue());
247234

248235
if ($this->deprecationCheck) {
249236
$this->triggerPrecedenceDeprecations($expr);
@@ -284,16 +271,6 @@ private function parseConditionalExpression($expr): AbstractExpression
284271
return $expr;
285272
}
286273

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-
297274
public function parsePrimaryExpression()
298275
{
299276
$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)