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

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