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

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. * @return $this
  277. */
  278. public function unwrap(): self
  279. {
  280. return $this;
  281. }
  282. /**
  283. * Returns all blocks.
  284. *
  285. * This method is for internal use only and should never be called
  286. * directly.
  287. *
  288. * @return array An array of blocks
  289. */
  290. public function getBlocks(): array
  291. {
  292. return $this->blocks;
  293. }
  294. public function display(array $context, array $blocks = []): void
  295. {
  296. foreach ($this->yield($context, $blocks) as $data) {
  297. echo $data;
  298. }
  299. }
  300. public function render(array $context): string
  301. {
  302. if (!$this->useYield) {
  303. $level = ob_get_level();
  304. if ($this->env->isDebug()) {
  305. ob_start();
  306. } else {
  307. ob_start(function () { return ''; });
  308. }
  309. try {
  310. $this->display($context);
  311. } catch (\Throwable $e) {
  312. while (ob_get_level() > $level) {
  313. ob_end_clean();
  314. }
  315. throw $e;
  316. }
  317. return ob_get_clean();
  318. }
  319. $content = '';
  320. foreach ($this->yield($context) as $data) {
  321. $content .= $data;
  322. }
  323. return $content;
  324. }
  325. /**
  326. * @return iterable<scalar|\Stringable|null>
  327. */
  328. public function yield(array $context, array $blocks = []): iterable
  329. {
  330. $context += $this->env->getGlobals();
  331. $blocks = array_merge($this->blocks, $blocks);
  332. try {
  333. yield from $this->doDisplay($context, $blocks);
  334. } catch (Error $e) {
  335. if (!$e->getSourceContext()) {
  336. $e->setSourceContext($this->getSourceContext());
  337. }
  338. // this is mostly useful for \Twig\Error\LoaderError exceptions
  339. // see \Twig\Error\LoaderError
  340. if (-1 === $e->getTemplateLine()) {
  341. $e->guess();
  342. }
  343. throw $e;
  344. } catch (\Throwable $e) {
  345. $e = new RuntimeError(\sprintf('An exception has been thrown during the rendering of a template ("%s").', $e->getMessage()), -1, $this->getSourceContext(), $e);
  346. $e->guess();
  347. throw $e;
  348. }
  349. }
  350. /**
  351. * @return iterable<scalar|\Stringable|null>
  352. */
  353. public function yieldBlock($name, array $context, array $blocks = [], $useBlocks = true, ?self $templateContext = null): iterable
  354. {
  355. if ($useBlocks && isset($blocks[$name])) {
  356. $template = $blocks[$name][0];
  357. $block = $blocks[$name][1];
  358. } elseif (isset($this->blocks[$name])) {
  359. $template = $this->blocks[$name][0];
  360. $block = $this->blocks[$name][1];
  361. } else {
  362. $template = null;
  363. $block = null;
  364. }
  365. // avoid RCEs when sandbox is enabled
  366. if (null !== $template && !$template instanceof self) {
  367. throw new \LogicException('A block must be a method on a \Twig\Template instance.');
  368. }
  369. if (null !== $template) {
  370. try {
  371. yield from $template->$block($context, $blocks);
  372. } catch (Error $e) {
  373. if (!$e->getSourceContext()) {
  374. $e->setSourceContext($template->getSourceContext());
  375. }
  376. // this is mostly useful for \Twig\Error\LoaderError exceptions
  377. // see \Twig\Error\LoaderError
  378. if (-1 === $e->getTemplateLine()) {
  379. $e->guess();
  380. }
  381. throw $e;
  382. } catch (\Throwable $e) {
  383. $e = new RuntimeError(\sprintf('An exception has been thrown during the rendering of a template ("%s").', $e->getMessage()), -1, $template->getSourceContext(), $e);
  384. $e->guess();
  385. throw $e;
  386. }
  387. } elseif ($parent = $this->getParent($context)) {
  388. yield from $parent->unwrap()->yieldBlock($name, $context, array_merge($this->blocks, $blocks), false, $templateContext ?? $this);
  389. } elseif (isset($blocks[$name])) {
  390. 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());
  391. } else {
  392. throw new RuntimeError(\sprintf('Block "%s" on template "%s" does not exist.', $name, $this->getTemplateName()), -1, ($templateContext ?? $this)->getSourceContext());
  393. }
  394. }
  395. /**
  396. * Yields a parent block.
  397. *
  398. * This method is for internal use only and should never be called
  399. * directly.
  400. *
  401. * @param string $name The block name to display from the parent
  402. * @param array $context The context
  403. * @param array $blocks The current set of blocks
  404. *
  405. * @return iterable<scalar|\Stringable|null>
  406. */
  407. public function yieldParentBlock($name, array $context, array $blocks = []): iterable
  408. {
  409. if (isset($this->traits[$name])) {
  410. yield from $this->traits[$name][0]->yieldBlock($this->traitAliases[$name] ?? $name, $context, $blocks, false);
  411. } elseif ($parent = $this->getParent($context)) {
  412. yield from $parent->unwrap()->yieldBlock($name, $context, $blocks, false);
  413. } else {
  414. throw new RuntimeError(\sprintf('The template has no parent and no traits defining the "%s" block.', $name), -1, $this->getSourceContext());
  415. }
  416. }
  417. protected function hasMacro(string $name, array $context): bool
  418. {
  419. if (method_exists($this, $name)) {
  420. return true;
  421. }
  422. if (!$parent = $this->getParent($context)) {
  423. return false;
  424. }
  425. return $parent->hasMacro($name, $context);
  426. }
  427. protected function getTemplateForMacro(string $name, array $context, int $line, Source $source): Template
  428. {
  429. if (method_exists($this, $name)) {
  430. return $this;
  431. }
  432. $parent = $this;
  433. while ($parent = $parent->getParent($context)) {
  434. if (method_exists($parent, $name)) {
  435. return $parent;
  436. }
  437. }
  438. throw new RuntimeError(\sprintf('Macro "%s" is not defined in template "%s".', substr($name, \strlen('macro_')), $this->getTemplateName()), $line, $source);
  439. }
  440. /**
  441. * Auto-generated method to display the template with the given context.
  442. *
  443. * @param array $context An array of parameters to pass to the template
  444. * @param array $blocks An array of blocks to pass to the template
  445. *
  446. * @return iterable<scalar|\Stringable|null>
  447. */
  448. abstract protected function doDisplay(array $context, array $blocks = []): iterable;
  449. }