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