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