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

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