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

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