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

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