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

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