vendor/twig/twig/src/Environment.php line 384

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\Cache\CacheInterface;
  12. use Twig\Cache\FilesystemCache;
  13. use Twig\Cache\NullCache;
  14. use Twig\Cache\RemovableCacheInterface;
  15. use Twig\Error\Error;
  16. use Twig\Error\LoaderError;
  17. use Twig\Error\RuntimeError;
  18. use Twig\Error\SyntaxError;
  19. use Twig\ExpressionParser\ExpressionParsers;
  20. use Twig\Extension\CoreExtension;
  21. use Twig\Extension\EscaperExtension;
  22. use Twig\Extension\ExtensionInterface;
  23. use Twig\Extension\OptimizerExtension;
  24. use Twig\Extension\YieldNotReadyExtension;
  25. use Twig\Loader\ArrayLoader;
  26. use Twig\Loader\ChainLoader;
  27. use Twig\Loader\LoaderInterface;
  28. use Twig\Node\ModuleNode;
  29. use Twig\Node\Node;
  30. use Twig\NodeVisitor\NodeVisitorInterface;
  31. use Twig\Runtime\EscaperRuntime;
  32. use Twig\RuntimeLoader\FactoryRuntimeLoader;
  33. use Twig\RuntimeLoader\RuntimeLoaderInterface;
  34. use Twig\TokenParser\TokenParserInterface;
  35. /**
  36. * Stores the Twig configuration and renders templates.
  37. *
  38. * @author Fabien Potencier <fabien@symfony.com>
  39. */
  40. class Environment
  41. {
  42. public const VERSION = '3.30.0';
  43. public const VERSION_ID = 33000;
  44. public const MAJOR_VERSION = 3;
  45. public const MINOR_VERSION = 30;
  46. public const RELEASE_VERSION = 0;
  47. public const EXTRA_VERSION = '';
  48. private $charset;
  49. private $loader;
  50. private $debug;
  51. private $autoReload;
  52. private $cache;
  53. private $lexer;
  54. private $parser;
  55. private $compiler;
  56. /** @var array<string, mixed> */
  57. private $globals = [];
  58. private $resolvedGlobals;
  59. private $loadedTemplates;
  60. private $strictVariables;
  61. private $originalCache;
  62. private $extensionSet;
  63. private $runtimeLoaders = [];
  64. private $runtimes = [];
  65. private $optionsHash;
  66. /** @var bool */
  67. private $useYield;
  68. private $defaultRuntimeLoader;
  69. private array $hotCache = [];
  70. /**
  71. * @var array<string, TemplateWrapper>
  72. */
  73. private array $loadedWrappers = [];
  74. /**
  75. * Constructor.
  76. *
  77. * Available options:
  78. *
  79. * * debug: When set to true, it automatically set "auto_reload" to true as
  80. * well (default to false).
  81. *
  82. * * charset: The charset used by the templates (default to UTF-8).
  83. *
  84. * * cache: An absolute path where to store the compiled templates,
  85. * a \Twig\Cache\CacheInterface implementation,
  86. * or false to disable compilation cache (default).
  87. *
  88. * * auto_reload: Whether to reload the template if the original source changed.
  89. * If you don't provide the auto_reload option, it will be
  90. * determined automatically based on the debug value.
  91. *
  92. * * strict_variables: Whether to ignore invalid variables in templates
  93. * (default to false).
  94. *
  95. * * autoescape: Whether to enable auto-escaping (default to html):
  96. * * false: disable auto-escaping
  97. * * html, js: set the autoescaping to one of the supported strategies
  98. * * name: set the autoescaping strategy based on the template name extension
  99. * * PHP callback: a PHP callback that returns an escaping strategy based on the template "name"
  100. *
  101. * * optimizations: A flag that indicates which optimizations to apply
  102. * (default to -1 which means that all optimizations are enabled;
  103. * set it to 0 to disable).
  104. *
  105. * * use_yield: true: forces templates to exclusively use "yield" instead of "echo" (all extensions must be yield ready)
  106. * false (default): allows templates to use a mix of "yield" and "echo" calls to allow for a progressive migration
  107. * Switch to "true" when possible as this will be the only supported mode in Twig 4.0
  108. */
  109. public function __construct(LoaderInterface $loader, array $options = [])
  110. {
  111. $this->setLoader($loader);
  112. $options = array_merge([
  113. 'debug' => false,
  114. 'charset' => 'UTF-8',
  115. 'strict_variables' => false,
  116. 'autoescape' => 'html',
  117. 'cache' => false,
  118. 'auto_reload' => null,
  119. 'optimizations' => -1,
  120. 'use_yield' => false,
  121. ], $options);
  122. $this->useYield = (bool) $options['use_yield'];
  123. $this->debug = (bool) $options['debug'];
  124. $this->setCharset($options['charset'] ?? 'UTF-8');
  125. $this->autoReload = null === $options['auto_reload'] ? $this->debug : (bool) $options['auto_reload'];
  126. $this->strictVariables = (bool) $options['strict_variables'];
  127. $this->setCache($options['cache']);
  128. $this->extensionSet = new ExtensionSet();
  129. $this->defaultRuntimeLoader = new FactoryRuntimeLoader([
  130. EscaperRuntime::class => function () { return new EscaperRuntime($this->charset); },
  131. ]);
  132. $this->addExtension(new CoreExtension());
  133. $escaperExt = new EscaperExtension($options['autoescape']);
  134. $escaperExt->setEnvironment($this, false);
  135. $this->addExtension($escaperExt);
  136. if (\PHP_VERSION_ID >= 80000) {
  137. $this->addExtension(new YieldNotReadyExtension($this->useYield));
  138. }
  139. $this->addExtension(new OptimizerExtension($options['optimizations']));
  140. }
  141. public function __clone()
  142. {
  143. trigger_deprecation('twig/twig', '3.30', 'Cloning a "%s" instance is deprecated and will throw in Twig 4.0; build a new environment instead.', self::class);
  144. }
  145. /**
  146. * @internal
  147. */
  148. public function useYield(): bool
  149. {
  150. return $this->useYield;
  151. }
  152. /**
  153. * Enables debugging mode.
  154. *
  155. * @return void
  156. */
  157. public function enableDebug()
  158. {
  159. $this->debug = true;
  160. $this->optionsHash = null;
  161. }
  162. /**
  163. * Disables debugging mode.
  164. *
  165. * @return void
  166. */
  167. public function disableDebug()
  168. {
  169. $this->debug = false;
  170. $this->optionsHash = null;
  171. }
  172. /**
  173. * Checks if debug mode is enabled.
  174. *
  175. * @return bool true if debug mode is enabled, false otherwise
  176. */
  177. public function isDebug()
  178. {
  179. return $this->debug;
  180. }
  181. /**
  182. * Enables the auto_reload option.
  183. *
  184. * @return void
  185. */
  186. public function enableAutoReload()
  187. {
  188. $this->autoReload = true;
  189. }
  190. /**
  191. * Disables the auto_reload option.
  192. *
  193. * @return void
  194. */
  195. public function disableAutoReload()
  196. {
  197. $this->autoReload = false;
  198. }
  199. /**
  200. * Checks if the auto_reload option is enabled.
  201. *
  202. * @return bool true if auto_reload is enabled, false otherwise
  203. */
  204. public function isAutoReload()
  205. {
  206. return $this->autoReload;
  207. }
  208. /**
  209. * Enables the strict_variables option.
  210. *
  211. * @return void
  212. */
  213. public function enableStrictVariables()
  214. {
  215. $this->strictVariables = true;
  216. $this->optionsHash = null;
  217. }
  218. /**
  219. * Disables the strict_variables option.
  220. *
  221. * @return void
  222. */
  223. public function disableStrictVariables()
  224. {
  225. $this->strictVariables = false;
  226. $this->optionsHash = null;
  227. }
  228. /**
  229. * Checks if the strict_variables option is enabled.
  230. *
  231. * @return bool true if strict_variables is enabled, false otherwise
  232. */
  233. public function isStrictVariables()
  234. {
  235. return $this->strictVariables;
  236. }
  237. public function removeCache(string $name): void
  238. {
  239. $cls = $this->getTemplateClass($name);
  240. $this->hotCache[$name] = $cls.'_'.bin2hex(random_bytes(16));
  241. unset($this->loadedWrappers[$cls]);
  242. if ($this->cache instanceof RemovableCacheInterface) {
  243. $this->cache->remove($name, $cls);
  244. } else {
  245. throw new \LogicException(\sprintf('The "%s" cache class does not support removing template cache as it does not implement the "RemovableCacheInterface" interface.', \get_class($this->cache)));
  246. }
  247. }
  248. /**
  249. * Gets the current cache implementation.
  250. *
  251. * @param bool $original Whether to return the original cache option or the real cache instance
  252. *
  253. * @return CacheInterface|string|false A Twig\Cache\CacheInterface implementation,
  254. * an absolute path to the compiled templates,
  255. * or false to disable cache
  256. */
  257. public function getCache($original = true)
  258. {
  259. return $original ? $this->originalCache : $this->cache;
  260. }
  261. /**
  262. * Sets the current cache implementation.
  263. *
  264. * @param CacheInterface|string|false $cache A Twig\Cache\CacheInterface implementation,
  265. * an absolute path to the compiled templates,
  266. * or false to disable cache
  267. *
  268. * @return void
  269. */
  270. public function setCache($cache)
  271. {
  272. if (\is_string($cache)) {
  273. $this->originalCache = $cache;
  274. $this->cache = new FilesystemCache($cache, $this->autoReload ? FilesystemCache::FORCE_BYTECODE_INVALIDATION : 0);
  275. } elseif (false === $cache) {
  276. $this->originalCache = $cache;
  277. $this->cache = new NullCache();
  278. } elseif ($cache instanceof CacheInterface) {
  279. $this->originalCache = $this->cache = $cache;
  280. } else {
  281. throw new \LogicException('Cache can only be a string, false, or a \Twig\Cache\CacheInterface implementation.');
  282. }
  283. }
  284. /**
  285. * Gets the template class associated with the given string.
  286. *
  287. * The generated template class is based on the following parameters:
  288. *
  289. * * The cache key for the given template;
  290. * * The currently enabled extensions;
  291. * * PHP version;
  292. * * Twig version;
  293. * * Options with what environment was created.
  294. *
  295. * @param string $name The name for which to calculate the template class name
  296. * @param int|null $index The index if it is an embedded template
  297. *
  298. * @internal
  299. */
  300. public function getTemplateClass(string $name, ?int $index = null): string
  301. {
  302. $key = ($this->hotCache[$name] ?? $this->getLoader()->getCacheKey($name)).($this->optionsHash ??= $this->getOptionsHash());
  303. return '__TwigTemplate_'.hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', $key).(null === $index ? '' : '___'.$index);
  304. }
  305. /**
  306. * Renders a template.
  307. *
  308. * @param string|TemplateWrapper $name The template name
  309. *
  310. * @throws LoaderError When the template cannot be found
  311. * @throws SyntaxError When an error occurred during compilation
  312. * @throws RuntimeError When an error occurred during rendering
  313. */
  314. public function render($name, array $context = []): string
  315. {
  316. return $this->load($name)->render($context);
  317. }
  318. /**
  319. * Displays a template.
  320. *
  321. * @param string|TemplateWrapper $name The template name
  322. *
  323. * @throws LoaderError When the template cannot be found
  324. * @throws SyntaxError When an error occurred during compilation
  325. * @throws RuntimeError When an error occurred during rendering
  326. */
  327. public function display($name, array $context = []): void
  328. {
  329. $this->load($name)->display($context);
  330. }
  331. /**
  332. * Loads a template.
  333. *
  334. * @param string|TemplateWrapper $name The template name
  335. *
  336. * @throws LoaderError When the template cannot be found
  337. * @throws RuntimeError When a previously generated cache is corrupted
  338. * @throws SyntaxError When an error occurred during compilation
  339. */
  340. public function load($name): TemplateWrapper
  341. {
  342. if ($name instanceof TemplateWrapper) {
  343. $name->unwrap($this);
  344. return $name;
  345. }
  346. if ($name instanceof Template) {
  347. trigger_deprecation('twig/twig', '3.9', 'Passing a "%s" instance to "%s" is deprecated.', self::class, __METHOD__);
  348. return $name;
  349. }
  350. $cls = $this->getTemplateClass($name);
  351. return $this->loadedWrappers[$cls] ??= new TemplateWrapper($this, $this->loadTemplate($cls, $name));
  352. }
  353. /**
  354. * Loads a template internal representation.
  355. *
  356. * This method is for internal use only and should never be called
  357. * directly.
  358. *
  359. * @param string $name The template name
  360. * @param int|null $index The index if it is an embedded template
  361. *
  362. * @throws LoaderError When the template cannot be found
  363. * @throws RuntimeError When a previously generated cache is corrupted
  364. * @throws SyntaxError When an error occurred during compilation
  365. *
  366. * @internal
  367. */
  368. public function loadTemplate(string $cls, string $name, ?int $index = null): Template
  369. {
  370. $mainCls = $cls;
  371. if (null !== $index) {
  372. $cls .= '___'.$index;
  373. }
  374. if (isset($this->loadedTemplates[$cls])) {
  375. return $this->loadedTemplates[$cls];
  376. }
  377. if (!class_exists($cls, false)) {
  378. $key = $this->cache->generateKey($name, $mainCls);
  379. if (!$this->isAutoReload() || $this->isTemplateFresh($name, $this->cache->getTimestamp($key))) {
  380. $this->cache->load($key);
  381. }
  382. if (!class_exists($cls, false)) {
  383. $source = $this->getLoader()->getSourceContext($name);
  384. $content = $this->compileSource($source);
  385. if (!isset($this->hotCache[$name])) {
  386. $this->cache->write($key, $content);
  387. $this->cache->load($key);
  388. }
  389. if (!class_exists($mainCls, false)) {
  390. /* Last line of defense if either $this->bcWriteCacheFile was used,
  391. * $this->cache is implemented as a no-op or we have a race condition
  392. * where the cache was cleared between the above calls to write to and load from
  393. * the cache.
  394. */
  395. eval('?>'.$content);
  396. }
  397. if (!class_exists($cls, false)) {
  398. throw new RuntimeError(\sprintf('Failed to load Twig template "%s", index "%s": cache might be corrupted.', $name, $index), -1, $source);
  399. }
  400. }
  401. }
  402. $this->extensionSet->initRuntime();
  403. return $this->loadedTemplates[$cls] = new $cls($this);
  404. }
  405. /**
  406. * Creates a template from source.
  407. *
  408. * This method should not be used as a generic way to load templates.
  409. *
  410. * @param string $template The template source
  411. * @param string|null $name An optional name of the template to be used in error messages
  412. *
  413. * @throws LoaderError When the template cannot be found
  414. * @throws SyntaxError When an error occurred during compilation
  415. */
  416. public function createTemplate(string $template, ?string $name = null): TemplateWrapper
  417. {
  418. $hash = hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', $template, false);
  419. if (null !== $name) {
  420. $name = \sprintf('%s (string template %s)', $name, $hash);
  421. } else {
  422. $name = \sprintf('__string_template__%s', $hash);
  423. }
  424. $loader = new ChainLoader([
  425. new ArrayLoader([$name => $template]),
  426. $current = $this->getLoader(),
  427. ]);
  428. $this->setLoader($loader);
  429. try {
  430. return new TemplateWrapper($this, $this->loadTemplate($this->getTemplateClass($name), $name));
  431. } finally {
  432. $this->setLoader($current);
  433. }
  434. }
  435. /**
  436. * Returns true if the template is still fresh.
  437. *
  438. * Besides checking the loader for freshness information,
  439. * this method also checks if the enabled extensions have
  440. * not changed.
  441. *
  442. * @param int $time The last modification time of the cached template
  443. */
  444. public function isTemplateFresh(string $name, int $time): bool
  445. {
  446. return $this->extensionSet->getLastModified() <= $time && $this->getLoader()->isFresh($name, $time);
  447. }
  448. /**
  449. * Tries to load a template consecutively from an array.
  450. *
  451. * Similar to load() but it also accepts instances of \Twig\TemplateWrapper
  452. * and an array of templates where each is tried to be loaded.
  453. *
  454. * @param string|TemplateWrapper|array<string|TemplateWrapper> $names A template or an array of templates to try consecutively
  455. *
  456. * @throws LoaderError When none of the templates can be found
  457. * @throws SyntaxError When an error occurred during compilation
  458. */
  459. public function resolveTemplate($names): TemplateWrapper
  460. {
  461. if (!\is_array($names)) {
  462. return $this->load($names);
  463. }
  464. $count = \count($names);
  465. foreach ($names as $name) {
  466. if ($name instanceof Template) {
  467. trigger_deprecation('twig/twig', '3.9', 'Passing a "%s" instance to "%s" is deprecated.', Template::class, __METHOD__);
  468. return new TemplateWrapper($this, $name);
  469. }
  470. if ($name instanceof TemplateWrapper) {
  471. return $this->load($name);
  472. }
  473. if (1 !== $count && !$this->getLoader()->exists($name)) {
  474. continue;
  475. }
  476. return $this->load($name);
  477. }
  478. throw new LoaderError(\sprintf('Unable to find one of the following templates: "%s".', implode('", "', $names)));
  479. }
  480. /**
  481. * @return void
  482. */
  483. public function setLexer(Lexer $lexer)
  484. {
  485. $this->lexer = $lexer;
  486. }
  487. /**
  488. * @throws SyntaxError When the code is syntactically wrong
  489. */
  490. public function tokenize(Source $source): TokenStream
  491. {
  492. if (null === $this->lexer) {
  493. $this->lexer = new Lexer($this);
  494. }
  495. return $this->lexer->tokenize($source);
  496. }
  497. /**
  498. * @return void
  499. */
  500. public function setParser(Parser $parser)
  501. {
  502. $this->parser = $parser;
  503. }
  504. /**
  505. * Converts a token stream to a node tree.
  506. *
  507. * @throws SyntaxError When the token stream is syntactically or semantically wrong
  508. */
  509. public function parse(TokenStream $stream): ModuleNode
  510. {
  511. if (null === $this->parser) {
  512. $this->parser = new Parser($this);
  513. }
  514. return $this->parser->parse($stream);
  515. }
  516. /**
  517. * @return void
  518. */
  519. public function setCompiler(Compiler $compiler)
  520. {
  521. $this->compiler = $compiler;
  522. }
  523. /**
  524. * Compiles a node and returns the PHP code.
  525. */
  526. public function compile(Node $node): string
  527. {
  528. if (null === $this->compiler) {
  529. $this->compiler = new Compiler($this);
  530. }
  531. return $this->compiler->compile($node)->getSource();
  532. }
  533. /**
  534. * Compiles a template source code.
  535. *
  536. * @throws SyntaxError When there was an error during tokenizing, parsing or compiling
  537. */
  538. public function compileSource(Source $source): string
  539. {
  540. try {
  541. return $this->compile($this->parse($this->tokenize($source)));
  542. } catch (Error $e) {
  543. $e->setSourceContext($source);
  544. throw $e;
  545. } catch (\Exception $e) {
  546. throw new SyntaxError(\sprintf('An exception has been thrown during the compilation of a template ("%s").', $e->getMessage()), -1, $source, $e);
  547. }
  548. }
  549. /**
  550. * @return void
  551. */
  552. public function setLoader(LoaderInterface $loader)
  553. {
  554. $this->loader = $loader;
  555. }
  556. public function getLoader(): LoaderInterface
  557. {
  558. return $this->loader;
  559. }
  560. /**
  561. * @return void
  562. */
  563. public function setCharset(string $charset)
  564. {
  565. if ('UTF8' === $charset = strtoupper($charset ?: '')) {
  566. // iconv on Windows requires "UTF-8" instead of "UTF8"
  567. $charset = 'UTF-8';
  568. }
  569. $this->charset = $charset;
  570. }
  571. public function getCharset(): string
  572. {
  573. return $this->charset;
  574. }
  575. public function hasExtension(string $class): bool
  576. {
  577. return $this->extensionSet->hasExtension($class);
  578. }
  579. /**
  580. * @return void
  581. */
  582. public function addRuntimeLoader(RuntimeLoaderInterface $loader)
  583. {
  584. $this->runtimeLoaders[] = $loader;
  585. }
  586. /**
  587. * @template TExtension of ExtensionInterface
  588. *
  589. * @param class-string<TExtension> $class
  590. *
  591. * @return TExtension
  592. */
  593. public function getExtension(string $class): ExtensionInterface
  594. {
  595. return $this->extensionSet->getExtension($class);
  596. }
  597. /**
  598. * Returns the runtime implementation of a Twig element (filter/function/tag/test).
  599. *
  600. * @template TRuntime of object
  601. *
  602. * @param class-string<TRuntime> $class A runtime class name
  603. *
  604. * @return TRuntime The runtime implementation
  605. *
  606. * @throws RuntimeError When the template cannot be found
  607. */
  608. public function getRuntime(string $class)
  609. {
  610. if (isset($this->runtimes[$class])) {
  611. return $this->runtimes[$class];
  612. }
  613. foreach ($this->runtimeLoaders as $loader) {
  614. if (null !== $runtime = $loader->load($class)) {
  615. return $this->runtimes[$class] = $runtime;
  616. }
  617. }
  618. if (null !== $runtime = $this->defaultRuntimeLoader->load($class)) {
  619. return $this->runtimes[$class] = $runtime;
  620. }
  621. throw new RuntimeError(\sprintf('Unable to load the "%s" runtime.', $class));
  622. }
  623. /**
  624. * @return void
  625. */
  626. public function addExtension(ExtensionInterface $extension)
  627. {
  628. $this->extensionSet->addExtension($extension);
  629. $this->optionsHash = null;
  630. }
  631. /**
  632. * @param ExtensionInterface[] $extensions An array of extensions
  633. *
  634. * @return void
  635. */
  636. public function setExtensions(array $extensions)
  637. {
  638. $this->extensionSet->setExtensions($extensions);
  639. $this->optionsHash = null;
  640. }
  641. /**
  642. * @return ExtensionInterface[] An array of extensions (keys are for internal usage only and should not be relied on)
  643. */
  644. public function getExtensions(): array
  645. {
  646. return $this->extensionSet->getExtensions();
  647. }
  648. /**
  649. * @return void
  650. */
  651. public function addTokenParser(TokenParserInterface $parser)
  652. {
  653. $this->extensionSet->addTokenParser($parser);
  654. }
  655. /**
  656. * @return TokenParserInterface[]
  657. *
  658. * @internal
  659. */
  660. public function getTokenParsers(): array
  661. {
  662. return $this->extensionSet->getTokenParsers();
  663. }
  664. /**
  665. * @internal
  666. */
  667. public function getTokenParser(string $name): ?TokenParserInterface
  668. {
  669. return $this->extensionSet->getTokenParser($name);
  670. }
  671. /**
  672. * @param callable(string): (TokenParserInterface|false) $callable
  673. */
  674. public function registerUndefinedTokenParserCallback(callable $callable): void
  675. {
  676. $this->extensionSet->registerUndefinedTokenParserCallback($callable);
  677. }
  678. /**
  679. * @return void
  680. */
  681. public function addNodeVisitor(NodeVisitorInterface $visitor)
  682. {
  683. $this->extensionSet->addNodeVisitor($visitor);
  684. }
  685. /**
  686. * @return NodeVisitorInterface[]
  687. *
  688. * @internal
  689. */
  690. public function getNodeVisitors(): array
  691. {
  692. return $this->extensionSet->getNodeVisitors();
  693. }
  694. /**
  695. * @return void
  696. */
  697. public function addFilter(TwigFilter $filter)
  698. {
  699. $this->extensionSet->addFilter($filter);
  700. }
  701. /**
  702. * @internal
  703. */
  704. public function getFilter(string $name): ?TwigFilter
  705. {
  706. return $this->extensionSet->getFilter($name);
  707. }
  708. /**
  709. * @param callable(string): (TwigFilter|false) $callable
  710. */
  711. public function registerUndefinedFilterCallback(callable $callable): void
  712. {
  713. $this->extensionSet->registerUndefinedFilterCallback($callable);
  714. }
  715. /**
  716. * Gets the registered Filters.
  717. *
  718. * Be warned that this method cannot return filters defined with registerUndefinedFilterCallback.
  719. *
  720. * @return TwigFilter[]
  721. *
  722. * @see registerUndefinedFilterCallback
  723. *
  724. * @internal
  725. */
  726. public function getFilters(): array
  727. {
  728. return $this->extensionSet->getFilters();
  729. }
  730. /**
  731. * @return void
  732. */
  733. public function addTest(TwigTest $test)
  734. {
  735. $this->extensionSet->addTest($test);
  736. }
  737. /**
  738. * @return TwigTest[]
  739. *
  740. * @internal
  741. */
  742. public function getTests(): array
  743. {
  744. return $this->extensionSet->getTests();
  745. }
  746. /**
  747. * @internal
  748. */
  749. public function getTest(string $name): ?TwigTest
  750. {
  751. return $this->extensionSet->getTest($name);
  752. }
  753. /**
  754. * @param callable(string): (TwigTest|false) $callable
  755. */
  756. public function registerUndefinedTestCallback(callable $callable): void
  757. {
  758. $this->extensionSet->registerUndefinedTestCallback($callable);
  759. }
  760. /**
  761. * @return void
  762. */
  763. public function addFunction(TwigFunction $function)
  764. {
  765. $this->extensionSet->addFunction($function);
  766. }
  767. /**
  768. * @internal
  769. */
  770. public function getFunction(string $name): ?TwigFunction
  771. {
  772. return $this->extensionSet->getFunction($name);
  773. }
  774. /**
  775. * @param callable(string): (TwigFunction|false) $callable
  776. */
  777. public function registerUndefinedFunctionCallback(callable $callable): void
  778. {
  779. $this->extensionSet->registerUndefinedFunctionCallback($callable);
  780. }
  781. /**
  782. * Gets registered functions.
  783. *
  784. * Be warned that this method cannot return functions defined with registerUndefinedFunctionCallback.
  785. *
  786. * @return TwigFunction[]
  787. *
  788. * @see registerUndefinedFunctionCallback
  789. *
  790. * @internal
  791. */
  792. public function getFunctions(): array
  793. {
  794. return $this->extensionSet->getFunctions();
  795. }
  796. /**
  797. * Registers a Global.
  798. *
  799. * New globals can be added before compiling or rendering a template;
  800. * but after, you can only update existing globals.
  801. *
  802. * @param mixed $value The global value
  803. *
  804. * @return void
  805. */
  806. public function addGlobal(string $name, $value)
  807. {
  808. if ($this->extensionSet->isInitialized() && !\array_key_exists($name, $this->getGlobals())) {
  809. throw new \LogicException(\sprintf('Unable to add global "%s" as the runtime or the extensions have already been initialized.', $name));
  810. }
  811. if (null !== $this->resolvedGlobals) {
  812. $this->resolvedGlobals[$name] = $value;
  813. } else {
  814. $this->globals[$name] = $value;
  815. }
  816. }
  817. /**
  818. * @return array<string, mixed>
  819. */
  820. public function getGlobals(): array
  821. {
  822. if ($this->extensionSet->isInitialized()) {
  823. if (null === $this->resolvedGlobals) {
  824. $this->resolvedGlobals = array_merge($this->extensionSet->getGlobals(), $this->globals);
  825. }
  826. return $this->resolvedGlobals;
  827. }
  828. return array_merge($this->extensionSet->getGlobals(), $this->globals);
  829. }
  830. public function resetGlobals(): void
  831. {
  832. $this->resolvedGlobals = null;
  833. $this->extensionSet->resetGlobals();
  834. }
  835. /**
  836. * @deprecated since Twig 3.14
  837. */
  838. public function mergeGlobals(array $context): array
  839. {
  840. trigger_deprecation('twig/twig', '3.14', 'The "%s" method is deprecated.', __METHOD__);
  841. return $context + $this->getGlobals();
  842. }
  843. /**
  844. * @internal
  845. */
  846. public function getExpressionParsers(): ExpressionParsers
  847. {
  848. return $this->extensionSet->getExpressionParsers();
  849. }
  850. private function getOptionsHash(): string
  851. {
  852. return implode(':', [
  853. $this->extensionSet->getSignature(),
  854. \PHP_MAJOR_VERSION,
  855. \PHP_MINOR_VERSION,
  856. self::VERSION,
  857. (int) $this->debug,
  858. (int) $this->strictVariables,
  859. $this->useYield ? '1' : '0',
  860. ]);
  861. }
  862. }