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

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