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; 13 14use Twig\Error\RuntimeError; 15use Twig\Extension\ExtensionInterface; 16use Twig\Extension\GlobalsInterface; 17use Twig\Extension\InitRuntimeInterface; 18use Twig\Extension\StagingExtension; 19use Twig\NodeVisitor\NodeVisitorInterface; 20use Twig\TokenParser\TokenParserInterface; 21 22/** 23 * @author Fabien Potencier <fabien@symfony.com> 24 * 25 * @internal 26 */ 27final class ExtensionSet 28{ 29 private $extensions; 30 private $initialized = false; 31 private $runtimeInitialized = false; 32 private $staging; 33 private $parsers; 34 private $visitors; 35 private $filters; 36 private $tests; 37 private $functions; 38 private $unaryOperators; 39 private $binaryOperators; 40 private $globals; 41 private $functionCallbacks = []; 42 private $filterCallbacks = []; 43 private $lastModified = 0; 44 45 public function __construct() 46 { 47 $this->staging = new StagingExtension(); 48 } 49 50 /** 51 * Initializes the runtime environment. 52 * 53 * @deprecated since Twig 2.7 54 */ 55 public function initRuntime(Environment $env) 56 { 57 if ($this->runtimeInitialized) { 58 return; 59 } 60 61 $this->runtimeInitialized = true; 62 63 foreach ($this->extensions as $extension) { 64 if ($extension instanceof InitRuntimeInterface) { 65 $extension->initRuntime($env); 66 } 67 } 68 } 69 70 public function hasExtension(string $class): bool 71 { 72 $class = ltrim($class, '\\'); 73 if (!isset($this->extensions[$class]) && class_exists($class, false)) { 74 // For BC/FC with namespaced aliases 75 $class = (new \ReflectionClass($class))->name; 76 } 77 78 return isset($this->extensions[$class]); 79 } 80 81 public function getExtension(string $class): ExtensionInterface 82 { 83 $class = ltrim($class, '\\'); 84 if (!isset($this->extensions[$class]) && class_exists($class, false)) { 85 // For BC/FC with namespaced aliases 86 $class = (new \ReflectionClass($class))->name; 87 } 88 89 if (!isset($this->extensions[$class])) { 90 throw new RuntimeError(sprintf('The "%s" extension is not enabled.', $class)); 91 } 92 93 return $this->extensions[$class]; 94 } 95 96 /** 97 * @param ExtensionInterface[] $extensions 98 */ 99 public function setExtensions(array $extensions) 100 { 101 foreach ($extensions as $extension) { 102 $this->addExtension($extension); 103 } 104 } 105 106 /** 107 * @return ExtensionInterface[] 108 */ 109 public function getExtensions(): array 110 { 111 return $this->extensions; 112 } 113 114 public function getSignature(): string 115 { 116 return json_encode(array_keys($this->extensions)); 117 } 118 119 public function isInitialized(): bool 120 { 121 return $this->initialized || $this->runtimeInitialized; 122 } 123 124 public function getLastModified(): int 125 { 126 if (0 !== $this->lastModified) { 127 return $this->lastModified; 128 } 129 130 foreach ($this->extensions as $extension) { 131 $r = new \ReflectionObject($extension); 132 if (file_exists($r->getFileName()) && ($extensionTime = filemtime($r->getFileName())) > $this->lastModified) { 133 $this->lastModified = $extensionTime; 134 } 135 } 136 137 return $this->lastModified; 138 } 139 140 public function addExtension(ExtensionInterface $extension) 141 { 142 $class = \get_class($extension); 143 144 if ($this->initialized) { 145 throw new \LogicException(sprintf('Unable to register extension "%s" as extensions have already been initialized.', $class)); 146 } 147 148 if (isset($this->extensions[$class])) { 149 throw new \LogicException(sprintf('Unable to register extension "%s" as it is already registered.', $class)); 150 } 151 152 // For BC/FC with namespaced aliases 153 $class = (new \ReflectionClass($class))->name; 154 $this->extensions[$class] = $extension; 155 } 156 157 public function addFunction(TwigFunction $function) 158 { 159 if ($this->initialized) { 160 throw new \LogicException(sprintf('Unable to add function "%s" as extensions have already been initialized.', $function->getName())); 161 } 162 163 $this->staging->addFunction($function); 164 } 165 166 /** 167 * @return TwigFunction[] 168 */ 169 public function getFunctions(): array 170 { 171 if (!$this->initialized) { 172 $this->initExtensions(); 173 } 174 175 return $this->functions; 176 } 177 178 /** 179 * @return TwigFunction|false 180 */ 181 public function getFunction(string $name) 182 { 183 if (!$this->initialized) { 184 $this->initExtensions(); 185 } 186 187 if (isset($this->functions[$name])) { 188 return $this->functions[$name]; 189 } 190 191 foreach ($this->functions as $pattern => $function) { 192 $pattern = str_replace('\\*', '(.*?)', preg_quote($pattern, '#'), $count); 193 194 if ($count && preg_match('#^'.$pattern.'$#', $name, $matches)) { 195 array_shift($matches); 196 $function->setArguments($matches); 197 198 return $function; 199 } 200 } 201 202 foreach ($this->functionCallbacks as $callback) { 203 if (false !== $function = $callback($name)) { 204 return $function; 205 } 206 } 207 208 return false; 209 } 210 211 public function registerUndefinedFunctionCallback(callable $callable) 212 { 213 $this->functionCallbacks[] = $callable; 214 } 215 216 public function addFilter(TwigFilter $filter) 217 { 218 if ($this->initialized) { 219 throw new \LogicException(sprintf('Unable to add filter "%s" as extensions have already been initialized.', $filter->getName())); 220 } 221 222 $this->staging->addFilter($filter); 223 } 224 225 /** 226 * @return TwigFilter[] 227 */ 228 public function getFilters(): array 229 { 230 if (!$this->initialized) { 231 $this->initExtensions(); 232 } 233 234 return $this->filters; 235 } 236 237 /** 238 * @return TwigFilter|false 239 */ 240 public function getFilter(string $name) 241 { 242 if (!$this->initialized) { 243 $this->initExtensions(); 244 } 245 246 if (isset($this->filters[$name])) { 247 return $this->filters[$name]; 248 } 249 250 foreach ($this->filters as $pattern => $filter) { 251 $pattern = str_replace('\\*', '(.*?)', preg_quote($pattern, '#'), $count); 252 253 if ($count && preg_match('#^'.$pattern.'$#', $name, $matches)) { 254 array_shift($matches); 255 $filter->setArguments($matches); 256 257 return $filter; 258 } 259 } 260 261 foreach ($this->filterCallbacks as $callback) { 262 if (false !== $filter = $callback($name)) { 263 return $filter; 264 } 265 } 266 267 return false; 268 } 269 270 public function registerUndefinedFilterCallback(callable $callable) 271 { 272 $this->filterCallbacks[] = $callable; 273 } 274 275 public function addNodeVisitor(NodeVisitorInterface $visitor) 276 { 277 if ($this->initialized) { 278 throw new \LogicException('Unable to add a node visitor as extensions have already been initialized.'); 279 } 280 281 $this->staging->addNodeVisitor($visitor); 282 } 283 284 /** 285 * @return NodeVisitorInterface[] 286 */ 287 public function getNodeVisitors(): array 288 { 289 if (!$this->initialized) { 290 $this->initExtensions(); 291 } 292 293 return $this->visitors; 294 } 295 296 public function addTokenParser(TokenParserInterface $parser) 297 { 298 if ($this->initialized) { 299 throw new \LogicException('Unable to add a token parser as extensions have already been initialized.'); 300 } 301 302 $this->staging->addTokenParser($parser); 303 } 304 305 /** 306 * @return TokenParserInterface[] 307 */ 308 public function getTokenParsers(): array 309 { 310 if (!$this->initialized) { 311 $this->initExtensions(); 312 } 313 314 return $this->parsers; 315 } 316 317 public function getGlobals(): array 318 { 319 if (null !== $this->globals) { 320 return $this->globals; 321 } 322 323 $globals = []; 324 foreach ($this->extensions as $extension) { 325 if (!$extension instanceof GlobalsInterface) { 326 continue; 327 } 328 329 $extGlobals = $extension->getGlobals(); 330 if (!\is_array($extGlobals)) { 331 throw new \UnexpectedValueException(sprintf('"%s::getGlobals()" must return an array of globals.', \get_class($extension))); 332 } 333 334 $globals = array_merge($globals, $extGlobals); 335 } 336 337 if ($this->initialized) { 338 $this->globals = $globals; 339 } 340 341 return $globals; 342 } 343 344 public function addTest(TwigTest $test) 345 { 346 if ($this->initialized) { 347 throw new \LogicException(sprintf('Unable to add test "%s" as extensions have already been initialized.', $test->getName())); 348 } 349 350 $this->staging->addTest($test); 351 } 352 353 /** 354 * @return TwigTest[] 355 */ 356 public function getTests(): array 357 { 358 if (!$this->initialized) { 359 $this->initExtensions(); 360 } 361 362 return $this->tests; 363 } 364 365 /** 366 * @return TwigTest|false 367 */ 368 public function getTest(string $name) 369 { 370 if (!$this->initialized) { 371 $this->initExtensions(); 372 } 373 374 if (isset($this->tests[$name])) { 375 return $this->tests[$name]; 376 } 377 378 foreach ($this->tests as $pattern => $test) { 379 $pattern = str_replace('\\*', '(.*?)', preg_quote($pattern, '#'), $count); 380 381 if ($count) { 382 if (preg_match('#^'.$pattern.'$#', $name, $matches)) { 383 array_shift($matches); 384 $test->setArguments($matches); 385 386 return $test; 387 } 388 } 389 } 390 391 return false; 392 } 393 394 public function getUnaryOperators(): array 395 { 396 if (!$this->initialized) { 397 $this->initExtensions(); 398 } 399 400 return $this->unaryOperators; 401 } 402 403 public function getBinaryOperators(): array 404 { 405 if (!$this->initialized) { 406 $this->initExtensions(); 407 } 408 409 return $this->binaryOperators; 410 } 411 412 private function initExtensions() 413 { 414 $this->parsers = []; 415 $this->filters = []; 416 $this->functions = []; 417 $this->tests = []; 418 $this->visitors = []; 419 $this->unaryOperators = []; 420 $this->binaryOperators = []; 421 422 foreach ($this->extensions as $extension) { 423 $this->initExtension($extension); 424 } 425 $this->initExtension($this->staging); 426 // Done at the end only, so that an exception during initialization does not mark the environment as initialized when catching the exception 427 $this->initialized = true; 428 } 429 430 private function initExtension(ExtensionInterface $extension) 431 { 432 // filters 433 foreach ($extension->getFilters() as $filter) { 434 $this->filters[$filter->getName()] = $filter; 435 } 436 437 // functions 438 foreach ($extension->getFunctions() as $function) { 439 $this->functions[$function->getName()] = $function; 440 } 441 442 // tests 443 foreach ($extension->getTests() as $test) { 444 $this->tests[$test->getName()] = $test; 445 } 446 447 // token parsers 448 foreach ($extension->getTokenParsers() as $parser) { 449 if (!$parser instanceof TokenParserInterface) { 450 throw new \LogicException('getTokenParsers() must return an array of \Twig\TokenParser\TokenParserInterface.'); 451 } 452 453 $this->parsers[] = $parser; 454 } 455 456 // node visitors 457 foreach ($extension->getNodeVisitors() as $visitor) { 458 $this->visitors[] = $visitor; 459 } 460 461 // operators 462 if ($operators = $extension->getOperators()) { 463 if (!\is_array($operators)) { 464 throw new \InvalidArgumentException(sprintf('"%s::getOperators()" must return an array with operators, got "%s".', \get_class($extension), \is_object($operators) ? \get_class($operators) : \gettype($operators).(\is_resource($operators) ? '' : '#'.$operators))); 465 } 466 467 if (2 !== \count($operators)) { 468 throw new \InvalidArgumentException(sprintf('"%s::getOperators()" must return an array of 2 elements, got %d.', \get_class($extension), \count($operators))); 469 } 470 471 $this->unaryOperators = array_merge($this->unaryOperators, $operators[0]); 472 $this->binaryOperators = array_merge($this->binaryOperators, $operators[1]); 473 } 474 } 475} 476 477class_alias('Twig\ExtensionSet', 'Twig_ExtensionSet'); 478