1<?php
2
3/*
4 * This file is part of Twig.
5 *
6 * (c) Fabien Potencier
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Twig\ExpressionParser;
13
14/**
15 * @template-implements \IteratorAggregate<ExpressionParserInterface>
16 *
17 * @internal
18 */
19final class ExpressionParsers implements \IteratorAggregate
20{
21    /**
22     * @var array<class-string<ExpressionParserInterface>, array<string, ExpressionParserInterface>>
23     */
24    private array $parsersByName = [];
25
26    /**
27     * @var array<class-string<ExpressionParserInterface>, ExpressionParserInterface>
28     */
29    private array $parsersByClass = [];
30
31    /**
32     * @var \WeakMap<ExpressionParserInterface, array<ExpressionParserInterface>>|null
33     */
34    private ?\WeakMap $precedenceChanges = null;
35
36    /**
37     * @param array<ExpressionParserInterface> $parsers
38     */
39    public function __construct(array $parsers = [])
40    {
41        $this->add($parsers);
42    }
43
44    /**
45     * @param array<ExpressionParserInterface> $parsers
46     *
47     * @return $this
48     */
49    public function add(array $parsers): static
50    {
51        foreach ($parsers as $parser) {
52            if ($parser->getPrecedence() > 512 || $parser->getPrecedence() < 0) {
53                trigger_deprecation('twig/twig', '3.21', 'Precedence for "%s" must be between 0 and 512, got %d.', $parser->getName(), $parser->getPrecedence());
54                // throw new \InvalidArgumentException(\sprintf('Precedence for "%s" must be between 0 and 512, got %d.', $parser->getName(), $parser->getPrecedence()));
55            }
56            $interface = $parser instanceof PrefixExpressionParserInterface ? PrefixExpressionParserInterface::class : InfixExpressionParserInterface::class;
57            $this->parsersByClass[$parser::class] = $parser;
58            foreach (self::getOperatorTokensFor($parser) as $token) {
59                $this->parsersByName[$interface][$token] = $parser;
60            }
61        }
62
63        return $this;
64    }
65
66    /**
67     * @template T of ExpressionParserInterface
68     *
69     * @param class-string<T> $class
70     *
71     * @return T|null
72     */
73    public function getByClass(string $class): ?ExpressionParserInterface
74    {
75        return $this->parsersByClass[$class] ?? null;
76    }
77
78    /**
79     * @template T of ExpressionParserInterface
80     *
81     * @param class-string<T> $interface
82     *
83     * @return T|null
84     */
85    public function getByName(string $interface, string $name): ?ExpressionParserInterface
86    {
87        return $this->parsersByName[$interface][$name] ?? null;
88    }
89
90    public function getIterator(): \Traversable
91    {
92        $seen = [];
93        foreach ($this->parsersByName as $parsers) {
94            foreach ($parsers as $parser) {
95                $id = spl_object_id($parser);
96                if (!isset($seen[$id])) {
97                    $seen[$id] = true;
98                    yield $parser;
99                }
100            }
101        }
102        foreach ($this->parsersByClass as $parser) {
103            $id = spl_object_id($parser);
104            if (!isset($seen[$id])) {
105                $seen[$id] = true;
106                yield $parser;
107            }
108        }
109    }
110
111    /**
112     * @internal
113     *
114     * @return \WeakMap<ExpressionParserInterface, array<ExpressionParserInterface>>
115     */
116    public function getPrecedenceChanges(): \WeakMap
117    {
118        if (null === $this->precedenceChanges) {
119            $this->precedenceChanges = new \WeakMap();
120            foreach ($this as $ep) {
121                if (!$ep->getPrecedenceChange()) {
122                    continue;
123                }
124                $min = min($ep->getPrecedenceChange()->getNewPrecedence(), $ep->getPrecedence());
125                $max = max($ep->getPrecedenceChange()->getNewPrecedence(), $ep->getPrecedence());
126                foreach ($this as $e) {
127                    if ($e->getPrecedence() > $min && $e->getPrecedence() < $max) {
128                        if (!isset($this->precedenceChanges[$e])) {
129                            $this->precedenceChanges[$e] = [];
130                        }
131                        $this->precedenceChanges[$e][] = $ep;
132                    }
133                }
134            }
135        }
136
137        return $this->precedenceChanges;
138    }
139
140    /**
141     * @internal
142     *
143     * @return array<string>
144     */
145    public static function getOperatorTokensFor(ExpressionParserInterface $parser): array
146    {
147        if (method_exists($parser, 'getOperatorTokens')) {
148            return $parser->getOperatorTokens();
149        }
150
151        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);
152
153        return [$parser->getName(), ...$parser->getAliases()];
154    }
155}
156