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

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