vendor/twig/twig/src/ExtensionSet.php line 453

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of Twig.
  4.  *
  5.  * (c) Fabien Potencier
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. namespace Twig;
  11. use Twig\Error\RuntimeError;
  12. use Twig\Extension\ExtensionInterface;
  13. use Twig\Extension\GlobalsInterface;
  14. use Twig\Extension\StagingExtension;
  15. use Twig\Node\Expression\Binary\AbstractBinary;
  16. use Twig\Node\Expression\Unary\AbstractUnary;
  17. use Twig\NodeVisitor\NodeVisitorInterface;
  18. use Twig\TokenParser\TokenParserInterface;
  19. /**
  20.  * @author Fabien Potencier <fabien@symfony.com>
  21.  *
  22.  * @internal
  23.  */
  24. final class ExtensionSet
  25. {
  26.     private $extensions;
  27.     private $initialized false;
  28.     private $runtimeInitialized false;
  29.     private $staging;
  30.     private $parsers;
  31.     private $visitors;
  32.     /** @var array<string, TwigFilter> */
  33.     private $filters;
  34.     /** @var array<string, TwigTest> */
  35.     private $tests;
  36.     /** @var array<string, TwigFunction> */
  37.     private $functions;
  38.     /** @var array<string, array{precedence: int, class: class-string<AbstractUnary>}> */
  39.     private $unaryOperators;
  40.     /** @var array<string, array{precedence: int, class: class-string<AbstractBinary>, associativity: ExpressionParser::OPERATOR_*}> */
  41.     private $binaryOperators;
  42.     /** @var array<string, mixed> */
  43.     private $globals;
  44.     private $functionCallbacks = [];
  45.     private $filterCallbacks = [];
  46.     private $parserCallbacks = [];
  47.     private $lastModified 0;
  48.     public function __construct()
  49.     {
  50.         $this->staging = new StagingExtension();
  51.     }
  52.     public function initRuntime()
  53.     {
  54.         $this->runtimeInitialized true;
  55.     }
  56.     public function hasExtension(string $class): bool
  57.     {
  58.         return isset($this->extensions[ltrim($class'\\')]);
  59.     }
  60.     public function getExtension(string $class): ExtensionInterface
  61.     {
  62.         $class ltrim($class'\\');
  63.         if (!isset($this->extensions[$class])) {
  64.             throw new RuntimeError(sprintf('The "%s" extension is not enabled.'$class));
  65.         }
  66.         return $this->extensions[$class];
  67.     }
  68.     /**
  69.      * @param ExtensionInterface[] $extensions
  70.      */
  71.     public function setExtensions(array $extensions): void
  72.     {
  73.         foreach ($extensions as $extension) {
  74.             $this->addExtension($extension);
  75.         }
  76.     }
  77.     /**
  78.      * @return ExtensionInterface[]
  79.      */
  80.     public function getExtensions(): array
  81.     {
  82.         return $this->extensions;
  83.     }
  84.     public function getSignature(): string
  85.     {
  86.         return json_encode(array_keys($this->extensions));
  87.     }
  88.     public function isInitialized(): bool
  89.     {
  90.         return $this->initialized || $this->runtimeInitialized;
  91.     }
  92.     public function getLastModified(): int
  93.     {
  94.         if (!== $this->lastModified) {
  95.             return $this->lastModified;
  96.         }
  97.         foreach ($this->extensions as $extension) {
  98.             $r = new \ReflectionObject($extension);
  99.             if (is_file($r->getFileName()) && ($extensionTime filemtime($r->getFileName())) > $this->lastModified) {
  100.                 $this->lastModified $extensionTime;
  101.             }
  102.         }
  103.         return $this->lastModified;
  104.     }
  105.     public function addExtension(ExtensionInterface $extension): void
  106.     {
  107.         $class \get_class($extension);
  108.         if ($this->initialized) {
  109.             throw new \LogicException(sprintf('Unable to register extension "%s" as extensions have already been initialized.'$class));
  110.         }
  111.         if (isset($this->extensions[$class])) {
  112.             throw new \LogicException(sprintf('Unable to register extension "%s" as it is already registered.'$class));
  113.         }
  114.         $this->extensions[$class] = $extension;
  115.     }
  116.     public function addFunction(TwigFunction $function): void
  117.     {
  118.         if ($this->initialized) {
  119.             throw new \LogicException(sprintf('Unable to add function "%s" as extensions have already been initialized.'$function->getName()));
  120.         }
  121.         $this->staging->addFunction($function);
  122.     }
  123.     /**
  124.      * @return TwigFunction[]
  125.      */
  126.     public function getFunctions(): array
  127.     {
  128.         if (!$this->initialized) {
  129.             $this->initExtensions();
  130.         }
  131.         return $this->functions;
  132.     }
  133.     public function getFunction(string $name): ?TwigFunction
  134.     {
  135.         if (!$this->initialized) {
  136.             $this->initExtensions();
  137.         }
  138.         if (isset($this->functions[$name])) {
  139.             return $this->functions[$name];
  140.         }
  141.         foreach ($this->functions as $pattern => $function) {
  142.             $pattern str_replace('\\*''(.*?)'preg_quote($pattern'#'), $count);
  143.             if ($count && preg_match('#^'.$pattern.'$#'$name$matches)) {
  144.                 array_shift($matches);
  145.                 $function->setArguments($matches);
  146.                 return $function;
  147.             }
  148.         }
  149.         foreach ($this->functionCallbacks as $callback) {
  150.             if (false !== $function $callback($name)) {
  151.                 return $function;
  152.             }
  153.         }
  154.         return null;
  155.     }
  156.     public function registerUndefinedFunctionCallback(callable $callable): void
  157.     {
  158.         $this->functionCallbacks[] = $callable;
  159.     }
  160.     public function addFilter(TwigFilter $filter): void
  161.     {
  162.         if ($this->initialized) {
  163.             throw new \LogicException(sprintf('Unable to add filter "%s" as extensions have already been initialized.'$filter->getName()));
  164.         }
  165.         $this->staging->addFilter($filter);
  166.     }
  167.     /**
  168.      * @return TwigFilter[]
  169.      */
  170.     public function getFilters(): array
  171.     {
  172.         if (!$this->initialized) {
  173.             $this->initExtensions();
  174.         }
  175.         return $this->filters;
  176.     }
  177.     public function getFilter(string $name): ?TwigFilter
  178.     {
  179.         if (!$this->initialized) {
  180.             $this->initExtensions();
  181.         }
  182.         if (isset($this->filters[$name])) {
  183.             return $this->filters[$name];
  184.         }
  185.         foreach ($this->filters as $pattern => $filter) {
  186.             $pattern str_replace('\\*''(.*?)'preg_quote($pattern'#'), $count);
  187.             if ($count && preg_match('#^'.$pattern.'$#'$name$matches)) {
  188.                 array_shift($matches);
  189.                 $filter->setArguments($matches);
  190.                 return $filter;
  191.             }
  192.         }
  193.         foreach ($this->filterCallbacks as $callback) {
  194.             if (false !== $filter $callback($name)) {
  195.                 return $filter;
  196.             }
  197.         }
  198.         return null;
  199.     }
  200.     public function registerUndefinedFilterCallback(callable $callable): void
  201.     {
  202.         $this->filterCallbacks[] = $callable;
  203.     }
  204.     public function addNodeVisitor(NodeVisitorInterface $visitor): void
  205.     {
  206.         if ($this->initialized) {
  207.             throw new \LogicException('Unable to add a node visitor as extensions have already been initialized.');
  208.         }
  209.         $this->staging->addNodeVisitor($visitor);
  210.     }
  211.     /**
  212.      * @return NodeVisitorInterface[]
  213.      */
  214.     public function getNodeVisitors(): array
  215.     {
  216.         if (!$this->initialized) {
  217.             $this->initExtensions();
  218.         }
  219.         return $this->visitors;
  220.     }
  221.     public function addTokenParser(TokenParserInterface $parser): void
  222.     {
  223.         if ($this->initialized) {
  224.             throw new \LogicException('Unable to add a token parser as extensions have already been initialized.');
  225.         }
  226.         $this->staging->addTokenParser($parser);
  227.     }
  228.     /**
  229.      * @return TokenParserInterface[]
  230.      */
  231.     public function getTokenParsers(): array
  232.     {
  233.         if (!$this->initialized) {
  234.             $this->initExtensions();
  235.         }
  236.         return $this->parsers;
  237.     }
  238.     public function getTokenParser(string $name): ?TokenParserInterface
  239.     {
  240.         if (!$this->initialized) {
  241.             $this->initExtensions();
  242.         }
  243.         if (isset($this->parsers[$name])) {
  244.             return $this->parsers[$name];
  245.         }
  246.         foreach ($this->parserCallbacks as $callback) {
  247.             if (false !== $parser $callback($name)) {
  248.                 return $parser;
  249.             }
  250.         }
  251.         return null;
  252.     }
  253.     public function registerUndefinedTokenParserCallback(callable $callable): void
  254.     {
  255.         $this->parserCallbacks[] = $callable;
  256.     }
  257.     /**
  258.      * @return array<string, mixed>
  259.      */
  260.     public function getGlobals(): array
  261.     {
  262.         if (null !== $this->globals) {
  263.             return $this->globals;
  264.         }
  265.         $globals = [];
  266.         foreach ($this->extensions as $extension) {
  267.             if (!$extension instanceof GlobalsInterface) {
  268.                 continue;
  269.             }
  270.             $extGlobals $extension->getGlobals();
  271.             if (!\is_array($extGlobals)) {
  272.                 throw new \UnexpectedValueException(sprintf('"%s::getGlobals()" must return an array of globals.'\get_class($extension)));
  273.             }
  274.             $globals array_merge($globals$extGlobals);
  275.         }
  276.         if ($this->initialized) {
  277.             $this->globals $globals;
  278.         }
  279.         return $globals;
  280.     }
  281.     public function addTest(TwigTest $test): void
  282.     {
  283.         if ($this->initialized) {
  284.             throw new \LogicException(sprintf('Unable to add test "%s" as extensions have already been initialized.'$test->getName()));
  285.         }
  286.         $this->staging->addTest($test);
  287.     }
  288.     /**
  289.      * @return TwigTest[]
  290.      */
  291.     public function getTests(): array
  292.     {
  293.         if (!$this->initialized) {
  294.             $this->initExtensions();
  295.         }
  296.         return $this->tests;
  297.     }
  298.     public function getTest(string $name): ?TwigTest
  299.     {
  300.         if (!$this->initialized) {
  301.             $this->initExtensions();
  302.         }
  303.         if (isset($this->tests[$name])) {
  304.             return $this->tests[$name];
  305.         }
  306.         foreach ($this->tests as $pattern => $test) {
  307.             $pattern str_replace('\\*''(.*?)'preg_quote($pattern'#'), $count);
  308.             if ($count) {
  309.                 if (preg_match('#^'.$pattern.'$#'$name$matches)) {
  310.                     array_shift($matches);
  311.                     $test->setArguments($matches);
  312.                     return $test;
  313.                 }
  314.             }
  315.         }
  316.         return null;
  317.     }
  318.     /**
  319.      * @return array<string, array{precedence: int, class: class-string<AbstractUnary>}>
  320.      */
  321.     public function getUnaryOperators(): array
  322.     {
  323.         if (!$this->initialized) {
  324.             $this->initExtensions();
  325.         }
  326.         return $this->unaryOperators;
  327.     }
  328.     /**
  329.      * @return array<string, array{precedence: int, class: class-string<AbstractBinary>, associativity: ExpressionParser::OPERATOR_*}>
  330.      */
  331.     public function getBinaryOperators(): array
  332.     {
  333.         if (!$this->initialized) {
  334.             $this->initExtensions();
  335.         }
  336.         return $this->binaryOperators;
  337.     }
  338.     private function initExtensions(): void
  339.     {
  340.         $this->parsers = [];
  341.         $this->filters = [];
  342.         $this->functions = [];
  343.         $this->tests = [];
  344.         $this->visitors = [];
  345.         $this->unaryOperators = [];
  346.         $this->binaryOperators = [];
  347.         foreach ($this->extensions as $extension) {
  348.             $this->initExtension($extension);
  349.         }
  350.         $this->initExtension($this->staging);
  351.         // Done at the end only, so that an exception during initialization does not mark the environment as initialized when catching the exception
  352.         $this->initialized true;
  353.     }
  354.     private function initExtension(ExtensionInterface $extension): void
  355.     {
  356.         // filters
  357.         foreach ($extension->getFilters() as $filter) {
  358.             $this->filters[$filter->getName()] = $filter;
  359.         }
  360.         // functions
  361.         foreach ($extension->getFunctions() as $function) {
  362.             $this->functions[$function->getName()] = $function;
  363.         }
  364.         // tests
  365.         foreach ($extension->getTests() as $test) {
  366.             $this->tests[$test->getName()] = $test;
  367.         }
  368.         // token parsers
  369.         foreach ($extension->getTokenParsers() as $parser) {
  370.             if (!$parser instanceof TokenParserInterface) {
  371.                 throw new \LogicException('getTokenParsers() must return an array of \Twig\TokenParser\TokenParserInterface.');
  372.             }
  373.             $this->parsers[$parser->getTag()] = $parser;
  374.         }
  375.         // node visitors
  376.         foreach ($extension->getNodeVisitors() as $visitor) {
  377.             $this->visitors[] = $visitor;
  378.         }
  379.         // operators
  380.         if ($operators $extension->getOperators()) {
  381.             if (!\is_array($operators)) {
  382.                 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)));
  383.             }
  384.             if (!== \count($operators)) {
  385.                 throw new \InvalidArgumentException(sprintf('"%s::getOperators()" must return an array of 2 elements, got %d.'\get_class($extension), \count($operators)));
  386.             }
  387.             $this->unaryOperators array_merge($this->unaryOperators$operators[0]);
  388.             $this->binaryOperators array_merge($this->binaryOperators$operators[1]);
  389.         }
  390.     }
  391. }