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

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