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

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