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

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