Skip to content

Commit 7b478e3

Browse files
committed
[FEATURE] Introduce argument definitions for templates and partials
1 parent 68911c7 commit 7b478e3

File tree

12 files changed

+313
-3
lines changed

12 files changed

+313
-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: 27 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,30 @@ 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)',
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+
),
243+
$argumentDefinitions,
244+
);
245+
return 'public function getArgumentDefinitions(): array {' . chr(10) .
246+
' return [' . chr(10) .
247+
' ' . implode(',' . chr(10) . ' ', $argumentDefinitionsCode) . ',' . chr(10) .
248+
' ];' . chr(10) .
249+
' }';
250+
}
251+
225252
/**
226253
* Replaces special characters by underscores
227254
* @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()
@@ -23,6 +24,11 @@ public function setIdentifier(string $identifier);
2324

2425
public function getIdentifier(): string;
2526

27+
/**
28+
* @return ArgumentDefinition[]
29+
*/
30+
public function getArgumentDefinitions(): array;
31+
2632
/**
2733
* Render the parsed template with rendering context
2834
*

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
/**
@@ -24,6 +25,11 @@ class ParsingState implements ParsedTemplateInterface
2425
{
2526
protected string $identifier;
2627

28+
/**
29+
* @var array<string, ArgumentDefinition>
30+
*/
31+
protected array $argumentDefinitions = [];
32+
2733
/**
2834
* Root node reference
2935
*/
@@ -78,6 +84,22 @@ public function getRootNode(): RootNode
7884
return $this->rootNode;
7985
}
8086

87+
/**
88+
* @return array<string, ArgumentDefinition>
89+
*/
90+
public function getArgumentDefinitions(): array
91+
{
92+
return $this->argumentDefinitions;
93+
}
94+
95+
/**
96+
* @param array<string, ArgumentDefinition> $argumentDefinitions
97+
*/
98+
public function setArgumentDefinitions(array $argumentDefinitions): void
99+
{
100+
$this->argumentDefinitions = $argumentDefinitions;
101+
}
102+
81103
/**
82104
* Render the parsed template with rendering context
83105
*
@@ -109,6 +131,21 @@ public function getNodeFromStack(): NodeInterface
109131
return $this->nodeStack[count($this->nodeStack) - 1];
110132
}
111133

134+
/**
135+
* Checks if the specified node type exists in the current stack
136+
*
137+
* @param class-string $nodeType
138+
*/
139+
public function hasNodeTypeInStack(string $nodeType): bool
140+
{
141+
foreach ($this->nodeStack as $node) {
142+
if ($node instanceof $nodeType) {
143+
return true;
144+
}
145+
}
146+
return false;
147+
}
148+
112149
/**
113150
* Pop the top stack element (=remove it) and return it back.
114151
*
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
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+
namespace TYPO3Fluid\Fluid\ViewHelpers;
9+
10+
use TYPO3Fluid\Fluid\Core\Compiler\TemplateCompiler;
11+
use TYPO3Fluid\Fluid\Core\Parser\ParsingState;
12+
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\NodeInterface;
13+
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\ViewHelperNode;
14+
use TYPO3Fluid\Fluid\Core\Rendering\RenderingContext;
15+
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
16+
use TYPO3Fluid\Fluid\Core\ViewHelper\ArgumentDefinition;
17+
18+
/**
19+
* ``f:argument`` allows to define requirements and type constraints to variables that
20+
* are provided to templates and partials. This can be very helpful to document how
21+
* a template or partial is supposed to be used and which input variables are required.
22+
*
23+
* These requirements are enforced during rendering of the template or partial:
24+
* If an argument is defined with this ViewHelper which isn't marked as ``optional``,
25+
* an exception will be thrown if that variable isn't present during rendering.
26+
* If a variable doesn't match the specified type and can't be converted automatically,
27+
* an exception will be thrown as well.
28+
*
29+
* Note that ``f:argument`` ViewHelpers must be used at the root level of the
30+
* template, and can't be nested into other ViewHelpers. Also, the usage of variables
31+
* in any of its arguments is not possible (e. g. you can't define an argument name
32+
* by using a variable).
33+
*
34+
* Example
35+
* ========
36+
*
37+
* For the following partial:
38+
*
39+
* .. code-block:: xml
40+
*
41+
* <f:argument name="title" type="string" />
42+
* <f:argument name="tags" type="string[]" optional="{true}" />
43+
* <f:argument name="user" type="string" optional="{true}" default="admin" />
44+
*
45+
* Title: {title}<br />
46+
* <f:if condition="{tags}">
47+
* Tags: {tags -> f:join(separator: ', ')}<br />
48+
* </f:if>
49+
* User: {user}
50+
*
51+
* The following render calls will be successful:
52+
*
53+
* .. code-block:: xml
54+
*
55+
* <!-- All arguments supplied -->
56+
* <f:render partial="MyPartial" arguments="{title: 'My title', tags: {0: 'tag1', 1: 'tag2'}, user: 'me'}" />
57+
* <!-- "user" will fall back to default value -->
58+
* <f:render partial="MyPartial" arguments="{title: 'My title', tags: {0: 'tag1', 1: 'tag2'}}" />
59+
* <!-- "tags" will be "null", "user" will fall back to default value -->
60+
* <f:render partial="MyPartial" arguments="{title: 'My title'}" />
61+
*
62+
* The following render calls will result in an exception:
63+
*
64+
* .. code-block:: xml
65+
*
66+
* <!-- required "title" has not been supplied -->
67+
* <f:render partial="MyPartial" />
68+
* <!-- "user" has been supplied as array, not as string -->
69+
* <f:render partial="MyPartial" arguments="{title: 'My title', user: {firstName: 'Jane', lastName: 'Doe'}}" />
70+
*
71+
* @api
72+
*/
73+
final class ArgumentViewHelper extends AbstractViewHelper
74+
{
75+
/**
76+
* No need to add escaping nodes since the ViewHelper doesn't output anything
77+
*/
78+
protected $escapeOutput = false;
79+
80+
public function initializeArguments(): void
81+
{
82+
$this->registerArgument('name', 'string', 'name of the template argument', true);
83+
$this->registerArgument('type', 'string', 'type of the template argument', true);
84+
$this->registerArgument('description', 'string', 'description of the template argument');
85+
$this->registerArgument('optional', 'boolean', 'true if the defined argument should be optional', false, false);
86+
$this->registerArgument('default', 'mixed', 'default value for optional argument');
87+
}
88+
89+
public function render(): string
90+
{
91+
return '';
92+
}
93+
94+
public function compile($argumentsName, $closureName, &$initializationPhpCode, ViewHelperNode $node, TemplateCompiler $compiler): string
95+
{
96+
return '\'\'';
97+
}
98+
99+
public static function nodeInitializedEvent(ViewHelperNode $node, array $arguments, ParsingState $parsingState): void
100+
{
101+
// Create static values of supplied arguments. A new empty rendering context is used here
102+
// because argument definitions shouldn't be dependent on any variables in the template.
103+
// Any variables that are used anyway (e. g. in default values) will be interpreted as "null"
104+
$emptyRenderingContext = new RenderingContext();
105+
$evaluatedArguments = array_map(
106+
static fn(NodeInterface $node): mixed => $node->evaluate($emptyRenderingContext),
107+
$arguments,
108+
);
109+
$argumentName = (string)$evaluatedArguments['name'];
110+
111+
// Make sure that arguments are not nested into other ViewHelpers as this might create confusion
112+
if ($parsingState->hasNodeTypeInStack(ViewHelperNode::class)) {
113+
throw new \TYPO3Fluid\Fluid\Core\Parser\Exception(sprintf(
114+
'Template argument "%s" needs to be defined at the root level of the template, not within a ViewHelper.',
115+
$argumentName,
116+
), 1744908510);
117+
}
118+
119+
// Make sure that this argument hasn't already been defined in the template
120+
$argumentDefinitions = $parsingState->getArgumentDefinitions();
121+
if (isset($argumentDefinitions[$argumentName])) {
122+
throw new \TYPO3Fluid\Fluid\Core\Parser\Exception(sprintf(
123+
'Template argument "%s" has been defined multiple times.',
124+
$argumentName,
125+
), 1744908509);
126+
}
127+
128+
// Create argument definition to be interpreted later during rendering
129+
// This will also be written to the cache by the TemplateCompiler
130+
$argumentDefinitions[$argumentName] = new ArgumentDefinition(
131+
$argumentName,
132+
(string)$evaluatedArguments['type'],
133+
array_key_exists('description', $evaluatedArguments) ? (string)$evaluatedArguments['description'] : '',
134+
array_key_exists('optional', $evaluatedArguments) ? !$evaluatedArguments['optional'] : false,
135+
array_key_exists('default', $evaluatedArguments) ? $evaluatedArguments['default'] : null,
136+
);
137+
$parsingState->setArgumentDefinitions($argumentDefinitions);
138+
}
139+
}

0 commit comments

Comments
 (0)