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

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