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\Error\Error;
18use Twig\Error\LoaderError;
19use Twig\Error\RuntimeError;
20use Twig\Error\SyntaxError;
21use Twig\Extension\CoreExtension;
22use Twig\Extension\EscaperExtension;
23use Twig\Extension\ExtensionInterface;
24use Twig\Extension\GlobalsInterface;
25use Twig\Extension\InitRuntimeInterface;
26use Twig\Extension\OptimizerExtension;
27use Twig\Extension\StagingExtension;
28use Twig\Loader\ArrayLoader;
29use Twig\Loader\ChainLoader;
30use Twig\Loader\LoaderInterface;
31use Twig\Loader\SourceContextLoaderInterface;
32use Twig\Node\ModuleNode;
33use Twig\NodeVisitor\NodeVisitorInterface;
34use Twig\RuntimeLoader\RuntimeLoaderInterface;
35use Twig\TokenParser\TokenParserInterface;
36
37/**
38 * Stores the Twig configuration.
39 *
40 * @author Fabien Potencier <fabien@symfony.com>
41 */
42class Environment
43{
44    const VERSION = '1.38.4';
45    const VERSION_ID = 13804;
46    const MAJOR_VERSION = 2;
47    const MINOR_VERSION = 38;
48    const RELEASE_VERSION = 4;
49    const EXTRA_VERSION = '';
50
51    protected $charset;
52    protected $loader;
53    protected $debug;
54    protected $autoReload;
55    protected $cache;
56    protected $lexer;
57    protected $parser;
58    protected $compiler;
59    protected $baseTemplateClass;
60    protected $extensions;
61    protected $parsers;
62    protected $visitors;
63    protected $filters;
64    protected $tests;
65    protected $functions;
66    protected $globals;
67    protected $runtimeInitialized = false;
68    protected $extensionInitialized = false;
69    protected $loadedTemplates;
70    protected $strictVariables;
71    protected $unaryOperators;
72    protected $binaryOperators;
73    protected $templateClassPrefix = '__TwigTemplate_';
74    protected $functionCallbacks = [];
75    protected $filterCallbacks = [];
76    protected $staging;
77
78    private $originalCache;
79    private $bcWriteCacheFile = false;
80    private $bcGetCacheFilename = false;
81    private $lastModifiedExtension = 0;
82    private $extensionsByClass = [];
83    private $runtimeLoaders = [];
84    private $runtimes = [];
85    private $optionsHash;
86    private $loading = [];
87
88    /**
89     * Constructor.
90     *
91     * Available options:
92     *
93     *  * debug: When set to true, it automatically set "auto_reload" to true as
94     *           well (default to false).
95     *
96     *  * charset: The charset used by the templates (default to UTF-8).
97     *
98     *  * base_template_class: The base template class to use for generated
99     *                         templates (default to \Twig\Template).
100     *
101     *  * cache: An absolute path where to store the compiled templates,
102     *           a \Twig\Cache\CacheInterface implementation,
103     *           or false to disable compilation cache (default).
104     *
105     *  * auto_reload: Whether to reload the template if the original source changed.
106     *                 If you don't provide the auto_reload option, it will be
107     *                 determined automatically based on the debug value.
108     *
109     *  * strict_variables: Whether to ignore invalid variables in templates
110     *                      (default to false).
111     *
112     *  * autoescape: Whether to enable auto-escaping (default to html):
113     *                  * false: disable auto-escaping
114     *                  * true: equivalent to html
115     *                  * html, js: set the autoescaping to one of the supported strategies
116     *                  * name: set the autoescaping strategy based on the template name extension
117     *                  * PHP callback: a PHP callback that returns an escaping strategy based on the template "name"
118     *
119     *  * optimizations: A flag that indicates which optimizations to apply
120     *                   (default to -1 which means that all optimizations are enabled;
121     *                   set it to 0 to disable).
122     */
123    public function __construct(LoaderInterface $loader = null, $options = [])
124    {
125        if (null !== $loader) {
126            $this->setLoader($loader);
127        } else {
128            @trigger_error('Not passing a "Twig\Lodaer\LoaderInterface" as the first constructor argument of "Twig\Environment" is deprecated since version 1.21.', E_USER_DEPRECATED);
129        }
130
131        $options = array_merge([
132            'debug' => false,
133            'charset' => 'UTF-8',
134            'base_template_class' => '\Twig\Template',
135            'strict_variables' => false,
136            'autoescape' => 'html',
137            'cache' => false,
138            'auto_reload' => null,
139            'optimizations' => -1,
140        ], $options);
141
142        $this->debug = (bool) $options['debug'];
143        $this->charset = strtoupper($options['charset']);
144        $this->baseTemplateClass = $options['base_template_class'];
145        $this->autoReload = null === $options['auto_reload'] ? $this->debug : (bool) $options['auto_reload'];
146        $this->strictVariables = (bool) $options['strict_variables'];
147        $this->setCache($options['cache']);
148
149        $this->addExtension(new CoreExtension());
150        $this->addExtension(new EscaperExtension($options['autoescape']));
151        $this->addExtension(new OptimizerExtension($options['optimizations']));
152        $this->staging = new StagingExtension();
153
154        // For BC
155        if (\is_string($this->originalCache)) {
156            $r = new \ReflectionMethod($this, 'writeCacheFile');
157            if (__CLASS__ !== $r->getDeclaringClass()->getName()) {
158                @trigger_error('The Twig\Environment::writeCacheFile method is deprecated since version 1.22 and will be removed in Twig 2.0.', E_USER_DEPRECATED);
159
160                $this->bcWriteCacheFile = true;
161            }
162
163            $r = new \ReflectionMethod($this, 'getCacheFilename');
164            if (__CLASS__ !== $r->getDeclaringClass()->getName()) {
165                @trigger_error('The Twig\Environment::getCacheFilename method is deprecated since version 1.22 and will be removed in Twig 2.0.', E_USER_DEPRECATED);
166
167                $this->bcGetCacheFilename = true;
168            }
169        }
170    }
171
172    /**
173     * Gets the base template class for compiled templates.
174     *
175     * @return string The base template class name
176     */
177    public function getBaseTemplateClass()
178    {
179        return $this->baseTemplateClass;
180    }
181
182    /**
183     * Sets the base template class for compiled templates.
184     *
185     * @param string $class The base template class name
186     */
187    public function setBaseTemplateClass($class)
188    {
189        $this->baseTemplateClass = $class;
190        $this->updateOptionsHash();
191    }
192
193    /**
194     * Enables debugging mode.
195     */
196    public function enableDebug()
197    {
198        $this->debug = true;
199        $this->updateOptionsHash();
200    }
201
202    /**
203     * Disables debugging mode.
204     */
205    public function disableDebug()
206    {
207        $this->debug = false;
208        $this->updateOptionsHash();
209    }
210
211    /**
212     * Checks if debug mode is enabled.
213     *
214     * @return bool true if debug mode is enabled, false otherwise
215     */
216    public function isDebug()
217    {
218        return $this->debug;
219    }
220
221    /**
222     * Enables the auto_reload option.
223     */
224    public function enableAutoReload()
225    {
226        $this->autoReload = true;
227    }
228
229    /**
230     * Disables the auto_reload option.
231     */
232    public function disableAutoReload()
233    {
234        $this->autoReload = false;
235    }
236
237    /**
238     * Checks if the auto_reload option is enabled.
239     *
240     * @return bool true if auto_reload is enabled, false otherwise
241     */
242    public function isAutoReload()
243    {
244        return $this->autoReload;
245    }
246
247    /**
248     * Enables the strict_variables option.
249     */
250    public function enableStrictVariables()
251    {
252        $this->strictVariables = true;
253        $this->updateOptionsHash();
254    }
255
256    /**
257     * Disables the strict_variables option.
258     */
259    public function disableStrictVariables()
260    {
261        $this->strictVariables = false;
262        $this->updateOptionsHash();
263    }
264
265    /**
266     * Checks if the strict_variables option is enabled.
267     *
268     * @return bool true if strict_variables is enabled, false otherwise
269     */
270    public function isStrictVariables()
271    {
272        return $this->strictVariables;
273    }
274
275    /**
276     * Gets the current cache implementation.
277     *
278     * @param bool $original Whether to return the original cache option or the real cache instance
279     *
280     * @return CacheInterface|string|false A Twig\Cache\CacheInterface implementation,
281     *                                     an absolute path to the compiled templates,
282     *                                     or false to disable cache
283     */
284    public function getCache($original = true)
285    {
286        return $original ? $this->originalCache : $this->cache;
287    }
288
289    /**
290     * Sets the current cache implementation.
291     *
292     * @param CacheInterface|string|false $cache A Twig\Cache\CacheInterface implementation,
293     *                                           an absolute path to the compiled templates,
294     *                                           or false to disable cache
295     */
296    public function setCache($cache)
297    {
298        if (\is_string($cache)) {
299            $this->originalCache = $cache;
300            $this->cache = new FilesystemCache($cache);
301        } elseif (false === $cache) {
302            $this->originalCache = $cache;
303            $this->cache = new NullCache();
304        } elseif (null === $cache) {
305            @trigger_error('Using "null" as the cache strategy is deprecated since version 1.23 and will be removed in Twig 2.0.', E_USER_DEPRECATED);
306            $this->originalCache = false;
307            $this->cache = new NullCache();
308        } elseif ($cache instanceof CacheInterface) {
309            $this->originalCache = $this->cache = $cache;
310        } else {
311            throw new \LogicException(sprintf('Cache can only be a string, false, or a \Twig\Cache\CacheInterface implementation.'));
312        }
313    }
314
315    /**
316     * Gets the cache filename for a given template.
317     *
318     * @param string $name The template name
319     *
320     * @return string|false The cache file name or false when caching is disabled
321     *
322     * @deprecated since 1.22 (to be removed in 2.0)
323     */
324    public function getCacheFilename($name)
325    {
326        @trigger_error(sprintf('The %s method is deprecated since version 1.22 and will be removed in Twig 2.0.', __METHOD__), E_USER_DEPRECATED);
327
328        $key = $this->cache->generateKey($name, $this->getTemplateClass($name));
329
330        return !$key ? false : $key;
331    }
332
333    /**
334     * Gets the template class associated with the given string.
335     *
336     * The generated template class is based on the following parameters:
337     *
338     *  * The cache key for the given template;
339     *  * The currently enabled extensions;
340     *  * Whether the Twig C extension is available or not;
341     *  * PHP version;
342     *  * Twig version;
343     *  * Options with what environment was created.
344     *
345     * @param string   $name  The name for which to calculate the template class name
346     * @param int|null $index The index if it is an embedded template
347     *
348     * @return string The template class name
349     */
350    public function getTemplateClass($name, $index = null)
351    {
352        $key = $this->getLoader()->getCacheKey($name).$this->optionsHash;
353
354        return $this->templateClassPrefix.hash('sha256', $key).(null === $index ? '' : '___'.$index);
355    }
356
357    /**
358     * Gets the template class prefix.
359     *
360     * @return string The template class prefix
361     *
362     * @deprecated since 1.22 (to be removed in 2.0)
363     */
364    public function getTemplateClassPrefix()
365    {
366        @trigger_error(sprintf('The %s method is deprecated since version 1.22 and will be removed in Twig 2.0.', __METHOD__), E_USER_DEPRECATED);
367
368        return $this->templateClassPrefix;
369    }
370
371    /**
372     * Renders a template.
373     *
374     * @param string|TemplateWrapper $name    The template name
375     * @param array                  $context An array of parameters to pass to the template
376     *
377     * @return string The rendered template
378     *
379     * @throws LoaderError  When the template cannot be found
380     * @throws SyntaxError  When an error occurred during compilation
381     * @throws RuntimeError When an error occurred during rendering
382     */
383    public function render($name, array $context = [])
384    {
385        return $this->load($name)->render($context);
386    }
387
388    /**
389     * Displays a template.
390     *
391     * @param string|TemplateWrapper $name    The template name
392     * @param array                  $context An array of parameters to pass to the template
393     *
394     * @throws LoaderError  When the template cannot be found
395     * @throws SyntaxError  When an error occurred during compilation
396     * @throws RuntimeError When an error occurred during rendering
397     */
398    public function display($name, array $context = [])
399    {
400        $this->load($name)->display($context);
401    }
402
403    /**
404     * Loads a template.
405     *
406     * @param string|TemplateWrapper|\Twig\Template $name The template name
407     *
408     * @throws LoaderError  When the template cannot be found
409     * @throws RuntimeError When a previously generated cache is corrupted
410     * @throws SyntaxError  When an error occurred during compilation
411     *
412     * @return TemplateWrapper
413     */
414    public function load($name)
415    {
416        if ($name instanceof TemplateWrapper) {
417            return $name;
418        }
419
420        if ($name instanceof Template) {
421            return new TemplateWrapper($this, $name);
422        }
423
424        return new TemplateWrapper($this, $this->loadTemplate($name));
425    }
426
427    /**
428     * Loads a template internal representation.
429     *
430     * This method is for internal use only and should never be called
431     * directly.
432     *
433     * @param string $name  The template name
434     * @param int    $index The index if it is an embedded template
435     *
436     * @return \Twig_TemplateInterface A template instance representing the given template name
437     *
438     * @throws LoaderError  When the template cannot be found
439     * @throws RuntimeError When a previously generated cache is corrupted
440     * @throws SyntaxError  When an error occurred during compilation
441     *
442     * @internal
443     */
444    public function loadTemplate($name, $index = null)
445    {
446        return $this->loadClass($this->getTemplateClass($name), $name, $index);
447    }
448
449    /**
450     * @internal
451     */
452    public function loadClass($cls, $name, $index = null)
453    {
454        $mainCls = $cls;
455        if (null !== $index) {
456            $cls .= '___'.$index;
457        }
458
459        if (isset($this->loadedTemplates[$cls])) {
460            return $this->loadedTemplates[$cls];
461        }
462
463        if (!class_exists($cls, false)) {
464            if ($this->bcGetCacheFilename) {
465                $key = $this->getCacheFilename($name);
466            } else {
467                $key = $this->cache->generateKey($name, $mainCls);
468            }
469
470            if (!$this->isAutoReload() || $this->isTemplateFresh($name, $this->cache->getTimestamp($key))) {
471                $this->cache->load($key);
472            }
473
474            if (!class_exists($cls, false)) {
475                $loader = $this->getLoader();
476                if (!$loader instanceof SourceContextLoaderInterface) {
477                    $source = new Source($loader->getSource($name), $name);
478                } else {
479                    $source = $loader->getSourceContext($name);
480                }
481
482                $content = $this->compileSource($source);
483
484                if ($this->bcWriteCacheFile) {
485                    $this->writeCacheFile($key, $content);
486                } else {
487                    $this->cache->write($key, $content);
488                    $this->cache->load($key);
489                }
490
491                if (!class_exists($mainCls, false)) {
492                    /* Last line of defense if either $this->bcWriteCacheFile was used,
493                     * $this->cache is implemented as a no-op or we have a race condition
494                     * where the cache was cleared between the above calls to write to and load from
495                     * the cache.
496                     */
497                    eval('?>'.$content);
498                }
499            }
500
501            if (!class_exists($cls, false)) {
502                throw new RuntimeError(sprintf('Failed to load Twig template "%s", index "%s": cache might be corrupted.', $name, $index), -1, $source);
503            }
504        }
505
506        if (!$this->runtimeInitialized) {
507            $this->initRuntime();
508        }
509
510        if (isset($this->loading[$cls])) {
511            throw new RuntimeError(sprintf('Circular reference detected for Twig template "%s", path: %s.', $name, implode(' -> ', array_merge($this->loading, [$name]))));
512        }
513
514        $this->loading[$cls] = $name;
515
516        try {
517            $this->loadedTemplates[$cls] = new $cls($this);
518            unset($this->loading[$cls]);
519        } catch (\Exception $e) {
520            unset($this->loading[$cls]);
521
522            throw $e;
523        }
524
525        return $this->loadedTemplates[$cls];
526    }
527
528    /**
529     * Creates a template from source.
530     *
531     * This method should not be used as a generic way to load templates.
532     *
533     * @param string $template The template name
534     *
535     * @return TemplateWrapper A template instance representing the given template name
536     *
537     * @throws LoaderError When the template cannot be found
538     * @throws SyntaxError When an error occurred during compilation
539     */
540    public function createTemplate($template)
541    {
542        $name = sprintf('__string_template__%s', hash('sha256', $template, false));
543
544        $loader = new ChainLoader([
545            new ArrayLoader([$name => $template]),
546            $current = $this->getLoader(),
547        ]);
548
549        $this->setLoader($loader);
550        try {
551            $template = new TemplateWrapper($this, $this->loadTemplate($name));
552        } catch (\Exception $e) {
553            $this->setLoader($current);
554
555            throw $e;
556        } catch (\Throwable $e) {
557            $this->setLoader($current);
558
559            throw $e;
560        }
561        $this->setLoader($current);
562
563        return $template;
564    }
565
566    /**
567     * Returns true if the template is still fresh.
568     *
569     * Besides checking the loader for freshness information,
570     * this method also checks if the enabled extensions have
571     * not changed.
572     *
573     * @param string $name The template name
574     * @param int    $time The last modification time of the cached template
575     *
576     * @return bool true if the template is fresh, false otherwise
577     */
578    public function isTemplateFresh($name, $time)
579    {
580        if (0 === $this->lastModifiedExtension) {
581            foreach ($this->extensions as $extension) {
582                $r = new \ReflectionObject($extension);
583                if (file_exists($r->getFileName()) && ($extensionTime = filemtime($r->getFileName())) > $this->lastModifiedExtension) {
584                    $this->lastModifiedExtension = $extensionTime;
585                }
586            }
587        }
588
589        return $this->lastModifiedExtension <= $time && $this->getLoader()->isFresh($name, $time);
590    }
591
592    /**
593     * Tries to load a template consecutively from an array.
594     *
595     * Similar to load() but it also accepts instances of \Twig\Template and
596     * \Twig\TemplateWrapper, and an array of templates where each is tried to be loaded.
597     *
598     * @param string|Template|\Twig\TemplateWrapper|array $names A template or an array of templates to try consecutively
599     *
600     * @return TemplateWrapper|Template
601     *
602     * @throws LoaderError When none of the templates can be found
603     * @throws SyntaxError When an error occurred during compilation
604     */
605    public function resolveTemplate($names)
606    {
607        if (!\is_array($names)) {
608            $names = [$names];
609        }
610
611        foreach ($names as $name) {
612            if ($name instanceof Template) {
613                return $name;
614            }
615            if ($name instanceof TemplateWrapper) {
616                return $name;
617            }
618
619            try {
620                return $this->loadTemplate($name);
621            } catch (LoaderError $e) {
622                if (1 === \count($names)) {
623                    throw $e;
624                }
625            }
626        }
627
628        throw new LoaderError(sprintf('Unable to find one of the following templates: "%s".', implode('", "', $names)));
629    }
630
631    /**
632     * Clears the internal template cache.
633     *
634     * @deprecated since 1.18.3 (to be removed in 2.0)
635     */
636    public function clearTemplateCache()
637    {
638        @trigger_error(sprintf('The %s method is deprecated since version 1.18.3 and will be removed in Twig 2.0.', __METHOD__), E_USER_DEPRECATED);
639
640        $this->loadedTemplates = [];
641    }
642
643    /**
644     * Clears the template cache files on the filesystem.
645     *
646     * @deprecated since 1.22 (to be removed in 2.0)
647     */
648    public function clearCacheFiles()
649    {
650        @trigger_error(sprintf('The %s method is deprecated since version 1.22 and will be removed in Twig 2.0.', __METHOD__), E_USER_DEPRECATED);
651
652        if (\is_string($this->originalCache)) {
653            foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($this->originalCache), \RecursiveIteratorIterator::LEAVES_ONLY) as $file) {
654                if ($file->isFile()) {
655                    @unlink($file->getPathname());
656                }
657            }
658        }
659    }
660
661    /**
662     * Gets the Lexer instance.
663     *
664     * @return \Twig_LexerInterface
665     *
666     * @deprecated since 1.25 (to be removed in 2.0)
667     */
668    public function getLexer()
669    {
670        @trigger_error(sprintf('The %s() method is deprecated since version 1.25 and will be removed in 2.0.', __FUNCTION__), E_USER_DEPRECATED);
671
672        if (null === $this->lexer) {
673            $this->lexer = new Lexer($this);
674        }
675
676        return $this->lexer;
677    }
678
679    public function setLexer(\Twig_LexerInterface $lexer)
680    {
681        $this->lexer = $lexer;
682    }
683
684    /**
685     * Tokenizes a source code.
686     *
687     * @param string|Source $source The template source code
688     * @param string        $name   The template name (deprecated)
689     *
690     * @return TokenStream
691     *
692     * @throws SyntaxError When the code is syntactically wrong
693     */
694    public function tokenize($source, $name = null)
695    {
696        if (!$source instanceof Source) {
697            @trigger_error(sprintf('Passing a string as the $source argument of %s() is deprecated since version 1.27. Pass a Twig\Source instance instead.', __METHOD__), E_USER_DEPRECATED);
698            $source = new Source($source, $name);
699        }
700
701        if (null === $this->lexer) {
702            $this->lexer = new Lexer($this);
703        }
704
705        return $this->lexer->tokenize($source);
706    }
707
708    /**
709     * Gets the Parser instance.
710     *
711     * @return \Twig_ParserInterface
712     *
713     * @deprecated since 1.25 (to be removed in 2.0)
714     */
715    public function getParser()
716    {
717        @trigger_error(sprintf('The %s() method is deprecated since version 1.25 and will be removed in 2.0.', __FUNCTION__), E_USER_DEPRECATED);
718
719        if (null === $this->parser) {
720            $this->parser = new Parser($this);
721        }
722
723        return $this->parser;
724    }
725
726    public function setParser(\Twig_ParserInterface $parser)
727    {
728        $this->parser = $parser;
729    }
730
731    /**
732     * Converts a token stream to a node tree.
733     *
734     * @return ModuleNode
735     *
736     * @throws SyntaxError When the token stream is syntactically or semantically wrong
737     */
738    public function parse(TokenStream $stream)
739    {
740        if (null === $this->parser) {
741            $this->parser = new Parser($this);
742        }
743
744        return $this->parser->parse($stream);
745    }
746
747    /**
748     * Gets the Compiler instance.
749     *
750     * @return \Twig_CompilerInterface
751     *
752     * @deprecated since 1.25 (to be removed in 2.0)
753     */
754    public function getCompiler()
755    {
756        @trigger_error(sprintf('The %s() method is deprecated since version 1.25 and will be removed in 2.0.', __FUNCTION__), E_USER_DEPRECATED);
757
758        if (null === $this->compiler) {
759            $this->compiler = new Compiler($this);
760        }
761
762        return $this->compiler;
763    }
764
765    public function setCompiler(\Twig_CompilerInterface $compiler)
766    {
767        $this->compiler = $compiler;
768    }
769
770    /**
771     * Compiles a node and returns the PHP code.
772     *
773     * @return string The compiled PHP source code
774     */
775    public function compile(\Twig_NodeInterface $node)
776    {
777        if (null === $this->compiler) {
778            $this->compiler = new Compiler($this);
779        }
780
781        return $this->compiler->compile($node)->getSource();
782    }
783
784    /**
785     * Compiles a template source code.
786     *
787     * @param string|Source $source The template source code
788     * @param string        $name   The template name (deprecated)
789     *
790     * @return string The compiled PHP source code
791     *
792     * @throws SyntaxError When there was an error during tokenizing, parsing or compiling
793     */
794    public function compileSource($source, $name = null)
795    {
796        if (!$source instanceof Source) {
797            @trigger_error(sprintf('Passing a string as the $source argument of %s() is deprecated since version 1.27. Pass a Twig\Source instance instead.', __METHOD__), E_USER_DEPRECATED);
798            $source = new Source($source, $name);
799        }
800
801        try {
802            return $this->compile($this->parse($this->tokenize($source)));
803        } catch (Error $e) {
804            $e->setSourceContext($source);
805            throw $e;
806        } catch (\Exception $e) {
807            throw new SyntaxError(sprintf('An exception has been thrown during the compilation of a template ("%s").', $e->getMessage()), -1, $source, $e);
808        }
809    }
810
811    public function setLoader(LoaderInterface $loader)
812    {
813        if (!$loader instanceof SourceContextLoaderInterface && 0 !== strpos(\get_class($loader), 'Mock_')) {
814            @trigger_error(sprintf('Twig loader "%s" should implement Twig\Loader\SourceContextLoaderInterface since version 1.27.', \get_class($loader)), E_USER_DEPRECATED);
815        }
816
817        $this->loader = $loader;
818    }
819
820    /**
821     * Gets the Loader instance.
822     *
823     * @return LoaderInterface
824     */
825    public function getLoader()
826    {
827        if (null === $this->loader) {
828            throw new \LogicException('You must set a loader first.');
829        }
830
831        return $this->loader;
832    }
833
834    /**
835     * Sets the default template charset.
836     *
837     * @param string $charset The default charset
838     */
839    public function setCharset($charset)
840    {
841        $this->charset = strtoupper($charset);
842    }
843
844    /**
845     * Gets the default template charset.
846     *
847     * @return string The default charset
848     */
849    public function getCharset()
850    {
851        return $this->charset;
852    }
853
854    /**
855     * Initializes the runtime environment.
856     *
857     * @deprecated since 1.23 (to be removed in 2.0)
858     */
859    public function initRuntime()
860    {
861        $this->runtimeInitialized = true;
862
863        foreach ($this->getExtensions() as $name => $extension) {
864            if (!$extension instanceof InitRuntimeInterface) {
865                $m = new \ReflectionMethod($extension, 'initRuntime');
866
867                $parentClass = $m->getDeclaringClass()->getName();
868                if ('Twig_Extension' !== $parentClass && 'Twig\Extension\AbstractExtension' !== $parentClass) {
869                    @trigger_error(sprintf('Defining the initRuntime() method in the "%s" extension is deprecated since version 1.23. Use the `needs_environment` option to get the \Twig_Environment instance in filters, functions, or tests; or explicitly implement Twig\Extension\InitRuntimeInterface if needed (not recommended).', $name), E_USER_DEPRECATED);
870                }
871            }
872
873            $extension->initRuntime($this);
874        }
875    }
876
877    /**
878     * Returns true if the given extension is registered.
879     *
880     * @param string $class The extension class name
881     *
882     * @return bool Whether the extension is registered or not
883     */
884    public function hasExtension($class)
885    {
886        $class = ltrim($class, '\\');
887        if (!isset($this->extensionsByClass[$class]) && class_exists($class, false)) {
888            // For BC/FC with namespaced aliases
889            $class = new \ReflectionClass($class);
890            $class = $class->name;
891        }
892
893        if (isset($this->extensions[$class])) {
894            if ($class !== \get_class($this->extensions[$class])) {
895                @trigger_error(sprintf('Referencing the "%s" extension by its name (defined by getName()) is deprecated since 1.26 and will be removed in Twig 2.0. Use the Fully Qualified Extension Class Name instead.', $class), E_USER_DEPRECATED);
896            }
897
898            return true;
899        }
900
901        return isset($this->extensionsByClass[$class]);
902    }
903
904    /**
905     * Adds a runtime loader.
906     */
907    public function addRuntimeLoader(RuntimeLoaderInterface $loader)
908    {
909        $this->runtimeLoaders[] = $loader;
910    }
911
912    /**
913     * Gets an extension by class name.
914     *
915     * @param string $class The extension class name
916     *
917     * @return ExtensionInterface
918     */
919    public function getExtension($class)
920    {
921        $class = ltrim($class, '\\');
922        if (!isset($this->extensionsByClass[$class]) && class_exists($class, false)) {
923            // For BC/FC with namespaced aliases
924            $class = new \ReflectionClass($class);
925            $class = $class->name;
926        }
927
928        if (isset($this->extensions[$class])) {
929            if ($class !== \get_class($this->extensions[$class])) {
930                @trigger_error(sprintf('Referencing the "%s" extension by its name (defined by getName()) is deprecated since 1.26 and will be removed in Twig 2.0. Use the Fully Qualified Extension Class Name instead.', $class), E_USER_DEPRECATED);
931            }
932
933            return $this->extensions[$class];
934        }
935
936        if (!isset($this->extensionsByClass[$class])) {
937            throw new RuntimeError(sprintf('The "%s" extension is not enabled.', $class));
938        }
939
940        return $this->extensionsByClass[$class];
941    }
942
943    /**
944     * Returns the runtime implementation of a Twig element (filter/function/test).
945     *
946     * @param string $class A runtime class name
947     *
948     * @return object The runtime implementation
949     *
950     * @throws RuntimeError When the template cannot be found
951     */
952    public function getRuntime($class)
953    {
954        if (isset($this->runtimes[$class])) {
955            return $this->runtimes[$class];
956        }
957
958        foreach ($this->runtimeLoaders as $loader) {
959            if (null !== $runtime = $loader->load($class)) {
960                return $this->runtimes[$class] = $runtime;
961            }
962        }
963
964        throw new RuntimeError(sprintf('Unable to load the "%s" runtime.', $class));
965    }
966
967    public function addExtension(ExtensionInterface $extension)
968    {
969        if ($this->extensionInitialized) {
970            throw new \LogicException(sprintf('Unable to register extension "%s" as extensions have already been initialized.', $extension->getName()));
971        }
972
973        $class = \get_class($extension);
974        if ($class !== $extension->getName()) {
975            if (isset($this->extensions[$extension->getName()])) {
976                unset($this->extensions[$extension->getName()], $this->extensionsByClass[$class]);
977                @trigger_error(sprintf('The possibility to register the same extension twice ("%s") is deprecated since version 1.23 and will be removed in Twig 2.0. Use proper PHP inheritance instead.', $extension->getName()), E_USER_DEPRECATED);
978            }
979        }
980
981        $this->lastModifiedExtension = 0;
982        $this->extensionsByClass[$class] = $extension;
983        $this->extensions[$extension->getName()] = $extension;
984        $this->updateOptionsHash();
985    }
986
987    /**
988     * Removes an extension by name.
989     *
990     * This method is deprecated and you should not use it.
991     *
992     * @param string $name The extension name
993     *
994     * @deprecated since 1.12 (to be removed in 2.0)
995     */
996    public function removeExtension($name)
997    {
998        @trigger_error(sprintf('The %s method is deprecated since version 1.12 and will be removed in Twig 2.0.', __METHOD__), E_USER_DEPRECATED);
999
1000        if ($this->extensionInitialized) {
1001            throw new \LogicException(sprintf('Unable to remove extension "%s" as extensions have already been initialized.', $name));
1002        }
1003
1004        $class = ltrim($name, '\\');
1005        if (!isset($this->extensionsByClass[$class]) && class_exists($class, false)) {
1006            // For BC/FC with namespaced aliases
1007            $class = new \ReflectionClass($class);
1008            $class = $class->name;
1009        }
1010
1011        if (isset($this->extensions[$class])) {
1012            if ($class !== \get_class($this->extensions[$class])) {
1013                @trigger_error(sprintf('Referencing the "%s" extension by its name (defined by getName()) is deprecated since 1.26 and will be removed in Twig 2.0. Use the Fully Qualified Extension Class Name instead.', $class), E_USER_DEPRECATED);
1014            }
1015
1016            unset($this->extensions[$class]);
1017        }
1018
1019        unset($this->extensions[$class]);
1020        $this->updateOptionsHash();
1021    }
1022
1023    /**
1024     * Registers an array of extensions.
1025     *
1026     * @param array $extensions An array of extensions
1027     */
1028    public function setExtensions(array $extensions)
1029    {
1030        foreach ($extensions as $extension) {
1031            $this->addExtension($extension);
1032        }
1033    }
1034
1035    /**
1036     * Returns all registered extensions.
1037     *
1038     * @return ExtensionInterface[] An array of extensions (keys are for internal usage only and should not be relied on)
1039     */
1040    public function getExtensions()
1041    {
1042        return $this->extensions;
1043    }
1044
1045    public function addTokenParser(TokenParserInterface $parser)
1046    {
1047        if ($this->extensionInitialized) {
1048            throw new \LogicException('Unable to add a token parser as extensions have already been initialized.');
1049        }
1050
1051        $this->staging->addTokenParser($parser);
1052    }
1053
1054    /**
1055     * Gets the registered Token Parsers.
1056     *
1057     * @return \Twig_TokenParserBrokerInterface
1058     *
1059     * @internal
1060     */
1061    public function getTokenParsers()
1062    {
1063        if (!$this->extensionInitialized) {
1064            $this->initExtensions();
1065        }
1066
1067        return $this->parsers;
1068    }
1069
1070    /**
1071     * Gets registered tags.
1072     *
1073     * Be warned that this method cannot return tags defined by \Twig_TokenParserBrokerInterface classes.
1074     *
1075     * @return TokenParserInterface[]
1076     *
1077     * @internal
1078     */
1079    public function getTags()
1080    {
1081        $tags = [];
1082        foreach ($this->getTokenParsers()->getParsers() as $parser) {
1083            if ($parser instanceof TokenParserInterface) {
1084                $tags[$parser->getTag()] = $parser;
1085            }
1086        }
1087
1088        return $tags;
1089    }
1090
1091    public function addNodeVisitor(NodeVisitorInterface $visitor)
1092    {
1093        if ($this->extensionInitialized) {
1094            throw new \LogicException('Unable to add a node visitor as extensions have already been initialized.');
1095        }
1096
1097        $this->staging->addNodeVisitor($visitor);
1098    }
1099
1100    /**
1101     * Gets the registered Node Visitors.
1102     *
1103     * @return NodeVisitorInterface[]
1104     *
1105     * @internal
1106     */
1107    public function getNodeVisitors()
1108    {
1109        if (!$this->extensionInitialized) {
1110            $this->initExtensions();
1111        }
1112
1113        return $this->visitors;
1114    }
1115
1116    /**
1117     * Registers a Filter.
1118     *
1119     * @param string|TwigFilter                $name   The filter name or a \Twig_SimpleFilter instance
1120     * @param \Twig_FilterInterface|TwigFilter $filter
1121     */
1122    public function addFilter($name, $filter = null)
1123    {
1124        if (!$name instanceof TwigFilter && !($filter instanceof TwigFilter || $filter instanceof \Twig_FilterInterface)) {
1125            throw new \LogicException('A filter must be an instance of \Twig_FilterInterface or \Twig_SimpleFilter.');
1126        }
1127
1128        if ($name instanceof TwigFilter) {
1129            $filter = $name;
1130            $name = $filter->getName();
1131        } else {
1132            @trigger_error(sprintf('Passing a name as a first argument to the %s method is deprecated since version 1.21. Pass an instance of "Twig_SimpleFilter" instead when defining filter "%s".', __METHOD__, $name), E_USER_DEPRECATED);
1133        }
1134
1135        if ($this->extensionInitialized) {
1136            throw new \LogicException(sprintf('Unable to add filter "%s" as extensions have already been initialized.', $name));
1137        }
1138
1139        $this->staging->addFilter($name, $filter);
1140    }
1141
1142    /**
1143     * Get a filter by name.
1144     *
1145     * Subclasses may override this method and load filters differently;
1146     * so no list of filters is available.
1147     *
1148     * @param string $name The filter name
1149     *
1150     * @return \Twig_Filter|false
1151     *
1152     * @internal
1153     */
1154    public function getFilter($name)
1155    {
1156        if (!$this->extensionInitialized) {
1157            $this->initExtensions();
1158        }
1159
1160        if (isset($this->filters[$name])) {
1161            return $this->filters[$name];
1162        }
1163
1164        foreach ($this->filters as $pattern => $filter) {
1165            $pattern = str_replace('\\*', '(.*?)', preg_quote($pattern, '#'), $count);
1166
1167            if ($count) {
1168                if (preg_match('#^'.$pattern.'$#', $name, $matches)) {
1169                    array_shift($matches);
1170                    $filter->setArguments($matches);
1171
1172                    return $filter;
1173                }
1174            }
1175        }
1176
1177        foreach ($this->filterCallbacks as $callback) {
1178            if (false !== $filter = \call_user_func($callback, $name)) {
1179                return $filter;
1180            }
1181        }
1182
1183        return false;
1184    }
1185
1186    public function registerUndefinedFilterCallback($callable)
1187    {
1188        $this->filterCallbacks[] = $callable;
1189    }
1190
1191    /**
1192     * Gets the registered Filters.
1193     *
1194     * Be warned that this method cannot return filters defined with registerUndefinedFilterCallback.
1195     *
1196     * @return \Twig_FilterInterface[]
1197     *
1198     * @see registerUndefinedFilterCallback
1199     *
1200     * @internal
1201     */
1202    public function getFilters()
1203    {
1204        if (!$this->extensionInitialized) {
1205            $this->initExtensions();
1206        }
1207
1208        return $this->filters;
1209    }
1210
1211    /**
1212     * Registers a Test.
1213     *
1214     * @param string|TwigTest              $name The test name or a \Twig_SimpleTest instance
1215     * @param \Twig_TestInterface|TwigTest $test A \Twig_TestInterface instance or a \Twig_SimpleTest instance
1216     */
1217    public function addTest($name, $test = null)
1218    {
1219        if (!$name instanceof TwigTest && !($test instanceof TwigTest || $test instanceof \Twig_TestInterface)) {
1220            throw new \LogicException('A test must be an instance of \Twig_TestInterface or \Twig_SimpleTest.');
1221        }
1222
1223        if ($name instanceof TwigTest) {
1224            $test = $name;
1225            $name = $test->getName();
1226        } else {
1227            @trigger_error(sprintf('Passing a name as a first argument to the %s method is deprecated since version 1.21. Pass an instance of "Twig_SimpleTest" instead when defining test "%s".', __METHOD__, $name), E_USER_DEPRECATED);
1228        }
1229
1230        if ($this->extensionInitialized) {
1231            throw new \LogicException(sprintf('Unable to add test "%s" as extensions have already been initialized.', $name));
1232        }
1233
1234        $this->staging->addTest($name, $test);
1235    }
1236
1237    /**
1238     * Gets the registered Tests.
1239     *
1240     * @return \Twig_TestInterface[]
1241     *
1242     * @internal
1243     */
1244    public function getTests()
1245    {
1246        if (!$this->extensionInitialized) {
1247            $this->initExtensions();
1248        }
1249
1250        return $this->tests;
1251    }
1252
1253    /**
1254     * Gets a test by name.
1255     *
1256     * @param string $name The test name
1257     *
1258     * @return \Twig_Test|false
1259     *
1260     * @internal
1261     */
1262    public function getTest($name)
1263    {
1264        if (!$this->extensionInitialized) {
1265            $this->initExtensions();
1266        }
1267
1268        if (isset($this->tests[$name])) {
1269            return $this->tests[$name];
1270        }
1271
1272        foreach ($this->tests as $pattern => $test) {
1273            $pattern = str_replace('\\*', '(.*?)', preg_quote($pattern, '#'), $count);
1274
1275            if ($count) {
1276                if (preg_match('#^'.$pattern.'$#', $name, $matches)) {
1277                    array_shift($matches);
1278                    $test->setArguments($matches);
1279
1280                    return $test;
1281                }
1282            }
1283        }
1284
1285        return false;
1286    }
1287
1288    /**
1289     * Registers a Function.
1290     *
1291     * @param string|TwigFunction                  $name     The function name or a \Twig_SimpleFunction instance
1292     * @param \Twig_FunctionInterface|TwigFunction $function
1293     */
1294    public function addFunction($name, $function = null)
1295    {
1296        if (!$name instanceof TwigFunction && !($function instanceof TwigFunction || $function instanceof \Twig_FunctionInterface)) {
1297            throw new \LogicException('A function must be an instance of \Twig_FunctionInterface or \Twig_SimpleFunction.');
1298        }
1299
1300        if ($name instanceof TwigFunction) {
1301            $function = $name;
1302            $name = $function->getName();
1303        } else {
1304            @trigger_error(sprintf('Passing a name as a first argument to the %s method is deprecated since version 1.21. Pass an instance of "Twig_SimpleFunction" instead when defining function "%s".', __METHOD__, $name), E_USER_DEPRECATED);
1305        }
1306
1307        if ($this->extensionInitialized) {
1308            throw new \LogicException(sprintf('Unable to add function "%s" as extensions have already been initialized.', $name));
1309        }
1310
1311        $this->staging->addFunction($name, $function);
1312    }
1313
1314    /**
1315     * Get a function by name.
1316     *
1317     * Subclasses may override this method and load functions differently;
1318     * so no list of functions is available.
1319     *
1320     * @param string $name function name
1321     *
1322     * @return \Twig_Function|false
1323     *
1324     * @internal
1325     */
1326    public function getFunction($name)
1327    {
1328        if (!$this->extensionInitialized) {
1329            $this->initExtensions();
1330        }
1331
1332        if (isset($this->functions[$name])) {
1333            return $this->functions[$name];
1334        }
1335
1336        foreach ($this->functions as $pattern => $function) {
1337            $pattern = str_replace('\\*', '(.*?)', preg_quote($pattern, '#'), $count);
1338
1339            if ($count) {
1340                if (preg_match('#^'.$pattern.'$#', $name, $matches)) {
1341                    array_shift($matches);
1342                    $function->setArguments($matches);
1343
1344                    return $function;
1345                }
1346            }
1347        }
1348
1349        foreach ($this->functionCallbacks as $callback) {
1350            if (false !== $function = \call_user_func($callback, $name)) {
1351                return $function;
1352            }
1353        }
1354
1355        return false;
1356    }
1357
1358    public function registerUndefinedFunctionCallback($callable)
1359    {
1360        $this->functionCallbacks[] = $callable;
1361    }
1362
1363    /**
1364     * Gets registered functions.
1365     *
1366     * Be warned that this method cannot return functions defined with registerUndefinedFunctionCallback.
1367     *
1368     * @return \Twig_FunctionInterface[]
1369     *
1370     * @see registerUndefinedFunctionCallback
1371     *
1372     * @internal
1373     */
1374    public function getFunctions()
1375    {
1376        if (!$this->extensionInitialized) {
1377            $this->initExtensions();
1378        }
1379
1380        return $this->functions;
1381    }
1382
1383    /**
1384     * Registers a Global.
1385     *
1386     * New globals can be added before compiling or rendering a template;
1387     * but after, you can only update existing globals.
1388     *
1389     * @param string $name  The global name
1390     * @param mixed  $value The global value
1391     */
1392    public function addGlobal($name, $value)
1393    {
1394        if ($this->extensionInitialized || $this->runtimeInitialized) {
1395            if (null === $this->globals) {
1396                $this->globals = $this->initGlobals();
1397            }
1398
1399            if (!\array_key_exists($name, $this->globals)) {
1400                // The deprecation notice must be turned into the following exception in Twig 2.0
1401                @trigger_error(sprintf('Registering global variable "%s" at runtime or when the extensions have already been initialized is deprecated since version 1.21.', $name), E_USER_DEPRECATED);
1402                //throw new \LogicException(sprintf('Unable to add global "%s" as the runtime or the extensions have already been initialized.', $name));
1403            }
1404        }
1405
1406        if ($this->extensionInitialized || $this->runtimeInitialized) {
1407            // update the value
1408            $this->globals[$name] = $value;
1409        } else {
1410            $this->staging->addGlobal($name, $value);
1411        }
1412    }
1413
1414    /**
1415     * Gets the registered Globals.
1416     *
1417     * @return array An array of globals
1418     *
1419     * @internal
1420     */
1421    public function getGlobals()
1422    {
1423        if (!$this->runtimeInitialized && !$this->extensionInitialized) {
1424            return $this->initGlobals();
1425        }
1426
1427        if (null === $this->globals) {
1428            $this->globals = $this->initGlobals();
1429        }
1430
1431        return $this->globals;
1432    }
1433
1434    /**
1435     * Merges a context with the defined globals.
1436     *
1437     * @param array $context An array representing the context
1438     *
1439     * @return array The context merged with the globals
1440     */
1441    public function mergeGlobals(array $context)
1442    {
1443        // we don't use array_merge as the context being generally
1444        // bigger than globals, this code is faster.
1445        foreach ($this->getGlobals() as $key => $value) {
1446            if (!\array_key_exists($key, $context)) {
1447                $context[$key] = $value;
1448            }
1449        }
1450
1451        return $context;
1452    }
1453
1454    /**
1455     * Gets the registered unary Operators.
1456     *
1457     * @return array An array of unary operators
1458     *
1459     * @internal
1460     */
1461    public function getUnaryOperators()
1462    {
1463        if (!$this->extensionInitialized) {
1464            $this->initExtensions();
1465        }
1466
1467        return $this->unaryOperators;
1468    }
1469
1470    /**
1471     * Gets the registered binary Operators.
1472     *
1473     * @return array An array of binary operators
1474     *
1475     * @internal
1476     */
1477    public function getBinaryOperators()
1478    {
1479        if (!$this->extensionInitialized) {
1480            $this->initExtensions();
1481        }
1482
1483        return $this->binaryOperators;
1484    }
1485
1486    /**
1487     * @deprecated since 1.23 (to be removed in 2.0)
1488     */
1489    public function computeAlternatives($name, $items)
1490    {
1491        @trigger_error(sprintf('The %s method is deprecated since version 1.23 and will be removed in Twig 2.0.', __METHOD__), E_USER_DEPRECATED);
1492
1493        return SyntaxError::computeAlternatives($name, $items);
1494    }
1495
1496    /**
1497     * @internal
1498     */
1499    protected function initGlobals()
1500    {
1501        $globals = [];
1502        foreach ($this->extensions as $name => $extension) {
1503            if (!$extension instanceof GlobalsInterface) {
1504                $m = new \ReflectionMethod($extension, 'getGlobals');
1505
1506                $parentClass = $m->getDeclaringClass()->getName();
1507                if ('Twig_Extension' !== $parentClass && 'Twig\Extension\AbstractExtension' !== $parentClass) {
1508                    @trigger_error(sprintf('Defining the getGlobals() method in the "%s" extension without explicitly implementing Twig\Extension\GlobalsInterface is deprecated since version 1.23.', $name), E_USER_DEPRECATED);
1509                }
1510            }
1511
1512            $extGlob = $extension->getGlobals();
1513            if (!\is_array($extGlob)) {
1514                throw new \UnexpectedValueException(sprintf('"%s::getGlobals()" must return an array of globals.', \get_class($extension)));
1515            }
1516
1517            $globals[] = $extGlob;
1518        }
1519
1520        $globals[] = $this->staging->getGlobals();
1521
1522        return \call_user_func_array('array_merge', $globals);
1523    }
1524
1525    /**
1526     * @internal
1527     */
1528    protected function initExtensions()
1529    {
1530        if ($this->extensionInitialized) {
1531            return;
1532        }
1533
1534        $this->parsers = new \Twig_TokenParserBroker([], [], false);
1535        $this->filters = [];
1536        $this->functions = [];
1537        $this->tests = [];
1538        $this->visitors = [];
1539        $this->unaryOperators = [];
1540        $this->binaryOperators = [];
1541
1542        foreach ($this->extensions as $extension) {
1543            $this->initExtension($extension);
1544        }
1545        $this->initExtension($this->staging);
1546        // Done at the end only, so that an exception during initialization does not mark the environment as initialized when catching the exception
1547        $this->extensionInitialized = true;
1548    }
1549
1550    /**
1551     * @internal
1552     */
1553    protected function initExtension(ExtensionInterface $extension)
1554    {
1555        // filters
1556        foreach ($extension->getFilters() as $name => $filter) {
1557            if ($filter instanceof TwigFilter) {
1558                $name = $filter->getName();
1559            } else {
1560                @trigger_error(sprintf('Using an instance of "%s" for filter "%s" is deprecated since version 1.21. Use \Twig_SimpleFilter instead.', \get_class($filter), $name), E_USER_DEPRECATED);
1561            }
1562
1563            $this->filters[$name] = $filter;
1564        }
1565
1566        // functions
1567        foreach ($extension->getFunctions() as $name => $function) {
1568            if ($function instanceof TwigFunction) {
1569                $name = $function->getName();
1570            } else {
1571                @trigger_error(sprintf('Using an instance of "%s" for function "%s" is deprecated since version 1.21. Use \Twig_SimpleFunction instead.', \get_class($function), $name), E_USER_DEPRECATED);
1572            }
1573
1574            $this->functions[$name] = $function;
1575        }
1576
1577        // tests
1578        foreach ($extension->getTests() as $name => $test) {
1579            if ($test instanceof TwigTest) {
1580                $name = $test->getName();
1581            } else {
1582                @trigger_error(sprintf('Using an instance of "%s" for test "%s" is deprecated since version 1.21. Use \Twig_SimpleTest instead.', \get_class($test), $name), E_USER_DEPRECATED);
1583            }
1584
1585            $this->tests[$name] = $test;
1586        }
1587
1588        // token parsers
1589        foreach ($extension->getTokenParsers() as $parser) {
1590            if ($parser instanceof TokenParserInterface) {
1591                $this->parsers->addTokenParser($parser);
1592            } elseif ($parser instanceof \Twig_TokenParserBrokerInterface) {
1593                @trigger_error('Registering a \Twig_TokenParserBrokerInterface instance is deprecated since version 1.21.', E_USER_DEPRECATED);
1594
1595                $this->parsers->addTokenParserBroker($parser);
1596            } else {
1597                throw new \LogicException('getTokenParsers() must return an array of \Twig_TokenParserInterface or \Twig_TokenParserBrokerInterface instances.');
1598            }
1599        }
1600
1601        // node visitors
1602        foreach ($extension->getNodeVisitors() as $visitor) {
1603            $this->visitors[] = $visitor;
1604        }
1605
1606        // operators
1607        if ($operators = $extension->getOperators()) {
1608            if (!\is_array($operators)) {
1609                throw new \InvalidArgumentException(sprintf('"%s::getOperators()" must return an array with operators, got "%s".', \get_class($extension), \is_object($operators) ? \get_class($operators) : \gettype($operators).(\is_resource($operators) ? '' : '#'.$operators)));
1610            }
1611
1612            if (2 !== \count($operators)) {
1613                throw new \InvalidArgumentException(sprintf('"%s::getOperators()" must return an array of 2 elements, got %d.', \get_class($extension), \count($operators)));
1614            }
1615
1616            $this->unaryOperators = array_merge($this->unaryOperators, $operators[0]);
1617            $this->binaryOperators = array_merge($this->binaryOperators, $operators[1]);
1618        }
1619    }
1620
1621    /**
1622     * @deprecated since 1.22 (to be removed in 2.0)
1623     */
1624    protected function writeCacheFile($file, $content)
1625    {
1626        $this->cache->write($file, $content);
1627    }
1628
1629    private function updateOptionsHash()
1630    {
1631        $hashParts = array_merge(
1632            array_keys($this->extensions),
1633            [
1634                (int) \function_exists('twig_template_get_attributes'),
1635                PHP_MAJOR_VERSION,
1636                PHP_MINOR_VERSION,
1637                self::VERSION,
1638                (int) $this->debug,
1639                $this->baseTemplateClass,
1640                (int) $this->strictVariables,
1641            ]
1642        );
1643        $this->optionsHash = implode(':', $hashParts);
1644    }
1645}
1646
1647class_alias('Twig\Environment', 'Twig_Environment');
1648