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