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

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