This commit is contained in:
commit
a3620a1f5f
1042 changed files with 226722 additions and 0 deletions
311
kirby/src/Template/Snippet.php
Normal file
311
kirby/src/Template/Snippet.php
Normal file
|
|
@ -0,0 +1,311 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Template;
|
||||
|
||||
use Kirby\Cms\App;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Exception\LogicException;
|
||||
use Kirby\Filesystem\F;
|
||||
use Kirby\Toolkit\A;
|
||||
use Kirby\Toolkit\Tpl;
|
||||
|
||||
/**
|
||||
* The Snippet class includes shared code parts
|
||||
* in templates and allows to pass data as well as to
|
||||
* optionally pass content to various predefined slots.
|
||||
*
|
||||
* @package Kirby Template
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>,
|
||||
* Nico Hoffmann <nico@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class Snippet extends Tpl
|
||||
{
|
||||
/**
|
||||
* Cache for the currently active
|
||||
* snippet. This is used to start
|
||||
* and end slots within this snippet
|
||||
* in the helper functions
|
||||
*/
|
||||
public static self|null $current = null;
|
||||
|
||||
/**
|
||||
* Contains all slots that are opened
|
||||
* but not yet closed
|
||||
*/
|
||||
protected array $capture = [];
|
||||
|
||||
/**
|
||||
* An empty dummy slots object used for snippets
|
||||
* that were loaded without passing slots
|
||||
*/
|
||||
protected static Slots|null $dummySlots = null;
|
||||
|
||||
/**
|
||||
* Keeps track of the state of the snippet
|
||||
*/
|
||||
protected bool $open = false;
|
||||
|
||||
/**
|
||||
* The parent snippet
|
||||
*/
|
||||
protected self|null $parent = null;
|
||||
|
||||
/**
|
||||
* The collection of closed slots that will be used
|
||||
* to pass down to the template for the snippet.
|
||||
*/
|
||||
protected array $slots = [];
|
||||
|
||||
/**
|
||||
* Creates a new snippet
|
||||
*
|
||||
* @param string|null $file Full path to the PHP file of the snippet;
|
||||
* can be `null` for "dummy" snippets
|
||||
* that don't exist
|
||||
* @param array $data Associative array with variables that
|
||||
* will be set inside the snippet
|
||||
*/
|
||||
public function __construct(
|
||||
protected string|null $file,
|
||||
protected array $data = []
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and opens a new snippet. This can be used
|
||||
* directly in a template or via the slots() helper
|
||||
*/
|
||||
public static function begin(string|null $file, array $data = []): static
|
||||
{
|
||||
$snippet = new static($file, $data);
|
||||
return $snippet->open();
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the snippet and catches
|
||||
* the default slot if no slots have been
|
||||
* defined in between opening and closing.
|
||||
*/
|
||||
public function close(): static
|
||||
{
|
||||
// make sure that ending a snippet
|
||||
// is only supported if the snippet has
|
||||
// been started before
|
||||
if ($this->open === false) {
|
||||
throw new LogicException(
|
||||
message: 'The snippet has not been opened'
|
||||
);
|
||||
}
|
||||
|
||||
// create a default slot for the content
|
||||
// that has been captured between start and end
|
||||
if ($this->slots === []) {
|
||||
$this->slots['default'] = new Slot('default');
|
||||
$this->slots['default']->content = ob_get_clean();
|
||||
} else {
|
||||
// swallow any "unslotted" content
|
||||
// between start and end
|
||||
ob_end_clean();
|
||||
}
|
||||
|
||||
$this->open = false;
|
||||
|
||||
// switch back to the parent in nested
|
||||
// snippet stacks
|
||||
static::$current = $this->parent;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used in the endsnippet() helper
|
||||
*/
|
||||
public static function end(): void
|
||||
{
|
||||
echo static::$current?->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the last openend slot
|
||||
*/
|
||||
public function endslot(): void
|
||||
{
|
||||
// take the last slot from the capture stack
|
||||
$slot = array_pop($this->capture);
|
||||
|
||||
// capture the content and close the slot
|
||||
$slot->close();
|
||||
|
||||
// add the slot to the scope
|
||||
$this->slots[$slot->name()] = $slot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns either an open snippet capturing slots
|
||||
* or the template string for self-enclosed snippets
|
||||
*/
|
||||
public static function factory(
|
||||
string|array|null $name,
|
||||
array $data = [],
|
||||
bool $slots = false
|
||||
): static|string {
|
||||
// instead of returning empty string when `$name` is null,
|
||||
// allow rest of code to run, otherwise the wrong snippet would
|
||||
// be closed and potential issues for nested snippets may occur
|
||||
$file = $name !== null ? static::file($name) : null;
|
||||
|
||||
// for snippets with slots, make sure to open a new
|
||||
// snippet and start capturing slots
|
||||
if ($slots === true) {
|
||||
return static::begin($file, $data);
|
||||
}
|
||||
|
||||
// for snippets without slots, directly load and return
|
||||
// the snippet's template file
|
||||
$data = static::scope($data);
|
||||
return static::load($file, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Absolute path to the file for
|
||||
* the snippet/s taking snippets defined in plugins
|
||||
* into account
|
||||
*/
|
||||
public static function file(string|array $name): string|null
|
||||
{
|
||||
$kirby = App::instance();
|
||||
$root = static::root();
|
||||
$names = A::wrap($name);
|
||||
|
||||
foreach ($names as $name) {
|
||||
$name = (string)$name;
|
||||
$file = $root . '/' . $name . '.php';
|
||||
|
||||
if (F::exists($file, $root) === false) {
|
||||
$file = $kirby->extensions('snippets')[$name] ?? null;
|
||||
}
|
||||
|
||||
if ($file) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the snippet and starts output
|
||||
* buffering to catch all slots in between
|
||||
*/
|
||||
public function open(): static
|
||||
{
|
||||
if (static::$current !== null) {
|
||||
$this->parent = static::$current;
|
||||
}
|
||||
|
||||
$this->open = true;
|
||||
static::$current = $this;
|
||||
|
||||
ob_start();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent snippet if it exists
|
||||
*/
|
||||
public function parent(): static|null
|
||||
{
|
||||
return $this->parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the snippet and passes the scope
|
||||
* with all slots and data
|
||||
*/
|
||||
public function render(array $data = [], array $slots = []): string
|
||||
{
|
||||
// always make sure that the snippet
|
||||
// is closed before it can be rendered
|
||||
if ($this->open === true) {
|
||||
$this->close();
|
||||
}
|
||||
|
||||
// manually add slots
|
||||
foreach ($slots as $slotName => $slotContent) {
|
||||
$this->slots[$slotName] = new Slot($slotName, $slotContent);
|
||||
}
|
||||
|
||||
// custom data overrides the data from the controller
|
||||
// as well as the data passed to the Snippet instance
|
||||
$data = array_replace_recursive($this->data, $data);
|
||||
|
||||
return static::load(
|
||||
file: $this->file,
|
||||
data: static::scope($data, $this->slots())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the root directory for all
|
||||
* snippet templates
|
||||
*/
|
||||
public static function root(): string
|
||||
{
|
||||
return App::instance()->root('snippets');
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a new slot with the given name
|
||||
*/
|
||||
public function slot(string $name = 'default'): Slot
|
||||
{
|
||||
$slot = new Slot($name);
|
||||
$slot->open();
|
||||
|
||||
// start a new slot
|
||||
$this->capture[] = $slot;
|
||||
|
||||
return $slot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the slots collection
|
||||
*/
|
||||
public function slots(): Slots
|
||||
{
|
||||
return new Slots($this->slots);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the data variables that get passed to a snippet
|
||||
*
|
||||
* @param \Kirby\Template\Slots|null $slots If null, an empty dummy object is used
|
||||
*/
|
||||
protected static function scope(
|
||||
array $data = [],
|
||||
Slots|null $slots = null
|
||||
): array {
|
||||
// initialize a dummy slots object and cache it for better performance
|
||||
$slots ??= static::$dummySlots ??= new Slots([]);
|
||||
$data = [...App::instance()->data, ...$data];
|
||||
|
||||
if (
|
||||
array_key_exists('slot', $data) === true ||
|
||||
array_key_exists('slots', $data) === true
|
||||
) {
|
||||
throw new InvalidArgumentException(
|
||||
message: 'Passing the $slot or $slots variables to snippets is not supported.'
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
...$data,
|
||||
'slot' => $slots->default,
|
||||
'slots' => $slots,
|
||||
];
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue