Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 52 additions & 1 deletion src/Template/Snippet.php
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,48 @@ public static function end(): void
echo static::$current?->render();
}

/**
* Closes any potentially open layout snippet.
*
* @param Snippet|null $outsideSnippet The expected value for `Snippet::$current`.
* @param string|null $template The content of the currently processed template or snippet.
* @param array $data Additional data provided to any potential layout snippet.
* @return string|null The new adjusted content of the template if `$template` was given.
*/
public static function endlayout(Snippet|null $outsideSnippet, string|null $template = null, array $data = []): string|null
{
// if last `endsnippet()` inside the current template
// has been omitted (= snippet was used as layout snippet),
// `Snippet::$current` will point to a snippet that was
// opened inside the template; if that snippet is the direct
// child of the snippet that was open before the template was
// rendered (which could be `null` if no snippet was open),
// take the buffer output from the template as default slot
// and render the snippet as final template output
if (
Snippet::$current === null ||
Snippet::$current->parent() !== $outsideSnippet
) {
return $template;
}

if (!isset($template)) {
// let the snippet close and render natively
echo static::$current?->render($data);
return null;
}
if (Snippet::$current->slots()->count() === 0) {
// no slots have been defined, but the template code
// should be used as default slot
return Snippet::$current->render($data, [
'default' => $template
]);
}
// swallow any "unslotted" content
// between start and end
return Snippet::$current->render($data);
}

/**
* Closes the last openend slot
*/
Expand Down Expand Up @@ -247,7 +289,16 @@ public function render(array $data = [], array $slots = []): string
// custom data overrides for the data that was passed to the snippet instance
$data = array_replace_recursive($this->data, $data);

return static::load($this->file, static::scope($data, $this->slots()));
// if the template is rendered inside a snippet,
// we need to keep the "outside" snippet object
// to compare it later
$snippet = Snippet::$current;

// load the template representing this snippet
$template = static::load($this->file, static::scope($data, $this->slots()));

// handle any potentially open layout snippet
return static::endlayout($snippet, $template);
}

/**
Expand Down
27 changes: 2 additions & 25 deletions src/Template/Template.php
Original file line number Diff line number Diff line change
Expand Up @@ -163,31 +163,8 @@ public function render(array $data = []): string
// load the template
$template = Tpl::load($this->file(), $data);

// if last `endsnippet()` inside the current template
// has been omitted (= snippet was used as layout snippet),
// `Snippet::$current` will point to a snippet that was
// opened inside the template; if that snippet is the direct
// child of the snippet that was open before the template was
// rendered (which could be `null` if no snippet was open),
// take the buffer output from the template as default slot
// and render the snippet as final template output
if (
Snippet::$current === null ||
Snippet::$current->parent() !== $snippet
) {
return $template;
}

// no slots have been defined, but the template code
// should be used as default slot
if (Snippet::$current->slots()->count() === 0) {
return Snippet::$current->render($data, [
'default' => $template
]);
}

// let the snippet close and render natively
return Snippet::$current->render($data);
// handle any potentially open layout snippet
return Snippet::endlayout($snippet, $template, $data);
Copy link
Author

@JojOatXGME JojOatXGME Aug 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that any output from before opening the layout snippet gets discarded. This was already the case before. I think ideally, we should call Snippet::endlayout before Tpl::load calls ob_get_contents(). However, I avoided to call Snippet::endlayout from within Tpl::load because I assumed dependencies from \Kirby\Toolkit to \Kirby\Template are to be avoided. Alternatively, it might make sense to trigger an exception if there is any output before opening a layout snippet.

}

/**
Expand Down
18 changes: 18 additions & 0 deletions tests/Template/SnippetTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,24 @@ public function testRenderWithoutClosingAndMultipleSlots()
$this->assertSame("<h1>Layout</h1>\n<header>Header content</header>\n<main>Body content</main>\n", $snippet->render());
}

/**
* @covers ::render
*/
public function testRenderOpenLayoutSnippet()
{
// all output must be captured
$this->expectOutputString('');

new App([
'roots' => [
'snippets' => static::FIXTURES
]
]);

$snippet = new Snippet(static::FIXTURES . '/with-layout.php');
$this->assertSame("<h1>Layout</h1>\nMy content\n<footer>with other stuff</footer>\n", $snippet->render());
}

/**
* @covers ::render
*/
Expand Down