Skip to content

Commit 6d0d85f

Browse files
authored
[FEATURE] Named Slots (#1118)
This patch enables Fluid's components to accept multiple slots. Each slot is identified by a `name` argument. The corresponding `<f:fragment>` ViewHelper can then be used to fill the slots of a component. The available slots of a component are now also exposed properly via the `ComponentDefinition` DTO. Unfortunately, the `TemplateParser` fails to propagate available slots of the current templates consistently to the `ParsingState`, which currently leads to ugly state merging within the parser. Hopefully, this issue can be addressed in the future to remove this workaround, which has been documented in the code. For this first implementation, we limit the usage if the new `<f:fragment>` ViewHelper to the context of components. However, we might expand this ViewHelper to other areas in Fluid, such as a way to partially extend/overwrite templates.
1 parent c66ac9c commit 6d0d85f

File tree

15 files changed

+416
-24
lines changed

15 files changed

+416
-24
lines changed

Documentation/ViewHelpers/Fluid.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
.. This reStructured text file has been automatically generated, do not change.
2+
.. Source: https://github.com/TYPO3/Fluid/blob/main/src/ViewHelpers/FragmentViewHelper.php
3+
4+
:edit-on-github-link: https://github.com/TYPO3/Fluid/edit/main/src/ViewHelpers/FragmentViewHelper.php
5+
:navigation-title: fragment
6+
7+
.. include:: /Includes.rst.txt
8+
9+
.. _typo3fluid-fluid-fragment:
10+
11+
==================================
12+
Fragment ViewHelper `<f:fragment>`
13+
==================================
14+
15+
.. .. note::
16+
.. This reference is part of the documentation of Fluid Standalone.
17+
.. If you are working with Fluid in TYPO3 CMS, please refer to
18+
.. :doc:`TYPO3's ViewHelper reference <t3viewhelper:Global/Fragment>` instead.
19+
20+
.. typo3:viewhelper:: fragment
21+
:source: ../Fluid.json

src/Core/Compiler/AbstractCompiledTemplate.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ public function getArgumentDefinitions(): array
3939
return [];
4040
}
4141

42+
public function getAvailableSlots(): array
43+
{
44+
return [];
45+
}
46+
4247
/**
4348
* Render the parsed template with rendering context
4449
*

src/Core/Compiler/TemplateCompiler.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,12 +189,14 @@ public function store(string $identifier, ParsingState $parsingState): ?string
189189
' }' . chr(10) .
190190
' %s' . chr(10) .
191191
' %s' . chr(10) .
192+
' %s' . chr(10) .
192193
'}' . chr(10),
193194
'class ' . $identifier . ' extends \TYPO3Fluid\Fluid\Core\Compiler\AbstractCompiledTemplate',
194195
$this->generateCodeForLayoutName($storedLayoutName),
195196
($parsingState->hasLayout() ? 'true' : 'false'),
196197
var_export($this->renderingContext->getViewHelperResolver()->getLocalNamespaces(), true),
197198
$this->generateArgumentDefinitionsCodeFromParsingState($parsingState),
199+
$this->generateAvailableSlotsCodeFromParsingState($parsingState),
198200
$generatedRenderFunctions,
199201
);
200202
$this->renderingContext->getCache()->set($identifier, $templateCode);
@@ -259,6 +261,17 @@ protected function generateArgumentDefinitionsCodeFromParsingState(ParsingState
259261
' }';
260262
}
261263

264+
protected function generateAvailableSlotsCodeFromParsingState(ParsingState $parsingState): string
265+
{
266+
$availableSlots = $parsingState->getAvailableSlots();
267+
if ($availableSlots === []) {
268+
return '';
269+
}
270+
return 'public function getAvailableSlots(): array {' . chr(10) .
271+
' return ' . var_export($availableSlots, true) . ';' . chr(10) .
272+
' }';
273+
}
274+
262275
/**
263276
* Replaces special characters by underscores
264277
* @see http://www.php.net/manual/en/language.variables.basics.php

src/Core/Component/AbstractComponentCollection.php

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
use TYPO3Fluid\Fluid\Core\ViewHelper\TemplateStructureViewHelperResolver;
1414
use TYPO3Fluid\Fluid\Core\ViewHelper\UnresolvableViewHelperException;
1515
use TYPO3Fluid\Fluid\Core\ViewHelper\ViewHelperResolverDelegateInterface;
16-
use TYPO3Fluid\Fluid\ViewHelpers\SlotViewHelper;
1716

1817
/**
1918
* Base class for a collection of components: Fluid templates that can be called with Fluid's
@@ -115,8 +114,7 @@ final public function getComponentDefinition(string $viewHelperName): ComponentD
115114
$viewHelperName,
116115
$parsedTemplate->getArgumentDefinitions(),
117116
$this->additionalArgumentsAllowed($viewHelperName),
118-
// For now, we just assume the default slot; This will change with the named slots feature.
119-
[SlotViewHelper::DEFAULT_SLOT],
117+
$parsedTemplate->getAvailableSlots(),
120118
);
121119
}
122120
return $this->componentDefinitionsCache[$viewHelperName];

src/Core/Component/ComponentAdapter.php

Lines changed: 71 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,16 @@
1010
namespace TYPO3Fluid\Fluid\Core\Component;
1111

1212
use TYPO3Fluid\Fluid\Core\Compiler\TemplateCompiler;
13+
use TYPO3Fluid\Fluid\Core\Parser\Exception as ParserException;
1314
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\NodeInterface;
1415
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\ViewHelperNode;
16+
use TYPO3Fluid\Fluid\Core\Rendering\RenderingContext;
1517
use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
1618
use TYPO3Fluid\Fluid\Core\ViewHelper\ArgumentDefinition;
1719
use TYPO3Fluid\Fluid\Core\ViewHelper\Exception;
1820
use TYPO3Fluid\Fluid\Core\ViewHelper\ViewHelperInterface;
1921
use TYPO3Fluid\Fluid\Core\ViewHelper\ViewHelperResolverDelegateInterface;
22+
use TYPO3Fluid\Fluid\ViewHelpers\FragmentViewHelper;
2023
use TYPO3Fluid\Fluid\ViewHelpers\SlotViewHelper;
2124

2225
/**
@@ -125,18 +128,43 @@ public function initializeArgumentsAndRender(): mixed
125128
return $this->getComponentDefinitionProvider()->getComponentRenderer()->renderComponent(
126129
$this->viewHelperNode->getName(),
127130
$this->arguments,
128-
$this->viewHelperNode->getChildNodes() !== []
129-
? [SlotViewHelper::DEFAULT_SLOT => $this->buildRenderChildrenClosure()]
130-
: [],
131+
$this->buildSlotClosures(),
131132
$this->renderingContext,
132133
);
133134
}
134135

135-
private function buildRenderChildrenClosure(): callable
136+
/**
137+
* @return \Closure[]
138+
*/
139+
private function buildSlotClosures(): array
140+
{
141+
if ($this->viewHelperNode->getChildNodes() === []) {
142+
return [];
143+
}
144+
$slotClosures = [];
145+
foreach ($this->extractFragmentViewHelperNodes($this->viewHelperNode) as $fragmentNode) {
146+
$fragmentName = $this->extractFragmentName($fragmentNode);
147+
if (isset($slotClosures[$fragmentName])) {
148+
throw new ParserException(sprintf(
149+
'Fragment "%s" for <%s:%s> is defined multiple times.',
150+
$fragmentName,
151+
$this->viewHelperNode->getNamespace(),
152+
$this->viewHelperNode->getName(),
153+
), 1750865701);
154+
}
155+
$slotClosures[$fragmentName] = $this->buildSlotClosure($fragmentNode);
156+
}
157+
if ($slotClosures === []) {
158+
$slotClosures[SlotViewHelper::DEFAULT_SLOT] = $this->buildSlotClosure($this->viewHelperNode);
159+
}
160+
return $slotClosures;
161+
}
162+
163+
private function buildSlotClosure(ViewHelperNode $viewHelperNode): \Closure
136164
{
137-
return function (): mixed {
165+
return function () use ($viewHelperNode): mixed {
138166
$this->renderingContextStack[] = $this->renderingContext;
139-
$result = $this->viewHelperNode->evaluateChildNodes($this->renderingContext);
167+
$result = $viewHelperNode->evaluateChildNodes($this->renderingContext);
140168
$this->setRenderingContext(array_pop($this->renderingContextStack));
141169
return $result;
142170
};
@@ -152,7 +180,7 @@ public function convert(TemplateCompiler $templateCompiler): array
152180
$initializationPhpCode = '// Rendering Component ' . $this->viewHelperNode->getNamespace() . ':' . $this->viewHelperNode->getName() . chr(10);
153181

154182
$argumentsVariableName = $templateCompiler->variableName('arguments');
155-
$renderChildrenClosureVariableName = $templateCompiler->variableName('renderChildrenClosure');
183+
$slotClosuresVariableName = $templateCompiler->variableName('slotClosures');
156184

157185
// Similarly to Fluid's ViewHelper processing, the responsible ViewHelper resolver delegate
158186
// is resolved, validated early and "baked in" to the cache to improve rendering times for
@@ -163,9 +191,7 @@ public function convert(TemplateCompiler $templateCompiler): array
163191
var_export($resolverDelegate->getNamespace(), true),
164192
var_export($this->viewHelperNode->getName(), true),
165193
$argumentsVariableName,
166-
$this->viewHelperNode->getChildNodes() !== []
167-
? '[\'' . SlotViewHelper::DEFAULT_SLOT . '\' => ' . $renderChildrenClosureVariableName . ']'
168-
: '[]',
194+
$slotClosuresVariableName,
169195
);
170196

171197
$accumulatedArgumentInitializationCode = '';
@@ -194,11 +220,25 @@ public function convert(TemplateCompiler $templateCompiler): array
194220

195221
$argumentInitializationCode .= '];' . chr(10);
196222

223+
$slotClosures = [];
224+
if ($this->viewHelperNode->getChildNodes() !== []) {
225+
foreach ($this->extractFragmentViewHelperNodes($this->viewHelperNode) as $fragmentNode) {
226+
$fragmentName = $this->extractFragmentName($fragmentNode);
227+
$slotClosures[$fragmentName] = $templateCompiler->wrapChildNodesInClosure($fragmentNode);
228+
}
229+
if ($slotClosures === []) {
230+
$slotClosures[SlotViewHelper::DEFAULT_SLOT] = $templateCompiler->wrapChildNodesInClosure($this->viewHelperNode);
231+
}
232+
}
233+
197234
// Build up closure which renders the child nodes
235+
foreach ($slotClosures as $name => $closureCode) {
236+
$slotClosures[$name] = var_export($name, true) . ' => ' . $closureCode;
237+
}
198238
$initializationPhpCode .= sprintf(
199-
'%s = %s;' . chr(10),
200-
$renderChildrenClosureVariableName,
201-
$templateCompiler->wrapChildNodesInClosure($this->viewHelperNode),
239+
'%s = [' . chr(10) . '%s' . chr(10) . '];' . chr(10),
240+
$slotClosuresVariableName,
241+
implode(',' . chr(10), $slotClosures),
202242
);
203243

204244
$initializationPhpCode .= $accumulatedArgumentInitializationCode . chr(10) . $argumentInitializationCode;
@@ -264,4 +304,22 @@ private function getComponentDefinitionProvider(): ViewHelperResolverDelegateInt
264304
}
265305
return $this->viewHelperNode->getResolverDelegate();
266306
}
307+
308+
/**
309+
* @return ViewHelperNode[]
310+
*/
311+
private function extractFragmentViewHelperNodes(ViewHelperNode $viewHelperNode): array
312+
{
313+
return array_filter(
314+
$viewHelperNode->getChildNodes(),
315+
fn(NodeInterface $node): bool => $node instanceof ViewHelperNode && $node->getUninitializedViewHelper() instanceof FragmentViewHelper,
316+
);
317+
}
318+
319+
private function extractFragmentName(ViewHelperNode $fragmentNode): string
320+
{
321+
return isset($fragmentNode->getArguments()['name'])
322+
? (string)$fragmentNode->getArguments()['name']->evaluate(new RenderingContext())
323+
: SlotViewHelper::DEFAULT_SLOT;
324+
}
267325
}

src/Core/Parser/ParsedTemplateInterface.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ public function getIdentifier(): string;
3131
*/
3232
public function getArgumentDefinitions(): array;
3333

34+
/**
35+
* @return string[]
36+
*/
37+
public function getAvailableSlots(): array;
38+
3439
/**
3540
* Render the parsed template with rendering context
3641
*

src/Core/Parser/ParsingState.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ class ParsingState implements ParsedTemplateInterface
3333
*/
3434
protected array $argumentDefinitions = [];
3535

36+
/**
37+
* @var string[]
38+
*/
39+
protected array $availableSlots = [];
40+
3641
/**
3742
* Root node reference
3843
*/
@@ -103,6 +108,22 @@ public function setArgumentDefinitions(array $argumentDefinitions): void
103108
$this->argumentDefinitions = $argumentDefinitions;
104109
}
105110

111+
/**
112+
* @return string[]
113+
*/
114+
public function getAvailableSlots(): array
115+
{
116+
return $this->availableSlots;
117+
}
118+
119+
/**
120+
* @param string[] $availableSlots
121+
*/
122+
public function setAvailableSlots(array $availableSlots): void
123+
{
124+
$this->availableSlots = $availableSlots;
125+
}
126+
106127
/**
107128
* Render the parsed template with rendering context
108129
*

src/Core/Parser/TemplateParser.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -623,6 +623,11 @@ protected function buildArgumentObjectTree(ParsingState $state, string $argument
623623
// affect the local state. Maybe it's also possible to get rid
624624
// of the local state altogether.
625625
$innerState = $this->buildObjectTree($this->createParsingState(null), $splitArgument, self::CONTEXT_INSIDE_VIEWHELPER_ARGUMENTS);
626+
// This can be removed once the outer-inner-state issue is resolved
627+
$state->setAvailableSlots(array_unique(array_merge(
628+
$state->getAvailableSlots(),
629+
$innerState->getAvailableSlots(),
630+
)));
626631
return $innerState->getRootNode();
627632
}
628633

0 commit comments

Comments
 (0)