vendor/twig/twig/src/Template.php line 359

Open in your IDE?
  1. <?php
  2. /*
  3. * This file is part of Twig.
  4. *
  5. * (c) Fabien Potencier
  6. * (c) Armin Ronacher
  7. *
  8. * For the full copyright and license information, please view the LICENSE
  9. * file that was distributed with this source code.
  10. */
  11. namespace Twig;
  12. use Twig\Error\Error;
  13. use Twig\Error\RuntimeError;
  14. /**
  15. * Default base class for compiled templates.
  16. *
  17. * This class is an implementation detail of how template compilation currently
  18. * works, which might change. It should never be used directly. Use $twig->load()
  19. * instead, which returns an instance of \Twig\TemplateWrapper.
  20. *
  21. * @author Fabien Potencier <fabien@symfony.com>
  22. *
  23. * @internal
  24. */
  25. abstract class Template
  26. {
  27. public const ANY_CALL = 'any';
  28. public const ARRAY_CALL = 'array';
  29. public const METHOD_CALL = 'method';
  30. protected $parent;
  31. protected $parents = [];
  32. protected $blocks = [];
  33. protected $traits = [];
  34. protected $traitAliases = [];
  35. protected $extensions = [];
  36. protected $sandbox;
  37. private $useYield;
  38. public function __construct(
  39. protected Environment $env,
  40. ) {
  41. $this->useYield = $env->useYield();
  42. $this->extensions = $env->getExtensions();
  43. }
  44. /**
  45. * Returns the template name.
  46. */
  47. abstract public function getTemplateName(): string;
  48. /**
  49. * Returns debug information about the template.
  50. *
  51. * @return array<int, int> Debug information
  52. */
  53. abstract public function getDebugInfo(): array;
  54. /**
  55. * Returns information about the original template source code.
  56. */
  57. abstract public function getSourceContext(): Source;
  58. /**
  59. * Returns the parent template.
  60. *
  61. * This method is for internal use only and should never be called
  62. * directly.
  63. *
  64. * @return self|TemplateWrapper|false The parent template or false if there is no parent
  65. */
  66. public function getParent(array $context): self|TemplateWrapper|false
  67. {
  68. if (null !== $this->parent) {
  69. return $this->parent;
  70. }
  71. if (!$parent = $this->doGetParent($context)) {
  72. return false;
  73. }
  74. if ($parent instanceof self || $parent instanceof TemplateWrapper) {
  75. return $this->parents[$parent->getSourceContext()->getName()] = $parent;
  76. }
  77. if (!isset($this->parents[$parent])) {
  78. $this->parents[$parent] = $this->loadTemplate($parent);
  79. }
  80. return $this->parents[$parent];
  81. }
  82. protected function doGetParent(array $context): bool|string|self|TemplateWrapper
  83. {
  84. return false;
  85. }
  86. public function isTraitable(): bool
  87. {
  88. return true;
  89. }
  90. /**
  91. * Displays a parent block.
  92. *
  93. * This method is for internal use only and should never be called
  94. * directly.
  95. *
  96. * @param string $name The block name to display from the parent
  97. * @param array $context The context
  98. * @param array $blocks The current set of blocks
  99. */
  100. public function displayParentBlock($name, array $context, array $blocks = []): void
  101. {
  102. foreach ($this->yieldParentBlock($name, $context, $blocks) as $data) {
  103. echo $data;
  104. }
  105. }
  106. /**
  107. * Displays a block.
  108. *
  109. * This method is for internal use only and should never be called
  110. * directly.
  111. *
  112. * @param string $name The block name to display
  113. * @param array $context The context
  114. * @param array $blocks The current set of blocks
  115. * @param bool $useBlocks Whether to use the current set of blocks
  116. */
  117. public function displayBlock($name, array $context, array $blocks = [], $useBlocks = true, ?self $templateContext = null): void
  118. {
  119. foreach ($this->yieldBlock($name, $context, $blocks, $useBlocks, $templateContext) as $data) {
  120. echo $data;
  121. }
  122. }
  123. /**
  124. * Renders a parent block.
  125. *
  126. * This method is for internal use only and should never be called
  127. * directly.
  128. *
  129. * @param string $name The block name to render from the parent
  130. * @param array $context The context
  131. * @param array $blocks The current set of blocks
  132. *
  133. * @return string The rendered block
  134. */
  135. public function renderParentBlock($name, array $context, array $blocks = []): string
  136. {
  137. if (!$this->useYield) {
  138. if ($this->env->isDebug()) {
  139. ob_start();
  140. } else {
  141. ob_start(function () { return ''; });
  142. }
  143. $this->displayParentBlock($name, $context, $blocks);
  144. return ob_get_clean();
  145. }
  146. $content = '';
  147. foreach ($this->yieldParentBlock($name, $context, $blocks) as $data) {
  148. $content .= $data;
  149. }
  150. return $content;
  151. }
  152. /**
  153. * Renders a block.
  154. *
  155. * This method is for internal use only and should never be called
  156. * directly.
  157. *
  158. * @param string $name The block name to render
  159. * @param array $context The context
  160. * @param array $blocks The current set of blocks
  161. * @param bool $useBlocks Whether to use the current set of blocks
  162. *
  163. * @return string The rendered block
  164. */
  165. public function renderBlock($name, array $context, array $blocks = [], $useBlocks = true): string
  166. {
  167. if (!$this->useYield) {
  168. $level = ob_get_level();
  169. if ($this->env->isDebug()) {
  170. ob_start();
  171. } else {
  172. ob_start(function () { return ''; });
  173. }
  174. try {
  175. $this->displayBlock($name, $context, $blocks, $useBlocks);
  176. } catch (\Throwable $e) {
  177. while (ob_get_level() > $level) {
  178. ob_end_clean();
  179. }
  180. throw $e;
  181. }
  182. return ob_get_clean();
  183. }
  184. $content = '';
  185. foreach ($this->yieldBlock($name, $context, $blocks, $useBlocks) as $data) {
  186. $content .= $data;
  187. }
  188. return $content;
  189. }
  190. /**
  191. * Returns whether a block exists or not in the current context of the template.
  192. *
  193. * This method checks blocks defined in the current template
  194. * or defined in "used" traits or defined in parent templates.
  195. *
  196. * @param string $name The block name
  197. * @param array $context The context
  198. * @param array $blocks The current set of blocks
  199. *
  200. * @return bool true if the block exists, false otherwise
  201. */
  202. public function hasBlock($name, array $context, array $blocks = []): bool
  203. {
  204. if (isset($blocks[$name])) {
  205. return $blocks[$name][0] instanceof self;
  206. }
  207. if (isset($this->blocks[$name])) {
  208. return true;
  209. }
  210. if ($parent = $this->getParent($context)) {
  211. return $parent->hasBlock($name, $context);
  212. }
  213. return false;
  214. }
  215. /**
  216. * Returns all block names in the current context of the template.
  217. *
  218. * This method checks blocks defined in the current template
  219. * or defined in "used" traits or defined in parent templates.
  220. *
  221. * @param array $context The context
  222. * @param array $blocks The current set of blocks
  223. *
  224. * @return array<string> An array of block names
  225. */
  226. public function getBlockNames(array $context, array $blocks = []): array
  227. {
  228. $names = array_merge(array_keys($blocks), array_keys($this->blocks));
  229. if ($parent = $this->getParent($context)) {
  230. $names = array_merge($names, $parent->getBlockNames($context));
  231. }
  232. return array_unique($names);
  233. }
  234. /**
  235. * @param string|TemplateWrapper|array<string|TemplateWrapper> $template
  236. */
  237. protected function loadTemplate($template, $templateName = null, $line = null, $index = null): self|TemplateWrapper
  238. {
  239. try {
  240. if (\is_array($template)) {
  241. return $this->env->resolveTemplate($template);
  242. }
  243. if ($template instanceof TemplateWrapper) {
  244. return $template;
  245. }
  246. if ($template instanceof self) {
  247. trigger_deprecation('twig/twig', '3.9', 'Passing a "%s" instance to "%s" is deprecated.', self::class, __METHOD__);
  248. return $template;
  249. }
  250. if ($template === $this->getTemplateName()) {
  251. $class = static::class;
  252. if (false !== $pos = strrpos($class, '___', -1)) {
  253. $class = substr($class, 0, $pos);
  254. }
  255. } else {
  256. $class = $this->env->getTemplateClass($template);
  257. }
  258. return $this->env->loadTemplate($class, $template, $index);
  259. } catch (Error $e) {
  260. if (!$e->getSourceContext()) {
  261. $e->setSourceContext($templateName ? new Source('', $templateName) : $this->getSourceContext());
  262. }
  263. if ($e->getTemplateLine() > 0) {
  264. throw $e;
  265. }
  266. if (!$line) {
  267. $e->guess();
  268. } else {
  269. $e->setTemplateLine($line);
  270. }
  271. throw $e;
  272. }
  273. }
  274. /**
  275. * @internal
  276. *
  277. * @return $this
  278. */
  279. public function unwrap(): self
  280. {
  281. return $this;
  282. }
  283. /**
  284. * Returns all blocks.
  285. *
  286. * This method is for internal use only and should never be called
  287. * directly.
  288. *
  289. * @return array An array of blocks
  290. */
  291. public function getBlocks(): array
  292. {
  293. return $this->blocks;
  294. }
  295. public function display(array $context, array $blocks = []): void
  296. {
  297. foreach ($this->yield($context, $blocks) as $data) {
  298. echo $data;
  299. }
  300. }
  301. public function render(array $context): string
  302. {
  303. if (!$this->useYield) {
  304. $level = ob_get_level();
  305. if ($this->env->isDebug()) {
  306. ob_start();
  307. } else {
  308. ob_start(function () { return ''; });
  309. }
  310. try {
  311. $this->display($context);
  312. } catch (\Throwable $e) {
  313. while (ob_get_level() > $level) {
  314. ob_end_clean();
  315. }
  316. throw $e;
  317. }
  318. return ob_get_clean();
  319. }
  320. $content = '';
  321. foreach ($this->yield($context) as $data) {
  322. $content .= $data;
  323. }
  324. return $content;
  325. }
  326. /**
  327. * @return iterable<scalar|\Stringable|null>
  328. */
  329. public function yield(array $context, array $blocks = []): iterable
  330. {
  331. $context += $this->env->getGlobals();
  332. $blocks = array_merge($this->blocks, $blocks);
  333. try {
  334. yield from $this->doDisplay($context, $blocks);
  335. } catch (Error $e) {
  336. if (!$e->getSourceContext()) {
  337. $e->setSourceContext($this->getSourceContext());
  338. }
  339. // this is mostly useful for \Twig\Error\LoaderError exceptions
  340. // see \Twig\Error\LoaderError
  341. if (-1 === $e->getTemplateLine()) {
  342. $e->guess();
  343. }
  344. throw $e;
  345. } catch (\Throwable $e) {
  346. $e = new RuntimeError(\sprintf('An exception has been thrown during the rendering of a template ("%s").', $e->getMessage()), -1, $this->getSourceContext(), $e);
  347. $e->guess();
  348. throw $e;
  349. }
  350. }
  351. /**
  352. * @return iterable<scalar|\Stringable|null>
  353. */
  354. public function yieldBlock($name, array $context, array $blocks = [], $useBlocks = true, ?self $templateContext = null): iterable
  355. {
  356. if ($useBlocks && isset($blocks[$name])) {
  357. $template = $blocks[$name][0];
  358. $block = $blocks[$name][1];
  359. } elseif (isset($this->blocks[$name])) {
  360. $template = $this->blocks[$name][0];
  361. $block = $this->blocks[$name][1];
  362. } else {
  363. $template = null;
  364. $block = null;
  365. }
  366. // avoid RCEs when sandbox is enabled
  367. if (null !== $template && !$template instanceof self) {
  368. throw new \LogicException('A block must be a method on a \Twig\Template instance.');
  369. }
  370. if (null !== $template) {
  371. try {
  372. yield from $template->$block($context, $blocks);
  373. } catch (Error $e) {
  374. if (!$e->getSourceContext()) {
  375. $e->setSourceContext($template->getSourceContext());
  376. }
  377. // this is mostly useful for \Twig\Error\LoaderError exceptions
  378. // see \Twig\Error\LoaderError
  379. if (-1 === $e->getTemplateLine()) {
  380. $e->guess();
  381. }
  382. throw $e;
  383. } catch (\Throwable $e) {
  384. $e = new RuntimeError(\sprintf('An exception has been thrown during the rendering of a template ("%s").', $e->getMessage()), -1, $template->getSourceContext(), $e);
  385. $e->guess();
  386. throw $e;
  387. }
  388. } elseif ($parent = $this->getParent($context)) {
  389. yield from $parent->unwrap()->yieldBlock($name, $context, array_merge($this->blocks, $blocks), false, $templateContext ?? $this);
  390. } elseif (isset($blocks[$name])) {
  391. throw new RuntimeError(\sprintf('Block "%s" should not call parent() in "%s" as the block does not exist in the parent template "%s".', $name, $blocks[$name][0]->getTemplateName(), $this->getTemplateName()), -1, $blocks[$name][0]->getSourceContext());
  392. } else {
  393. throw new RuntimeError(\sprintf('Block "%s" on template "%s" does not exist.', $name, $this->getTemplateName()), -1, ($templateContext ?? $this)->getSourceContext());
  394. }
  395. }
  396. /**
  397. * Yields a parent block.
  398. *
  399. * This method is for internal use only and should never be called
  400. * directly.
  401. *
  402. * @param string $name The block name to display from the parent
  403. * @param array $context The context
  404. * @param array $blocks The current set of blocks
  405. *
  406. * @return iterable<scalar|\Stringable|null>
  407. */
  408. public function yieldParentBlock($name, array $context, array $blocks = []): iterable
  409. {
  410. if (isset($this->traits[$name])) {
  411. yield from $this->traits[$name][0]->yieldBlock($this->traitAliases[$name] ?? $name, $context, $blocks, false);
  412. } elseif ($parent = $this->getParent($context)) {
  413. yield from $parent->unwrap()->yieldBlock($name, $context, $blocks, false);
  414. } else {
  415. throw new RuntimeError(\sprintf('The template has no parent and no traits defining the "%s" block.', $name), -1, $this->getSourceContext());
  416. }
  417. }
  418. protected function hasMacro(string $name, array $context): bool
  419. {
  420. if (method_exists($this, $name)) {
  421. return true;
  422. }
  423. if (!$parent = $this->getParent($context)) {
  424. return false;
  425. }
  426. return $parent->hasMacro($name, $context);
  427. }
  428. protected function getTemplateForMacro(string $name, array $context, int $line, Source $source): self
  429. {
  430. if (method_exists($this, $name)) {
  431. return $this;
  432. }
  433. $parent = $this;
  434. while ($parent = $parent->getParent($context)) {
  435. if (method_exists($parent, $name)) {
  436. return $parent;
  437. }
  438. }
  439. throw new RuntimeError(\sprintf('Macro "%s" is not defined in template "%s".', substr($name, \strlen('macro_')), $this->getTemplateName()), $line, $source);
  440. }
  441. /**
  442. * Auto-generated method to display the template with the given context.
  443. *
  444. * @param array $context An array of parameters to pass to the template
  445. * @param array $blocks An array of blocks to pass to the template
  446. *
  447. * @return iterable<scalar|\Stringable|null>
  448. */
  449. abstract protected function doDisplay(array $context, array $blocks = []): iterable;
  450. }