1<?php
2
3/*
4 * This file is part of Twig.
5 *
6 * (c) Fabien Potencier
7 * (c) Armin Ronacher
8 *
9 * For the full copyright and license information, please view the LICENSE
10 * file that was distributed with this source code.
11 */
12
13namespace Twig\Node;
14
15use Twig\Attribute\YieldReady;
16use Twig\Compiler;
17use Twig\Source;
18
19/**
20 * Represents a node in the AST.
21 *
22 * @author Fabien Potencier <fabien@symfony.com>
23 *
24 * @implements \IteratorAggregate<int|string, Node>
25 */
26#[YieldReady]
27class Node implements \Countable, \IteratorAggregate
28{
29    /**
30     * @var array<string|int, Node>
31     */
32    protected $nodes;
33    protected $attributes;
34    protected $lineno;
35    protected $tag;
36
37    private $sourceContext;
38    /** @var array<string, NameDeprecation> */
39    private $nodeNameDeprecations = [];
40    /** @var array<string, NameDeprecation> */
41    private $attributeNameDeprecations = [];
42
43    /**
44     * @param array<string|int, Node> $nodes      An array of named nodes
45     * @param array                   $attributes An array of attributes (should not be nodes)
46     * @param int                     $lineno     The line number
47     */
48    public function __construct(array $nodes = [], array $attributes = [], int $lineno = 0)
49    {
50        if (self::class === static::class) {
51            trigger_deprecation('twig/twig', '3.15', \sprintf('Instantiating "%s" directly is deprecated; the class will become abstract in 4.0.', self::class));
52        }
53
54        foreach ($nodes as $name => $node) {
55            if (!$node instanceof self) {
56                throw new \InvalidArgumentException(\sprintf('Using "%s" for the value of node "%s" of "%s" is not supported. You must pass a \Twig\Node\Node instance.', get_debug_type($node), $name, static::class));
57            }
58        }
59        $this->nodes = $nodes;
60        $this->attributes = $attributes;
61        $this->lineno = $lineno;
62
63        if (\func_num_args() > 3) {
64            trigger_deprecation('twig/twig', '3.12', \sprintf('The "tag" constructor argument of the "%s" class is deprecated and ignored (check which TokenParser class set it to "%s"), the tag is now automatically set by the Parser when needed.', static::class, func_get_arg(3) ?: 'null'));
65        }
66    }
67
68    public function __toString(): string
69    {
70        $repr = static::class;
71
72        if ($this->tag) {
73            $repr .= \sprintf("\n  tag: %s", $this->tag);
74        }
75
76        $attributes = [];
77        foreach ($this->attributes as $name => $value) {
78            if (\is_callable($value)) {
79                $v = '\Closure';
80            } elseif ($value instanceof \Stringable) {
81                $v = (string) $value;
82            } else {
83                $v = str_replace("\n", '', var_export($value, true));
84            }
85            $attributes[] = \sprintf('%s: %s', $name, $v);
86        }
87
88        if ($attributes) {
89            $repr .= \sprintf("\n  attributes:\n    %s", implode("\n    ", $attributes));
90        }
91
92        if (\count($this->nodes)) {
93            $repr .= "\n  nodes:";
94            foreach ($this->nodes as $name => $node) {
95                $len = \strlen($name) + 6;
96                $noderepr = [];
97                foreach (explode("\n", (string) $node) as $line) {
98                    $noderepr[] = str_repeat(' ', $len).$line;
99                }
100
101                $repr .= \sprintf("\n    %s: %s", $name, ltrim(implode("\n", $noderepr)));
102            }
103        }
104
105        return $repr;
106    }
107
108    public function __clone()
109    {
110        foreach ($this->nodes as $name => $node) {
111            $this->nodes[$name] = clone $node;
112        }
113    }
114
115    /**
116     * @return void
117     */
118    public function compile(Compiler $compiler)
119    {
120        foreach ($this->nodes as $node) {
121            $compiler->subcompile($node);
122        }
123    }
124
125    public function getTemplateLine(): int
126    {
127        return $this->lineno;
128    }
129
130    public function getNodeTag(): ?string
131    {
132        return $this->tag;
133    }
134
135    /**
136     * @internal
137     */
138    public function setNodeTag(string $tag): void
139    {
140        if ($this->tag) {
141            throw new \LogicException('The tag of a node can only be set once.');
142        }
143
144        $this->tag = $tag;
145    }
146
147    public function hasAttribute(string $name): bool
148    {
149        return \array_key_exists($name, $this->attributes);
150    }
151
152    public function getAttribute(string $name)
153    {
154        if (!\array_key_exists($name, $this->attributes)) {
155            throw new \LogicException(\sprintf('Attribute "%s" does not exist for Node "%s".', $name, static::class));
156        }
157
158        $triggerDeprecation = \func_num_args() > 1 ? func_get_arg(1) : true;
159        if ($triggerDeprecation && isset($this->attributeNameDeprecations[$name])) {
160            $dep = $this->attributeNameDeprecations[$name];
161            if ($dep->getNewName()) {
162                trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Getting attribute "%s" on a "%s" class is deprecated, get the "%s" attribute instead.', $name, static::class, $dep->getNewName());
163            } else {
164                trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Getting attribute "%s" on a "%s" class is deprecated.', $name, static::class);
165            }
166        }
167
168        return $this->attributes[$name];
169    }
170
171    public function setAttribute(string $name, $value): void
172    {
173        $triggerDeprecation = \func_num_args() > 2 ? func_get_arg(2) : true;
174        if ($triggerDeprecation && isset($this->attributeNameDeprecations[$name])) {
175            $dep = $this->attributeNameDeprecations[$name];
176            if ($dep->getNewName()) {
177                trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Setting attribute "%s" on a "%s" class is deprecated, set the "%s" attribute instead.', $name, static::class, $dep->getNewName());
178            } else {
179                trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Setting attribute "%s" on a "%s" class is deprecated.', $name, static::class);
180            }
181        }
182
183        $this->attributes[$name] = $value;
184    }
185
186    public function deprecateAttribute(string $name, NameDeprecation $dep): void
187    {
188        $this->attributeNameDeprecations[$name] = $dep;
189    }
190
191    public function removeAttribute(string $name): void
192    {
193        unset($this->attributes[$name]);
194    }
195
196    /**
197     * @param string|int $name
198     */
199    public function hasNode(string $name): bool
200    {
201        return isset($this->nodes[$name]);
202    }
203
204    /**
205     * @param string|int $name
206     */
207    public function getNode(string $name): self
208    {
209        if (!isset($this->nodes[$name])) {
210            throw new \LogicException(\sprintf('Node "%s" does not exist for Node "%s".', $name, static::class));
211        }
212
213        $triggerDeprecation = \func_num_args() > 1 ? func_get_arg(1) : true;
214        if ($triggerDeprecation && isset($this->nodeNameDeprecations[$name])) {
215            $dep = $this->nodeNameDeprecations[$name];
216            if ($dep->getNewName()) {
217                trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Getting node "%s" on a "%s" class is deprecated, get the "%s" node instead.', $name, static::class, $dep->getNewName());
218            } else {
219                trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Getting node "%s" on a "%s" class is deprecated.', $name, static::class);
220            }
221        }
222
223        return $this->nodes[$name];
224    }
225
226    /**
227     * @param string|int $name
228     */
229    public function setNode(string $name, self $node): void
230    {
231        $triggerDeprecation = \func_num_args() > 2 ? func_get_arg(2) : true;
232        if ($triggerDeprecation && isset($this->nodeNameDeprecations[$name])) {
233            $dep = $this->nodeNameDeprecations[$name];
234            if ($dep->getNewName()) {
235                trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Setting node "%s" on a "%s" class is deprecated, set the "%s" node instead.', $name, static::class, $dep->getNewName());
236            } else {
237                trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Setting node "%s" on a "%s" class is deprecated.', $name, static::class);
238            }
239        }
240
241        if (null !== $this->sourceContext) {
242            $node->setSourceContext($this->sourceContext);
243        }
244        $this->nodes[$name] = $node;
245    }
246
247    /**
248     * @param string|int $name
249     */
250    public function removeNode(string $name): void
251    {
252        unset($this->nodes[$name]);
253    }
254
255    /**
256     * @param string|int $name
257     */
258    public function deprecateNode(string $name, NameDeprecation $dep): void
259    {
260        $this->nodeNameDeprecations[$name] = $dep;
261    }
262
263    /**
264     * @return int
265     */
266    #[\ReturnTypeWillChange]
267    public function count()
268    {
269        return \count($this->nodes);
270    }
271
272    public function getIterator(): \Traversable
273    {
274        return new \ArrayIterator($this->nodes);
275    }
276
277    public function getTemplateName(): ?string
278    {
279        return $this->sourceContext ? $this->sourceContext->getName() : null;
280    }
281
282    public function setSourceContext(Source $source): void
283    {
284        $this->sourceContext = $source;
285        foreach ($this->nodes as $node) {
286            $node->setSourceContext($source);
287        }
288    }
289
290    public function getSourceContext(): ?Source
291    {
292        return $this->sourceContext;
293    }
294}
295