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

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