Skip to content

Commit 59b9548

Browse files
committed
{include parent} and {include this} work inside dynamically named blocks
The name of the enclosing block is resolved at runtime from the stack of blocks being currently rendered, which is shared along the inheritance chain. Previously this was a compile error, or, when the dynamic block was nested inside a static one, the include silently bound to the outer static block. To be released in 3.1.
1 parent bd81f98 commit 59b9548

5 files changed

Lines changed: 75 additions & 15 deletions

File tree

docs/internals/blocks.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,19 @@ the same conservative walk (`findBlockDeclarations`: any statement that is not
6060
a proven inline wrapper may compile its content into a separate method, so it
6161
resets the "top level" status).
6262

63+
## {include parent}/{include this} in dynamically named blocks
64+
65+
Inside a block with a **static** name, `{include parent}`/`{include this}`
66+
resolve the name at compile time from the closest enclosing block. Inside a
67+
**dynamically named** block the compiler emits `null` and the runtime takes the
68+
name from `Template::$renderingBlocks` — a stack of names of blocks currently
69+
being rendered, pushed/popped by `renderBlock()` and **shared by reference
70+
along the inheritance chain** (`createTemplate`, like the block registry; a
71+
block function executes bound to its defining instance while `renderBlock` runs
72+
on another one). Note the closest-block search no longer skips dynamic blocks:
73+
`{include parent}` nested in a dynamic block inside a static one now refers to
74+
the dynamic block's runtime name, not silently to the outer static block.
75+
6376
The linter's `TemplateReferenceCheck` currently verifies only that **statically
6477
named template paths exist** (`{include}`, `{import}`, `{extends}`/`{layout}`,
6578
`{embed file}`, `{sandbox}`, and the `from` of `{include block from}`), resolving

src/Latte/Essential/Nodes/IncludeBlockNode.php

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
*/
2727
class IncludeBlockNode extends StatementNode
2828
{
29-
public ExpressionNode $name;
29+
/** null in {include parent}/{include this} inside a dynamically named block; the name is taken from the runtime */
30+
public ?ExpressionNode $name = null;
3031
public ?ExpressionNode $from = null;
3132
public ArrayNode $args;
3233
public ModifierNode $modifier;
@@ -64,14 +65,17 @@ public static function create(Tag $tag, TemplateParser $parser): static
6465
$item = $tag->closestTag(
6566
[BlockNode::class, DefineNode::class],
6667
fn($item) => ($item->node instanceof BlockNode || $item->node instanceof DefineNode)
67-
&& $item->node->block && !$item->node->block->isDynamic(),
68+
&& $item->node->block
69+
&& (!$item->node->block->isDynamic() || !$node->from),
6870
);
6971
if (!$item || !($item->node instanceof BlockNode || $item->node instanceof DefineNode)) {
7072
throw new CompileException("Cannot include $tokenName->text block outside of any block.", $tag->position);
7173
}
7274

7375
assert($item->node->block !== null);
74-
$node->name = $item->node->block->name;
76+
$node->name = $item->node->block->isDynamic()
77+
? null // the name of the enclosing block is known only at runtime
78+
: $item->node->block->name;
7579
}
7680

7781
$node->modifier->escape = !$node->modifier->removeFilter('noescape') && !$node->parent;
@@ -106,9 +110,9 @@ private function printBlock(PrintContext $context, string $contentFilter): strin
106110
return $context->format(
107111
'$this->render' . ($this->parent ? 'ParentBlock' : 'Block')
108112
. '(%raw, %node? + '
109-
. (isset($block) && !$block->parameters ? 'get_defined_vars()' : '[]')
113+
. ($this->name === null || (isset($block) && !$block->parameters) ? 'get_defined_vars()' : '[]')
110114
. '%raw) %line;',
111-
$context->ensureString($this->name, 'Block name'),
115+
$this->name === null ? 'null' : $context->ensureString($this->name, 'Block name'),
112116
$this->args,
113117
$contentFilter ? ", $contentFilter" : '',
114118
$this->position,
@@ -118,7 +122,7 @@ private function printBlock(PrintContext $context, string $contentFilter): strin
118122

119123
private function printBlockFrom(PrintContext $context, string $contentFilter): string
120124
{
121-
assert($this->from !== null);
125+
assert($this->from !== null && $this->name !== null);
122126
return $context->format(
123127
'$this->createTemplate(%raw, %node? + $this->params, "include")->renderToContentType(%raw, %raw) %line;',
124128
$context->ensureString($this->from, 'Template name'),
@@ -132,7 +136,9 @@ private function printBlockFrom(PrintContext $context, string $contentFilter): s
132136

133137
public function &getIterator(): \Generator
134138
{
135-
yield $this->name;
139+
if ($this->name) {
140+
yield $this->name;
141+
}
136142
if ($this->from) {
137143
yield $this->from;
138144
}

src/Latte/Runtime/Template.php

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ class Template
4444

4545
/** @var mixed[][] */
4646
private array $blockStack = [];
47+
48+
/** @var string[] names of blocks being currently rendered */
49+
private array $renderingBlocks = [];
4750
private bool $collectingBlocks = false;
4851
private ?Template $referringTemplate = null;
4952
private ?string $referenceType = null;
@@ -111,7 +114,7 @@ public function render(?string $block = null): void
111114
* @internal
112115
*/
113116
public function renderBlock(
114-
string $name,
117+
?string $name,
115118
array $params,
116119
string|\Closure|null $mod = null,
117120
int|string|null $layer = null,
@@ -121,6 +124,8 @@ public function renderBlock(
121124
return;
122125
}
123126

127+
$name ??= end($this->renderingBlocks)
128+
?: throw new Latte\RuntimeException('Cannot include this block outside of any block.');
124129
$block = $layer
125130
? ($this->blocks[$layer][$name] ?? null)
126131
: ($this->blocks[self::LayerLocal][$name] ?? $this->blocks[self::LayerTop][$name] ?? null);
@@ -135,12 +140,17 @@ public function renderBlock(
135140

136141
$fn = reset($block->functions);
137142
assert($fn !== false);
138-
$this->filter(
139-
fn() => $fn($params),
140-
$mod,
141-
$block->contentType ?? static::ContentType,
142-
"block $name",
143-
);
143+
$this->renderingBlocks[] = $name;
144+
try {
145+
$this->filter(
146+
fn() => $fn($params),
147+
$mod,
148+
$block->contentType ?? static::ContentType,
149+
"block $name",
150+
);
151+
} finally {
152+
array_pop($this->renderingBlocks);
153+
}
144154
}
145155

146156

@@ -149,8 +159,10 @@ public function renderBlock(
149159
* @param mixed[] $params
150160
* @internal
151161
*/
152-
public function renderParentBlock(string $name, array $params): void
162+
public function renderParentBlock(?string $name, array $params): void
153163
{
164+
$name ??= end($this->renderingBlocks)
165+
?: throw new Latte\RuntimeException('Cannot include parent block outside of any block.');
154166
$block = $this->blocks[self::LayerLocal][$name] ?? $this->blocks[self::LayerTop][$name] ?? null;
155167
if (!$block || ($function = next($block->functions)) === false) {
156168
throw new Latte\RuntimeException("Cannot include undefined parent block '$name'.");
@@ -200,6 +212,8 @@ public function createTemplate(string $name, array $params, string $relation): s
200212

201213
$this->blocks[self::LayerSnippet] += $child->blocks[self::LayerSnippet];
202214
$child->blocks[self::LayerSnippet] = &$this->blocks[self::LayerSnippet];
215+
216+
$child->renderingBlocks = &$this->renderingBlocks;
203217
}
204218

205219
return $child;

tests/tags/block.dynamic.phpt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,11 @@ Assert::exception(
5353
InvalidArgumentException::class,
5454
'Block name must be a string, null given.',
5555
);
56+
57+
58+
// {include parent} inside a dynamically named block resolves the name at runtime
59+
Assert::exception(
60+
fn() => createLatte()->renderToString('{var $n = x}{block $n}A {include parent}{/block}'),
61+
Latte\RuntimeException::class,
62+
"Cannot include undefined parent block 'x'.",
63+
);

tests/tags/embed.file.phpt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1111,3 +1111,22 @@ Assert::exception(
11111111
Latte\CompileException::class,
11121112
'%a?%Cannot redeclare block %a%',
11131113
);
1114+
1115+
1116+
// dynamically named block in a condition combined with {include parent}
1117+
$latte = new Latte\Engine;
1118+
$latte->setLoader(new Latte\Loaders\StringLoader([
1119+
'embed.latte' => '<b>{block a}orig-a{/block}</b>',
1120+
'main' => <<<'XX'
1121+
{embed "embed.latte"}
1122+
{var $a = a}
1123+
{if true}
1124+
{block $a}dynamic-A {include parent}{/block}
1125+
{else}
1126+
{block $a}never-A {include parent}{/block}
1127+
{/if}
1128+
{/embed}
1129+
XX,
1130+
]));
1131+
1132+
Assert::match('<b>dynamic-A orig-a</b>', trim($latte->renderToString('main')));

0 commit comments

Comments
 (0)