Skip to content

Commit 5798cfb

Browse files
committed
[FEATURE] Introduce argument definitions for templates and partials
The new `<f:argument>` ViewHelper allows to define an API for templates, partials and layouts that will be validated when the template/partial/layout is rendered. For each argument, a type can be defined. By default, arguments are defined as non-optional, but they can be defined as optional by using `optional="{false}"`. Optional arguments can have default values, which are defined with `default="my default value"`. To avoid unnecessary complexity, the ViewHelper ensures that no dynamic content (like variables) can be used in an argument definition. Also, `<f:argument>` can't be nested into other ViewHelpers. Template argument definitions are processed and verified by the new `StrictArgumentProcessor`, which properly ensures argument types by casting scalar values and checking the types more thoroughly than the `LenientArgumentProcessor`. This means that currently template arguments are validated in a different way than ViewHelper arguments, however this will be resolved with Fluid v5. Currently, there is no way to prohibit additional arguments that are not defined with the `<f:argument>` ViewHelper, however this might still be added in the future.
1 parent 14441fa commit 5798cfb

File tree

17 files changed

+588
-3
lines changed

17 files changed

+588
-3
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/ArgumentViewHelper.php
3+
4+
:edit-on-github-link: https://github.com/TYPO3/Fluid/edit/main/src/ViewHelpers/ArgumentViewHelper.php
5+
:navigation-title: argument
6+
7+
.. include:: /Includes.rst.txt
8+
9+
.. _typo3fluid-fluid-argument:
10+
11+
==================================
12+
Argument ViewHelper `<f:argument>`
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/Argument>` instead.
19+
20+
.. typo3:viewhelper:: argument
21+
:source: ../Fluid.json
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<f:argument name="title" type="string" />
2+
<f:argument name="tags" type="string[]" optional="{true}" />
3+
<f:argument name="user" type="string" optional="{true}" default="admin" />
4+
5+
Title: {title}<br />
6+
<f:if condition="{tags}">
7+
Tags: {tags -> f:join(separator: ', ')}<br />
8+
</f:if>
9+
User: {user}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
/*
4+
* This file belongs to the package "TYPO3 Fluid".
5+
* See LICENSE.txt that was shipped with this package.
6+
*/
7+
8+
/**
9+
* EXAMPLE: Template arguments
10+
*
11+
* This example demonstrates the possibility to declare
12+
* argument definitions for template files, which need
13+
* to be met by the provided variables.
14+
*/
15+
16+
use TYPO3Fluid\FluidExamples\Helper\ExampleHelper;
17+
18+
require_once __DIR__ . '/../vendor/autoload.php';
19+
20+
$exampleHelper = new ExampleHelper();
21+
$view = $exampleHelper->init();
22+
23+
$view->assignMultiple([
24+
'title' => 'My title',
25+
'tags' => ['tag1', 'tag2'],
26+
]);
27+
28+
// Assigning the template path and filename to be rendered. Doing this overrides
29+
// resolving normally done by the TemplatePaths and directly renders this file.
30+
$paths = $view->getRenderingContext()->getTemplatePaths();
31+
$paths->setTemplatePathAndFilename(__DIR__ . '/Resources/Private/Singles/TemplateArguments.html');
32+
33+
// Rendering the View: plain old rendering of single file, no bells and whistles.
34+
$output = $view->render();
35+
36+
$exampleHelper->output($output);

src/Core/Compiler/AbstractCompiledTemplate.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ public function getVariableContainer(): VariableProviderInterface
3434
return new StandardVariableProvider();
3535
}
3636

37+
public function getArgumentDefinitions(): array
38+
{
39+
return [];
40+
}
41+
3742
/**
3843
* Render the parsed template with rendering context
3944
*

src/Core/Compiler/TemplateCompiler.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\RootNode;
1616
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\ViewHelperNode;
1717
use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
18+
use TYPO3Fluid\Fluid\Core\ViewHelper\ArgumentDefinition;
1819

1920
/**
2021
* @internal Nobody should need to override this class.
@@ -178,11 +179,13 @@ public function store(string $identifier, ParsingState $parsingState): ?string
178179
' $renderingContext->getViewHelperResolver()->addNamespaces(%s);' . chr(10) .
179180
' }' . chr(10) .
180181
' %s' . chr(10) .
182+
' %s' . chr(10) .
181183
'}' . chr(10),
182184
'class ' . $identifier . ' extends \TYPO3Fluid\Fluid\Core\Compiler\AbstractCompiledTemplate',
183185
$this->generateCodeForLayoutName($storedLayoutName),
184186
($parsingState->hasLayout() ? 'true' : 'false'),
185187
var_export($this->renderingContext->getViewHelperResolver()->getNamespaces(), true),
188+
$this->generateArgumentDefinitionsCodeFromParsingState($parsingState),
186189
$generatedRenderFunctions,
187190
);
188191
$this->renderingContext->getCache()->set($identifier, $templateCode);
@@ -222,6 +225,31 @@ protected function generateSectionCodeFromParsingState(ParsingState $parsingStat
222225
return $generatedRenderFunctions;
223226
}
224227

228+
protected function generateArgumentDefinitionsCodeFromParsingState(ParsingState $parsingState): string
229+
{
230+
$argumentDefinitions = $parsingState->getArgumentDefinitions();
231+
if ($argumentDefinitions === []) {
232+
return '';
233+
}
234+
$argumentDefinitionsCode = array_map(
235+
static fn(ArgumentDefinition $argumentDefinition): string => sprintf(
236+
'new \\TYPO3Fluid\\Fluid\\Core\\ViewHelper\\ArgumentDefinition(%s, %s, %s, %s, %s, %s)',
237+
var_export($argumentDefinition->getName(), true),
238+
var_export($argumentDefinition->getType(), true),
239+
var_export($argumentDefinition->getDescription(), true),
240+
var_export($argumentDefinition->isRequired(), true),
241+
var_export($argumentDefinition->getDefaultValue(), true),
242+
var_export($argumentDefinition->getEscape(), true),
243+
),
244+
$argumentDefinitions,
245+
);
246+
return 'public function getArgumentDefinitions(): array {' . chr(10) .
247+
' return [' . chr(10) .
248+
' ' . implode(',' . chr(10) . ' ', $argumentDefinitionsCode) . ',' . chr(10) .
249+
' ];' . chr(10) .
250+
' }';
251+
}
252+
225253
/**
226254
* Replaces special characters by underscores
227255
* @see http://www.php.net/manual/en/language.variables.basics.php

src/Core/Parser/ParsedTemplateInterface.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\NodeInterface;
1313
use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
1414
use TYPO3Fluid\Fluid\Core\Variables\VariableProviderInterface;
15+
use TYPO3Fluid\Fluid\Core\ViewHelper\ArgumentDefinition;
1516

1617
/**
1718
* This interface is returned by \TYPO3Fluid\Fluid\Core\Parser\TemplateParser->parse()
@@ -25,6 +26,11 @@ public function setIdentifier(string $identifier);
2526

2627
public function getIdentifier(): string;
2728

29+
/**
30+
* @return ArgumentDefinition[]
31+
*/
32+
public function getArgumentDefinitions(): array;
33+
2834
/**
2935
* Render the parsed template with rendering context
3036
*

src/Core/Parser/ParsingState.php

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\RootNode;
1414
use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
1515
use TYPO3Fluid\Fluid\Core\Variables\VariableProviderInterface;
16+
use TYPO3Fluid\Fluid\Core\ViewHelper\ArgumentDefinition;
1617
use TYPO3Fluid\Fluid\View;
1718

1819
/**
@@ -26,6 +27,11 @@ class ParsingState implements ParsedTemplateInterface
2627
{
2728
protected string $identifier;
2829

30+
/**
31+
* @var array<string, ArgumentDefinition>
32+
*/
33+
protected array $argumentDefinitions = [];
34+
2935
/**
3036
* Root node reference
3137
*/
@@ -80,6 +86,22 @@ public function getRootNode(): RootNode
8086
return $this->rootNode;
8187
}
8288

89+
/**
90+
* @return array<string, ArgumentDefinition>
91+
*/
92+
public function getArgumentDefinitions(): array
93+
{
94+
return $this->argumentDefinitions;
95+
}
96+
97+
/**
98+
* @param array<string, ArgumentDefinition> $argumentDefinitions
99+
*/
100+
public function setArgumentDefinitions(array $argumentDefinitions): void
101+
{
102+
$this->argumentDefinitions = $argumentDefinitions;
103+
}
104+
83105
/**
84106
* Render the parsed template with rendering context
85107
*
@@ -111,6 +133,21 @@ public function getNodeFromStack(): NodeInterface
111133
return $this->nodeStack[count($this->nodeStack) - 1];
112134
}
113135

136+
/**
137+
* Checks if the specified node type exists in the current stack
138+
*
139+
* @param class-string $nodeType
140+
*/
141+
public function hasNodeTypeInStack(string $nodeType): bool
142+
{
143+
foreach ($this->nodeStack as $node) {
144+
if ($node instanceof $nodeType) {
145+
return true;
146+
}
147+
}
148+
return false;
149+
}
150+
114151
/**
115152
* Pop the top stack element (=remove it) and return it back.
116153
*

src/View/AbstractTemplateView.php

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\ViewHelperNode;
1616
use TYPO3Fluid\Fluid\Core\Rendering\RenderingContext;
1717
use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
18+
use TYPO3Fluid\Fluid\Core\Variables\VariableProviderInterface;
19+
use TYPO3Fluid\Fluid\Core\ViewHelper\ArgumentProcessorInterface;
20+
use TYPO3Fluid\Fluid\Core\ViewHelper\StrictArgumentProcessor;
1821
use TYPO3Fluid\Fluid\View\Exception\InvalidSectionException;
1922
use TYPO3Fluid\Fluid\View\Exception\InvalidTemplateResourceException;
2023
use TYPO3Fluid\Fluid\ViewHelpers\SectionViewHelper;
@@ -144,6 +147,16 @@ public function render($actionName = null)
144147

145148
if (!$parsedTemplate->hasLayout()) {
146149
$this->startRendering(self::RENDERING_TEMPLATE, $parsedTemplate, $this->baseRenderingContext);
150+
try {
151+
// @todo make argument processor configurable with Fluid v5
152+
$this->processAndValidateTemplateVariables(
153+
$parsedTemplate,
154+
$this->baseRenderingContext->getVariableProvider(),
155+
new StrictArgumentProcessor(),
156+
);
157+
} catch (Exception $validationError) {
158+
return $renderingContext->getErrorHandler()->handleViewError($validationError);
159+
}
147160
$output = $parsedTemplate->render($this->baseRenderingContext);
148161
$this->stopRendering();
149162
} else {
@@ -159,6 +172,16 @@ function ($parent, TemplatePaths $paths) use ($layoutName) {
159172
return $error->getSource();
160173
}
161174
$this->startRendering(self::RENDERING_LAYOUT, $parsedTemplate, $this->baseRenderingContext);
175+
try {
176+
// @todo make argument processor configurable with Fluid v5
177+
$this->processAndValidateTemplateVariables(
178+
$parsedLayout,
179+
$this->baseRenderingContext->getVariableProvider(),
180+
new StrictArgumentProcessor(),
181+
);
182+
} catch (Exception $validationError) {
183+
return $renderingContext->getErrorHandler()->handleViewError($validationError);
184+
}
162185
$output = $parsedLayout->render($this->baseRenderingContext);
163186
$this->stopRendering();
164187
}
@@ -286,6 +309,16 @@ function ($parent, TemplatePaths $paths) use ($partialName) {
286309
if ($sectionName !== null) {
287310
$output = $this->renderSection($sectionName, $variables, $ignoreUnknown);
288311
} else {
312+
try {
313+
// @todo make argument processor configurable with Fluid v5
314+
$this->processAndValidateTemplateVariables(
315+
$parsedPartial,
316+
$renderingContext->getVariableProvider(),
317+
new StrictArgumentProcessor(),
318+
);
319+
} catch (Exception $validationError) {
320+
return $renderingContext->getErrorHandler()->handleViewError($validationError);
321+
}
289322
$output = $parsedPartial->render($renderingContext);
290323
}
291324
$this->stopRendering();
@@ -362,4 +395,42 @@ protected function getCurrentRenderingContext()
362395
$currentRendering = end($this->renderingStack);
363396
return !empty($currentRendering['renderingContext']) ? $currentRendering['renderingContext'] : $this->baseRenderingContext;
364397
}
398+
399+
protected function processAndValidateTemplateVariables(
400+
ParsedTemplateInterface $parsedTemplate,
401+
VariableProviderInterface $variableProvider,
402+
ArgumentProcessorInterface $argumentProcessor,
403+
): void {
404+
$renderingTypeLabel = match ($this->getCurrentRenderingType()) {
405+
self::RENDERING_PARTIAL => 'partial',
406+
self::RENDERING_TEMPLATE => 'template',
407+
self::RENDERING_LAYOUT => 'layout',
408+
};
409+
foreach ($parsedTemplate->getArgumentDefinitions() as $argumentDefinition) {
410+
$argumentName = $argumentDefinition->getName();
411+
if ($variableProvider->exists($argumentName)) {
412+
$processedValue = $argumentProcessor->process($variableProvider->get($argumentName), $argumentDefinition);
413+
if (!$argumentProcessor->isValid($processedValue, $argumentDefinition)) {
414+
throw new Exception(sprintf(
415+
'The argument "%s" for %s "%s" is registered with type "%s", but the provided value is of type "%s".',
416+
$argumentName,
417+
$renderingTypeLabel,
418+
$parsedTemplate->getIdentifier(),
419+
$argumentDefinition->getType(),
420+
is_object($processedValue) ? get_class($processedValue) : gettype($processedValue),
421+
), 1746637333);
422+
}
423+
$variableProvider->add($argumentName, $processedValue);
424+
} elseif ($argumentDefinition->isRequired()) {
425+
throw new Exception(sprintf(
426+
'The argument "%s" for %s "%s" is required, but was not provided.',
427+
$argumentName,
428+
$renderingTypeLabel,
429+
$parsedTemplate->getIdentifier(),
430+
), 1746637334);
431+
} else {
432+
$variableProvider->add($argumentName, $argumentDefinition->getDefaultValue());
433+
}
434+
}
435+
}
365436
}

0 commit comments

Comments
 (0)