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

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\ExpressionParser\ExpressionParsers;
  13. use Twig\ExpressionParser\Infix\BinaryOperatorExpressionParser;
  14. use Twig\ExpressionParser\InfixAssociativity;
  15. use Twig\ExpressionParser\InfixExpressionParserInterface;
  16. use Twig\ExpressionParser\PrecedenceChange;
  17. use Twig\ExpressionParser\Prefix\UnaryOperatorExpressionParser;
  18. use Twig\Extension\AttributeExtension;
  19. use Twig\Extension\ExtensionInterface;
  20. use Twig\Extension\GlobalsInterface;
  21. use Twig\Extension\LastModifiedExtensionInterface;
  22. use Twig\Extension\StagingExtension;
  23. use Twig\Node\Expression\AbstractExpression;
  24. use Twig\NodeVisitor\NodeVisitorInterface;
  25. use Twig\TokenParser\TokenParserInterface;
  26. // Help opcache.preload discover always-needed symbols
  27. // @see https://github.com/php/php-src/issues/10131
  28. class_exists(BinaryOperatorExpressionParser::class);
  29. /**
  30.  * @author Fabien Potencier <fabien@symfony.com>
  31.  *
  32.  * @internal
  33.  */
  34. final class ExtensionSet
  35. {
  36.     private $extensions;
  37.     private $initialized false;
  38.     private $runtimeInitialized false;
  39.     private $staging;
  40.     private $parsers;
  41.     private $visitors;
  42.     /** @var array<string, TwigFilter> */
  43.     private $filters;
  44.     /** @var array<string, TwigFilter> */
  45.     private $dynamicFilters;
  46.     /** @var array<string, TwigTest> */
  47.     private $tests;
  48.     /** @var array<string, TwigTest> */
  49.     private $dynamicTests;
  50.     /** @var array<string, TwigFunction> */
  51.     private $functions;
  52.     /** @var array<string, TwigFunction> */
  53.     private $dynamicFunctions;
  54.     private ExpressionParsers $expressionParsers;
  55.     /** @var array<string, mixed>|null */
  56.     private $globals;
  57.     /** @var array<callable(string): (TwigFunction|false)> */
  58.     private $functionCallbacks = [];
  59.     /** @var array<callable(string): (TwigFilter|false)> */
  60.     private $filterCallbacks = [];
  61.     /** @var array<callable(string): (TwigTest|false)> */
  62.     private $testCallbacks = [];
  63.     /** @var array<callable(string): (TokenParserInterface|false)> */
  64.     private $parserCallbacks = [];
  65.     private $lastModified 0;
  66.     public function __construct()
  67.     {
  68.         $this->staging = new StagingExtension();
  69.     }
  70.     /**
  71.      * @return void
  72.      */
  73.     public function initRuntime()
  74.     {
  75.         $this->runtimeInitialized true;
  76.     }
  77.     public function hasExtension(string $class): bool
  78.     {
  79.         return isset($this->extensions[ltrim($class'\\')]);
  80.     }
  81.     public function getExtension(string $class): ExtensionInterface
  82.     {
  83.         $class ltrim($class'\\');
  84.         if (!isset($this->extensions[$class])) {
  85.             throw new RuntimeError(\sprintf('The "%s" extension is not enabled.'$class));
  86.         }
  87.         return $this->extensions[$class];
  88.     }
  89.     /**
  90.      * @param ExtensionInterface[] $extensions
  91.      */
  92.     public function setExtensions(array $extensions): void
  93.     {
  94.         foreach ($extensions as $extension) {
  95.             $this->addExtension($extension);
  96.         }
  97.     }
  98.     /**
  99.      * @return ExtensionInterface[]
  100.      */
  101.     public function getExtensions(): array
  102.     {
  103.         return $this->extensions;
  104.     }
  105.     public function getSignature(): string
  106.     {
  107.         return json_encode(array_keys($this->extensions));
  108.     }
  109.     public function isInitialized(): bool
  110.     {
  111.         return $this->initialized || $this->runtimeInitialized;
  112.     }
  113.     public function getLastModified(): int
  114.     {
  115.         if (!== $this->lastModified) {
  116.             return $this->lastModified;
  117.         }
  118.         $lastModified 0;
  119.         foreach ($this->extensions as $extension) {
  120.             if ($extension instanceof LastModifiedExtensionInterface) {
  121.                 $lastModified max($extension->getLastModified(), $lastModified);
  122.             } else {
  123.                 $r = new \ReflectionObject($extension);
  124.                 if (is_file($r->getFileName())) {
  125.                     $lastModified max(filemtime($r->getFileName()), $lastModified);
  126.                 }
  127.             }
  128.         }
  129.         return $this->lastModified $lastModified;
  130.     }
  131.     public function addExtension(ExtensionInterface $extension): void
  132.     {
  133.         if ($extension instanceof AttributeExtension) {
  134.             $class $extension->getClass();
  135.         } else {
  136.             $class $extension::class;
  137.         }
  138.         if ($this->initialized) {
  139.             throw new \LogicException(\sprintf('Unable to register extension "%s" as extensions have already been initialized.'$class));
  140.         }
  141.         if (isset($this->extensions[$class])) {
  142.             throw new \LogicException(\sprintf('Unable to register extension "%s" as it is already registered.'$class));
  143.         }
  144.         $this->extensions[$class] = $extension;
  145.     }
  146.     public function addFunction(TwigFunction $function): void
  147.     {
  148.         if ($this->initialized) {
  149.             throw new \LogicException(\sprintf('Unable to add function "%s" as extensions have already been initialized.'$function->getName()));
  150.         }
  151.         $this->staging->addFunction($function);
  152.     }
  153.     /**
  154.      * @return TwigFunction[]
  155.      */
  156.     public function getFunctions(): array
  157.     {
  158.         if (!$this->initialized) {
  159.             $this->initExtensions();
  160.         }
  161.         return $this->functions;
  162.     }
  163.     public function getFunction(string $name): ?TwigFunction
  164.     {
  165.         if (!$this->initialized) {
  166.             $this->initExtensions();
  167.         }
  168.         if (isset($this->functions[$name])) {
  169.             return $this->functions[$name];
  170.         }
  171.         foreach ($this->dynamicFunctions as $pattern => $function) {
  172.             if (preg_match($pattern$name$matches)) {
  173.                 array_shift($matches);
  174.                 return $function->withDynamicArguments($name$function->getName(), $matches);
  175.             }
  176.         }
  177.         foreach ($this->functionCallbacks as $callback) {
  178.             if (false !== $function $callback($name)) {
  179.                 return $function;
  180.             }
  181.         }
  182.         return null;
  183.     }
  184.     /**
  185.      * @param callable(string): (TwigFunction|false) $callable
  186.      */
  187.     public function registerUndefinedFunctionCallback(callable $callable): void
  188.     {
  189.         $this->functionCallbacks[] = $callable;
  190.     }
  191.     public function addFilter(TwigFilter $filter): void
  192.     {
  193.         if ($this->initialized) {
  194.             throw new \LogicException(\sprintf('Unable to add filter "%s" as extensions have already been initialized.'$filter->getName()));
  195.         }
  196.         $this->staging->addFilter($filter);
  197.     }
  198.     /**
  199.      * @return TwigFilter[]
  200.      */
  201.     public function getFilters(): array
  202.     {
  203.         if (!$this->initialized) {
  204.             $this->initExtensions();
  205.         }
  206.         return $this->filters;
  207.     }
  208.     public function getFilter(string $name): ?TwigFilter
  209.     {
  210.         if (!$this->initialized) {
  211.             $this->initExtensions();
  212.         }
  213.         if (isset($this->filters[$name])) {
  214.             return $this->filters[$name];
  215.         }
  216.         foreach ($this->dynamicFilters as $pattern => $filter) {
  217.             if (preg_match($pattern$name$matches)) {
  218.                 array_shift($matches);
  219.                 return $filter->withDynamicArguments($name$filter->getName(), $matches);
  220.             }
  221.         }
  222.         foreach ($this->filterCallbacks as $callback) {
  223.             if (false !== $filter $callback($name)) {
  224.                 return $filter;
  225.             }
  226.         }
  227.         return null;
  228.     }
  229.     /**
  230.      * @param callable(string): (TwigFilter|false) $callable
  231.      */
  232.     public function registerUndefinedFilterCallback(callable $callable): void
  233.     {
  234.         $this->filterCallbacks[] = $callable;
  235.     }
  236.     public function addNodeVisitor(NodeVisitorInterface $visitor): void
  237.     {
  238.         if ($this->initialized) {
  239.             throw new \LogicException('Unable to add a node visitor as extensions have already been initialized.');
  240.         }
  241.         $this->staging->addNodeVisitor($visitor);
  242.     }
  243.     /**
  244.      * @return NodeVisitorInterface[]
  245.      */
  246.     public function getNodeVisitors(): array
  247.     {
  248.         if (!$this->initialized) {
  249.             $this->initExtensions();
  250.         }
  251.         return $this->visitors;
  252.     }
  253.     public function addTokenParser(TokenParserInterface $parser): void
  254.     {
  255.         if ($this->initialized) {
  256.             throw new \LogicException('Unable to add a token parser as extensions have already been initialized.');
  257.         }
  258.         $this->staging->addTokenParser($parser);
  259.     }
  260.     /**
  261.      * @return TokenParserInterface[]
  262.      */
  263.     public function getTokenParsers(): array
  264.     {
  265.         if (!$this->initialized) {
  266.             $this->initExtensions();
  267.         }
  268.         return $this->parsers;
  269.     }
  270.     public function getTokenParser(string $name): ?TokenParserInterface
  271.     {
  272.         if (!$this->initialized) {
  273.             $this->initExtensions();
  274.         }
  275.         if (isset($this->parsers[$name])) {
  276.             return $this->parsers[$name];
  277.         }
  278.         foreach ($this->parserCallbacks as $callback) {
  279.             if (false !== $parser $callback($name)) {
  280.                 return $parser;
  281.             }
  282.         }
  283.         return null;
  284.     }
  285.     /**
  286.      * @param callable(string): (TokenParserInterface|false) $callable
  287.      */
  288.     public function registerUndefinedTokenParserCallback(callable $callable): void
  289.     {
  290.         $this->parserCallbacks[] = $callable;
  291.     }
  292.     /**
  293.      * @return array<string, mixed>
  294.      */
  295.     public function getGlobals(): array
  296.     {
  297.         if (null !== $this->globals) {
  298.             return $this->globals;
  299.         }
  300.         $globals = [];
  301.         foreach ($this->extensions as $extension) {
  302.             if (!$extension instanceof GlobalsInterface) {
  303.                 continue;
  304.             }
  305.             $globals array_merge($globals$extension->getGlobals());
  306.         }
  307.         if ($this->initialized) {
  308.             $this->globals $globals;
  309.         }
  310.         return $globals;
  311.     }
  312.     public function resetGlobals(): void
  313.     {
  314.         $this->globals null;
  315.     }
  316.     public function addTest(TwigTest $test): void
  317.     {
  318.         if ($this->initialized) {
  319.             throw new \LogicException(\sprintf('Unable to add test "%s" as extensions have already been initialized.'$test->getName()));
  320.         }
  321.         $this->staging->addTest($test);
  322.     }
  323.     /**
  324.      * @return TwigTest[]
  325.      */
  326.     public function getTests(): array
  327.     {
  328.         if (!$this->initialized) {
  329.             $this->initExtensions();
  330.         }
  331.         return $this->tests;
  332.     }
  333.     public function getTest(string $name): ?TwigTest
  334.     {
  335.         if (!$this->initialized) {
  336.             $this->initExtensions();
  337.         }
  338.         if (isset($this->tests[$name])) {
  339.             return $this->tests[$name];
  340.         }
  341.         foreach ($this->dynamicTests as $pattern => $test) {
  342.             if (preg_match($pattern$name$matches)) {
  343.                 array_shift($matches);
  344.                 return $test->withDynamicArguments($name$test->getName(), $matches);
  345.             }
  346.         }
  347.         foreach ($this->testCallbacks as $callback) {
  348.             if (false !== $test $callback($name)) {
  349.                 return $test;
  350.             }
  351.         }
  352.         return null;
  353.     }
  354.     /**
  355.      * @param callable(string): (TwigTest|false) $callable
  356.      */
  357.     public function registerUndefinedTestCallback(callable $callable): void
  358.     {
  359.         $this->testCallbacks[] = $callable;
  360.     }
  361.     public function getExpressionParsers(): ExpressionParsers
  362.     {
  363.         if (!$this->initialized) {
  364.             $this->initExtensions();
  365.         }
  366.         return $this->expressionParsers;
  367.     }
  368.     private function initExtensions(): void
  369.     {
  370.         $this->parsers = [];
  371.         $this->filters = [];
  372.         $this->functions = [];
  373.         $this->tests = [];
  374.         $this->dynamicFilters = [];
  375.         $this->dynamicFunctions = [];
  376.         $this->dynamicTests = [];
  377.         $this->visitors = [];
  378.         $this->expressionParsers = new ExpressionParsers();
  379.         foreach ($this->extensions as $extension) {
  380.             $this->initExtension($extension);
  381.         }
  382.         $this->initExtension($this->staging);
  383.         // Done at the end only, so that an exception during initialization does not mark the environment as initialized when catching the exception
  384.         $this->initialized true;
  385.     }
  386.     private function initExtension(ExtensionInterface $extension): void
  387.     {
  388.         // filters
  389.         foreach ($extension->getFilters() as $filter) {
  390.             $this->filters[$name $filter->getName()] = $filter;
  391.             if (str_contains($name'*')) {
  392.                 $this->dynamicFilters['#^'.str_replace('\\*''(.*?)'preg_quote($name'#')).'$#'] = $filter;
  393.             }
  394.         }
  395.         // functions
  396.         foreach ($extension->getFunctions() as $function) {
  397.             $this->functions[$name $function->getName()] = $function;
  398.             if (str_contains($name'*')) {
  399.                 $this->dynamicFunctions['#^'.str_replace('\\*''(.*?)'preg_quote($name'#')).'$#'] = $function;
  400.             }
  401.         }
  402.         // tests
  403.         foreach ($extension->getTests() as $test) {
  404.             $this->tests[$name $test->getName()] = $test;
  405.             if (str_contains($name'*')) {
  406.                 $this->dynamicTests['#^'.str_replace('\\*''(.*?)'preg_quote($name'#')).'$#'] = $test;
  407.             }
  408.         }
  409.         // token parsers
  410.         foreach ($extension->getTokenParsers() as $parser) {
  411.             if (!$parser instanceof TokenParserInterface) {
  412.                 throw new \LogicException('getTokenParsers() must return an array of \Twig\TokenParser\TokenParserInterface.');
  413.             }
  414.             $this->parsers[$parser->getTag()] = $parser;
  415.         }
  416.         // node visitors
  417.         foreach ($extension->getNodeVisitors() as $visitor) {
  418.             $this->visitors[] = $visitor;
  419.         }
  420.         // expression parsers
  421.         if (method_exists($extension'getExpressionParsers')) {
  422.             $this->expressionParsers->add($extension->getExpressionParsers());
  423.         }
  424.         $operators $extension->getOperators();
  425.         if (!\is_array($operators)) {
  426.             throw new \InvalidArgumentException(\sprintf('"%s::getOperators()" must return an array with operators, got "%s".'$extension::class, get_debug_type($operators).(\is_resource($operators) ? '' '#'.$operators)));
  427.         }
  428.         if (!== \count($operators)) {
  429.             throw new \InvalidArgumentException(\sprintf('"%s::getOperators()" must return an array of 2 elements, got %d.'$extension::class, \count($operators)));
  430.         }
  431.         $expressionParsers = [];
  432.         foreach ($operators[0] as $operator => $op) {
  433.             $expressionParsers[] = new UnaryOperatorExpressionParser($op['class'], $operator$op['precedence'], $op['precedence_change'] ?? null''$op['aliases'] ?? []);
  434.         }
  435.         foreach ($operators[1] as $operator => $op) {
  436.             $op['associativity'] = match ($op['associativity']) {
  437.                 => InfixAssociativity::Left,
  438.                 => InfixAssociativity::Right,
  439.                 default => throw new \InvalidArgumentException(\sprintf('Invalid associativity "%s" for operator "%s".'$op['associativity'], $operator)),
  440.             };
  441.             if (isset($op['callable'])) {
  442.                 $expressionParsers[] = $this->convertInfixExpressionParser($op['class'], $operator$op['precedence'], $op['associativity'], $op['precedence_change'] ?? null$op['aliases'] ?? [], $op['callable']);
  443.             } else {
  444.                 $expressionParsers[] = new BinaryOperatorExpressionParser($op['class'], $operator$op['precedence'], $op['associativity'], $op['precedence_change'] ?? null''$op['aliases'] ?? []);
  445.             }
  446.         }
  447.         if (\count($expressionParsers)) {
  448.             trigger_deprecation('twig/twig''3.21'\sprintf('Extension "%s" uses the old signature for "getOperators()", please implement "getExpressionParsers()" instead.'$extension::class));
  449.             $this->expressionParsers->add($expressionParsers);
  450.         }
  451.     }
  452.     private function convertInfixExpressionParser(string $nodeClassstring $operatorint $precedenceInfixAssociativity $associativity, ?PrecedenceChange $precedenceChange, array $aliases, callable $callable): InfixExpressionParserInterface
  453.     {
  454.         trigger_deprecation('twig/twig''3.21'\sprintf('Using a non-ExpressionParserInterface object to define the "%s" binary operator is deprecated.'$operator));
  455.         return new class($nodeClass$operator$precedence$associativity$precedenceChange$aliases$callable) extends BinaryOperatorExpressionParser {
  456.             public function __construct(
  457.                 string $nodeClass,
  458.                 string $operator,
  459.                 int $precedence,
  460.                 InfixAssociativity $associativity InfixAssociativity::Left,
  461.                 ?PrecedenceChange $precedenceChange null,
  462.                 array $aliases = [],
  463.                 private $callable null,
  464.             ) {
  465.                 parent::__construct($nodeClass$operator$precedence$associativity$precedenceChange$aliases);
  466.             }
  467.             public function parse(Parser $parserAbstractExpression $exprToken $token): AbstractExpression
  468.             {
  469.                 return ($this->callable)($parser$expr);
  470.             }
  471.         };
  472.     }
  473. }