Skip to content

Commit

Permalink
Merge pull request #375 from CPS-IT/feature/1.x/helper-context
Browse files Browse the repository at this point in the history
[!!!][FEATURE] Require `render` method with helper context in helpers
  • Loading branch information
eliashaeussler authored Jan 7, 2025
2 parents 6906230 + 28d0268 commit cdbb2ff
Show file tree
Hide file tree
Showing 28 changed files with 1,276 additions and 140 deletions.
17 changes: 7 additions & 10 deletions Classes/Renderer/Component/Layout/HandlebarsLayout.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,22 @@ class HandlebarsLayout
{
protected bool $parsed = false;

/**
* @var array<string, list<HandlebarsLayoutAction>>
*/
protected array $actions = [];

/**
* @param callable $parseFunction
* @param array<string, HandlebarsLayoutAction[]> $actions
*/
public function __construct(
protected $parseFunction,
protected array $actions = [],
) {}

public function parse(): void
{
($this->parseFunction)();
$this->parsed = true;
($this->parseFunction)();
}

public function addAction(string $name, HandlebarsLayoutAction $action): void
Expand All @@ -57,7 +60,7 @@ public function addAction(string $name, HandlebarsLayoutAction $action): void
}

/**
* @return array<string, HandlebarsLayoutAction[]>|HandlebarsLayoutAction[]
* @return ($name is null ? array<string, HandlebarsLayoutAction[]> : HandlebarsLayoutAction[])
*/
public function getActions(string $name = null): array
{
Expand All @@ -77,10 +80,4 @@ public function isParsed(): bool
{
return $this->parsed;
}

public function setParsed(bool $parsed): self
{
$this->parsed = $parsed;
return $this;
}
}
8 changes: 3 additions & 5 deletions Classes/Renderer/Component/Layout/HandlebarsLayoutAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
namespace Fr\Typo3Handlebars\Renderer\Component\Layout;

use Fr\Typo3Handlebars\Exception;
use Fr\Typo3Handlebars\Renderer;

/**
* HandlebarsLayoutAction
Expand All @@ -40,13 +41,10 @@ class HandlebarsLayoutAction
protected readonly string $mode;

/**
* @param array<string, mixed> $data
* @param callable $renderFunction
* @throws Exception\UnsupportedTypeException
*/
public function __construct(
protected readonly array $data,
protected $renderFunction,
protected readonly Renderer\Helper\Context\HelperContext $context,
string $mode = self::REPLACE,
) {
$this->mode = strtolower($mode);
Expand All @@ -60,7 +58,7 @@ public function __construct(
*/
public function render(string $value): string
{
$renderResult = ($this->renderFunction)($this->data);
$renderResult = $this->context->renderChildren($this->context->renderingContext);

return match ($this->mode) {
self::APPEND => $value . $renderResult,
Expand Down
16 changes: 7 additions & 9 deletions Classes/Renderer/Helper/BlockHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,18 @@
* @license GPL-2.0-or-later
* @see https://github.com/shannonmoeller/handlebars-layouts#block-name
*/
#[Attribute\AsHelper('block')]
final readonly class BlockHelper implements HelperInterface
{
/**
* @param array<string, mixed> $options
* @throws Exception\UnsupportedTypeException
*/
#[Attribute\AsHelper('block')]
public function evaluate(string $name, array $options): string
public function render(Context\HelperContext $context): string
{
$data = $options['_this'];
$actions = $data['_layoutActions'] ?? [];
$stack = $data['_layoutStack'] ?? [];
$name = $context[0];
$renderingContext = $context->renderingContext;
$actions = $renderingContext['_layoutActions'] ?? [];
$stack = $renderingContext['_layoutStack'] ?? [];

// Parse layouts and fetch all parsed layout actions for the requested block
while (!empty($stack)) {
Expand All @@ -58,12 +58,10 @@ public function evaluate(string $name, array $options): string
}

// Walk through layout actions and apply them to the rendered block
$fn = $options['fn'] ?? static fn() => '';

return array_reduce(
$actions,
static fn(string $value, Renderer\Component\Layout\HandlebarsLayoutAction $action): string => $action->render($value),
$fn($data),
$context->renderChildren($renderingContext) ?? '',
);
}
}
52 changes: 25 additions & 27 deletions Classes/Renderer/Helper/ContentHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,34 +34,34 @@
* @license GPL-2.0-or-later
* @see https://github.com/shannonmoeller/handlebars-layouts#content-name-modeappendprependreplace
*/
#[Attribute\AsHelper('content')]
final readonly class ContentHelper implements HelperInterface
{
public function __construct(
private Log\LoggerInterface $logger,
) {}

/**
* @param array<string, mixed> $options
* @return string|bool
*/
#[Attribute\AsHelper('content')]
public function evaluate(string $name, array $options)
public function render(Context\HelperContext $context): ?bool
{
$data = $options['_this'];
$mode = $options['hash']['mode'] ?? Renderer\Component\Layout\HandlebarsLayoutAction::REPLACE;
$layoutStack = $this->getLayoutStack($options);
$name = $context[0];
$mode = $context['mode'] ?? Renderer\Component\Layout\HandlebarsLayoutAction::REPLACE;
$layoutStack = $this->getLayoutStack($context);

// Early return if "content" helper is requested outside of an "extend" helper block
if (empty($layoutStack)) {
$this->logger->error('Handlebars layout helper "content" can only be used within an "extend" helper block!', ['name' => $name]);
return '';
$this->logger->error(
'Handlebars layout helper "content" can only be used within an "extend" helper block!',
['name' => $name],
);

return $context->isBlockHelper() ? null : false;
}

// Get upper layout from stack
$layout = end($layoutStack);

// Usage in conditional context: Test whether given required block is registered
if (!\is_callable($options['fn'] ?? '')) {
if (!$context->isBlockHelper()) {
if (!$layout->isParsed()) {
$layout->parse();
}
Expand All @@ -70,34 +70,32 @@ public function evaluate(string $name, array $options)
}

// Add concrete action for the requested block
$action = new Renderer\Component\Layout\HandlebarsLayoutAction($data, $options['fn'], $mode);
$action = new Renderer\Component\Layout\HandlebarsLayoutAction($context, $mode);
$layout->addAction($name, $action);

// This helper does not return any content, it's just here to register layout actions
return '';
return null;
}

/**
* @param array<string, mixed> $options
* @return Renderer\Component\Layout\HandlebarsLayout[]
*/
protected function getLayoutStack(array $options): array
protected function getLayoutStack(Context\HelperContext $context): array
{
// Fetch layout stack from current context
if (isset($options['_this']['_layoutStack'])) {
return $options['_this']['_layoutStack'];
}
$renderingContext = $context->renderingContext;
$contextStack = $context->contextStack;

// Early return if only context is currently processed
if (!isset($options['contexts'])) {
return [];
// Fetch layout stack from current context
if (isset($renderingContext['_layoutStack'])) {
return $renderingContext['_layoutStack'];
}

// Fetch layout stack from previous contexts
while (!empty($options['contexts'])) {
$context = array_pop($options['contexts']);
if (isset($context['_layoutStack'])) {
return $context['_layoutStack'];
while (!$contextStack->isEmpty()) {
$currentContext = $contextStack->pop();

if (isset($currentContext['_layoutStack'])) {
return $currentContext['_layoutStack'];
}
}

Expand Down
132 changes: 132 additions & 0 deletions Classes/Renderer/Helper/Context/HelperContext.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
<?php

declare(strict_types=1);

/*
* This file is part of the TYPO3 CMS extension "handlebars".
*
* Copyright (C) 2024 Elias Häußler <e.haeussler@familie-redlich.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

namespace Fr\Typo3Handlebars\Renderer\Helper\Context;

/**
* HelperContext
*
* @author Elias Häußler <e.haeussler@familie-redlich.de>
* @license GPL-2.0-or-later
*
* @implements \ArrayAccess<int|string, mixed>
*/
final class HelperContext implements \ArrayAccess
{
/**
* @param list<mixed> $arguments
* @param array<string, mixed> $hash
* @param array<string, mixed> $renderingContext
* @param array<'root'|int, array<string, mixed>> $data
* @param callable|null $childrenClosure
* @param callable|null $inverseClosure
*/
public function __construct(
public readonly array $arguments, // 1...n-1
public readonly array $hash, // n['hash']
public readonly RenderingContextStack $contextStack, // n['contexts']
public array &$renderingContext, // n['_this']
public array &$data, // n['data'] => 'root', ...
private $childrenClosure = null, // n['fn']
private $inverseClosure = null, // n['inverse']
) {}

/**
* @param list<mixed> $options
*/
public static function fromRuntimeCall(array &$options): self
{
$context = array_pop($options);

$arguments = $options;
$hash = $context['hash'];
$contextStack = RenderingContextStack::fromRuntimeCall($context['contexts']);
$renderingContext = &$context['_this'];
$data = &$context['data'];
$childrenClosure = $context['fn'] ?? null;
$inverseClosure = $context['inverse'] ?? null;

return new self(
$arguments,
$hash,
$contextStack,
$renderingContext,
$data,
$childrenClosure,
$inverseClosure,
);
}

public function isBlockHelper(): bool
{
return $this->childrenClosure !== null;
}

public function renderChildren(): mixed
{
if ($this->childrenClosure === null) {
return null;
}

return ($this->childrenClosure)(...\func_get_args());
}

public function renderInverse(): mixed
{
if ($this->inverseClosure === null) {
return null;
}

return ($this->inverseClosure)(...\func_get_args());
}

public function offsetExists(mixed $offset): bool
{
if (is_numeric($offset)) {
return isset($this->arguments[$offset]);
}

return isset($this->hash[$offset]);
}

public function offsetGet(mixed $offset): mixed
{
if (is_numeric($offset)) {
return $this->arguments[$offset]
?? throw new \OutOfBoundsException('Argument "' . $offset . '" does not exist.', 1736235839);
}

return $this->hash[$offset]
?? throw new \OutOfBoundsException('Hash "' . $offset . '" does not exist.', 1736235851);
}

public function offsetSet(mixed $offset, mixed $value): never
{
throw new \LogicException('Helper context is locked and cannot be modified.', 1734434746);
}

public function offsetUnset(mixed $offset): never
{
throw new \LogicException('Helper context is locked and cannot be modified.', 1734434780);
}
}
Loading

0 comments on commit cdbb2ff

Please sign in to comment.