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