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

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