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

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 (0 !== $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 (2 !== \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. }