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

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\AbstractExpression;
  16. use Twig\NodeVisitor\NodeVisitorInterface;
  17. use Twig\TokenParser\TokenParserInterface;
  18. /**
  19.  * @author Fabien Potencier <fabien@symfony.com>
  20.  *
  21.  * @internal
  22.  */
  23. final class ExtensionSet
  24. {
  25.     private $extensions;
  26.     private $initialized false;
  27.     private $runtimeInitialized false;
  28.     private $staging;
  29.     private $parsers;
  30.     private $visitors;
  31.     /** @var array<string, TwigFilter> */
  32.     private $filters;
  33.     /** @var array<string, TwigFilter> */
  34.     private $dynamicFilters;
  35.     /** @var array<string, TwigTest> */
  36.     private $tests;
  37.     /** @var array<string, TwigTest> */
  38.     private $dynamicTests;
  39.     /** @var array<string, TwigFunction> */
  40.     private $functions;
  41.     /** @var array<string, TwigFunction> */
  42.     private $dynamicFunctions;
  43.     /** @var array<string, array{precedence: int, class: class-string<AbstractExpression>}> */
  44.     private $unaryOperators;
  45.     /** @var array<string, array{precedence: int, class?: class-string<AbstractExpression>, associativity: ExpressionParser::OPERATOR_*}> */
  46.     private $binaryOperators;
  47.     /** @var array<string, mixed>|null */
  48.     private $globals;
  49.     private $functionCallbacks = [];
  50.     private $filterCallbacks = [];
  51.     private $parserCallbacks = [];
  52.     private $lastModified 0;
  53.     public function __construct()
  54.     {
  55.         $this->staging = new StagingExtension();
  56.     }
  57.     public function initRuntime()
  58.     {
  59.         $this->runtimeInitialized true;
  60.     }
  61.     public function hasExtension(string $class): bool
  62.     {
  63.         return isset($this->extensions[ltrim($class'\\')]);
  64.     }
  65.     public function getExtension(string $class): ExtensionInterface
  66.     {
  67.         $class ltrim($class'\\');
  68.         if (!isset($this->extensions[$class])) {
  69.             throw new RuntimeError(\sprintf('The "%s" extension is not enabled.'$class));
  70.         }
  71.         return $this->extensions[$class];
  72.     }
  73.     /**
  74.      * @param ExtensionInterface[] $extensions
  75.      */
  76.     public function setExtensions(array $extensions): void
  77.     {
  78.         foreach ($extensions as $extension) {
  79.             $this->addExtension($extension);
  80.         }
  81.     }
  82.     /**
  83.      * @return ExtensionInterface[]
  84.      */
  85.     public function getExtensions(): array
  86.     {
  87.         return $this->extensions;
  88.     }
  89.     public function getSignature(): string
  90.     {
  91.         return json_encode(array_keys($this->extensions));
  92.     }
  93.     public function isInitialized(): bool
  94.     {
  95.         return $this->initialized || $this->runtimeInitialized;
  96.     }
  97.     public function getLastModified(): int
  98.     {
  99.         if (!== $this->lastModified) {
  100.             return $this->lastModified;
  101.         }
  102.         foreach ($this->extensions as $extension) {
  103.             $r = new \ReflectionObject($extension);
  104.             if (is_file($r->getFileName()) && ($extensionTime filemtime($r->getFileName())) > $this->lastModified) {
  105.                 $this->lastModified $extensionTime;
  106.             }
  107.         }
  108.         return $this->lastModified;
  109.     }
  110.     public function addExtension(ExtensionInterface $extension): void
  111.     {
  112.         $class \get_class($extension);
  113.         if ($this->initialized) {
  114.             throw new \LogicException(\sprintf('Unable to register extension "%s" as extensions have already been initialized.'$class));
  115.         }
  116.         if (isset($this->extensions[$class])) {
  117.             throw new \LogicException(\sprintf('Unable to register extension "%s" as it is already registered.'$class));
  118.         }
  119.         $this->extensions[$class] = $extension;
  120.     }
  121.     public function addFunction(TwigFunction $function): void
  122.     {
  123.         if ($this->initialized) {
  124.             throw new \LogicException(\sprintf('Unable to add function "%s" as extensions have already been initialized.'$function->getName()));
  125.         }
  126.         $this->staging->addFunction($function);
  127.     }
  128.     /**
  129.      * @return TwigFunction[]
  130.      */
  131.     public function getFunctions(): array
  132.     {
  133.         if (!$this->initialized) {
  134.             $this->initExtensions();
  135.         }
  136.         return $this->functions;
  137.     }
  138.     public function getFunction(string $name): ?TwigFunction
  139.     {
  140.         if (!$this->initialized) {
  141.             $this->initExtensions();
  142.         }
  143.         if (isset($this->functions[$name])) {
  144.             return $this->functions[$name];
  145.         }
  146.         foreach ($this->dynamicFunctions as $pattern => $function) {
  147.             if (preg_match($pattern$name$matches)) {
  148.                 array_shift($matches);
  149.                 return $function->withDynamicArguments($name$function->getName(), $matches);
  150.             }
  151.         }
  152.         foreach ($this->functionCallbacks as $callback) {
  153.             if (false !== $function $callback($name)) {
  154.                 return $function;
  155.             }
  156.         }
  157.         return null;
  158.     }
  159.     public function registerUndefinedFunctionCallback(callable $callable): void
  160.     {
  161.         $this->functionCallbacks[] = $callable;
  162.     }
  163.     public function addFilter(TwigFilter $filter): void
  164.     {
  165.         if ($this->initialized) {
  166.             throw new \LogicException(\sprintf('Unable to add filter "%s" as extensions have already been initialized.'$filter->getName()));
  167.         }
  168.         $this->staging->addFilter($filter);
  169.     }
  170.     /**
  171.      * @return TwigFilter[]
  172.      */
  173.     public function getFilters(): array
  174.     {
  175.         if (!$this->initialized) {
  176.             $this->initExtensions();
  177.         }
  178.         return $this->filters;
  179.     }
  180.     public function getFilter(string $name): ?TwigFilter
  181.     {
  182.         if (!$this->initialized) {
  183.             $this->initExtensions();
  184.         }
  185.         if (isset($this->filters[$name])) {
  186.             return $this->filters[$name];
  187.         }
  188.         foreach ($this->dynamicFilters as $pattern => $filter) {
  189.             if (preg_match($pattern$name$matches)) {
  190.                 array_shift($matches);
  191.                 return $filter->withDynamicArguments($name$filter->getName(), $matches);
  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.             $globals array_merge($globals$extension->getGlobals());
  272.         }
  273.         if ($this->initialized) {
  274.             $this->globals $globals;
  275.         }
  276.         return $globals;
  277.     }
  278.     public function resetGlobals(): void
  279.     {
  280.         $this->globals null;
  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->dynamicTests as $pattern => $test) {
  308.             if (preg_match($pattern$name$matches)) {
  309.                 array_shift($matches);
  310.                 return $test->withDynamicArguments($name$test->getName(), $matches);
  311.             }
  312.         }
  313.         return null;
  314.     }
  315.     /**
  316.      * @return array<string, array{precedence: int, class: class-string<AbstractExpression>}>
  317.      */
  318.     public function getUnaryOperators(): array
  319.     {
  320.         if (!$this->initialized) {
  321.             $this->initExtensions();
  322.         }
  323.         return $this->unaryOperators;
  324.     }
  325.     /**
  326.      * @return array<string, array{precedence: int, class?: class-string<AbstractExpression>, associativity: ExpressionParser::OPERATOR_*}>
  327.      */
  328.     public function getBinaryOperators(): array
  329.     {
  330.         if (!$this->initialized) {
  331.             $this->initExtensions();
  332.         }
  333.         return $this->binaryOperators;
  334.     }
  335.     private function initExtensions(): void
  336.     {
  337.         $this->parsers = [];
  338.         $this->filters = [];
  339.         $this->functions = [];
  340.         $this->tests = [];
  341.         $this->dynamicFilters = [];
  342.         $this->dynamicFunctions = [];
  343.         $this->dynamicTests = [];
  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[$name $filter->getName()] = $filter;
  359.             if (str_contains($name'*')) {
  360.                 $this->dynamicFilters['#^'.str_replace('\\*''(.*?)'preg_quote($name'#')).'$#'] = $filter;
  361.             }
  362.         }
  363.         // functions
  364.         foreach ($extension->getFunctions() as $function) {
  365.             $this->functions[$name $function->getName()] = $function;
  366.             if (str_contains($name'*')) {
  367.                 $this->dynamicFunctions['#^'.str_replace('\\*''(.*?)'preg_quote($name'#')).'$#'] = $function;
  368.             }
  369.         }
  370.         // tests
  371.         foreach ($extension->getTests() as $test) {
  372.             $this->tests[$name $test->getName()] = $test;
  373.             if (str_contains($name'*')) {
  374.                 $this->dynamicTests['#^'.str_replace('\\*''(.*?)'preg_quote($name'#')).'$#'] = $test;
  375.             }
  376.         }
  377.         // token parsers
  378.         foreach ($extension->getTokenParsers() as $parser) {
  379.             if (!$parser instanceof TokenParserInterface) {
  380.                 throw new \LogicException('getTokenParsers() must return an array of \Twig\TokenParser\TokenParserInterface.');
  381.             }
  382.             $this->parsers[$parser->getTag()] = $parser;
  383.         }
  384.         // node visitors
  385.         foreach ($extension->getNodeVisitors() as $visitor) {
  386.             $this->visitors[] = $visitor;
  387.         }
  388.         // operators
  389.         if ($operators $extension->getOperators()) {
  390.             if (!\is_array($operators)) {
  391.                 throw new \InvalidArgumentException(\sprintf('"%s::getOperators()" must return an array with operators, got "%s".'\get_class($extension), get_debug_type($operators).(\is_resource($operators) ? '' '#'.$operators)));
  392.             }
  393.             if (!== \count($operators)) {
  394.                 throw new \InvalidArgumentException(\sprintf('"%s::getOperators()" must return an array of 2 elements, got %d.'\get_class($extension), \count($operators)));
  395.             }
  396.             $this->unaryOperators array_merge($this->unaryOperators$operators[0]);
  397.             $this->binaryOperators array_merge($this->binaryOperators$operators[1]);
  398.         }
  399.     }
  400. }