Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Documentation/Extending/ViewHelpers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,17 @@ ViewHelpers might be ones that implement `strip_tags`, `nl2br` or other
string-manipulating PHP functions). And data ViewHelpers may return any type,
but must be used a bit more carefully.

Tag-based ViewHelpers can now also return the :php:`TagBuilder` instance
directly instead of rendering it to a string immediately. This is a special
case compared to arbitrary string-compatible objects: :php:`TagBuilder`
implements :php:`UnsafeHTML`, so Fluid treats the returned value as HTML output
that must not be escaped again.

This keeps the structured tag object reusable after the ViewHelper itself has
finished rendering. For example, an image ViewHelper can return its
:php:`TagBuilder`, and another layer can still add or remove an attribute afterwards
before the final output is converted to a string.

In other words: be careful what data types your ViewHelper returns.
Non-string-compatible values may cause problems if you use the ViewHelper in
ways that were not intended. Like in PHP, data types must either match or be
Expand Down
4 changes: 2 additions & 2 deletions src/Core/ViewHelper/AbstractTagBasedViewHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,8 @@ public function validateAdditionalArguments(array $arguments): void
// Skip validation of additional arguments since we want to pass all arguments to the tag
}

public function render(): string
public function render(): string|TagBuilder
{
return $this->tag->render();
return $this->tag;
}
}
9 changes: 8 additions & 1 deletion src/Core/ViewHelper/TagBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@

namespace TYPO3Fluid\Fluid\Core\ViewHelper;

use TYPO3Fluid\Fluid\Core\Parser\UnsafeHTML;

/**
* Tag builder. Can be easily accessed in AbstractTagBasedViewHelper
*
* @api
*/
class TagBuilder
class TagBuilder implements UnsafeHTML
{
/**
* Name of the Tag to be rendered
Expand Down Expand Up @@ -298,4 +300,9 @@ public function render(): string
}
return $output;
}

public function __toString(): string
{
return $this->render();
}
}
53 changes: 53 additions & 0 deletions tests/Functional/Core/ViewHelper/TagBuilderChainingTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

declare(strict_types=1);

/*
* This file belongs to the package "TYPO3 Fluid".
* See LICENSE.txt that was shipped with this package.
*/

namespace TYPO3Fluid\Fluid\Tests\Functional\Core\ViewHelper;

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use TYPO3Fluid\Fluid\Tests\Functional\AbstractFunctionalTestCase;
use TYPO3Fluid\Fluid\View\TemplateView;

final class TagBuilderChainingTest extends AbstractFunctionalTestCase
{
public static function chainedTagBuilderCanBeMutatedDataProvider(): array
{
return [
'tag syntax' => [
'<test:tagMutation attributeValue="{second}"><test:tagBasedTest data="{first: \'one\'}">content</test:tagBasedTest></test:tagMutation>',
['second' => 'two'],
'<div data-first="one" data-second="two">content</div>',
],
'inline syntax' => [
'{test:tagBasedTest(data: {first: \'one\'}) -> test:tagMutation(attributeValue: second)}',
['second' => 'two'],
'<div data-first="one" data-second="two" />',
],
];
}

#[DataProvider('chainedTagBuilderCanBeMutatedDataProvider')]
#[Test]
public function chainedTagBuilderCanBeMutated(string $source, array $variables, string $expected): void
{
$view = new TemplateView();
$view->assignMultiple($variables);
$view->getRenderingContext()->setCache(self::$cache);
$view->getRenderingContext()->getTemplatePaths()->setTemplateSource($source);
$view->getRenderingContext()->getViewHelperResolver()->addNamespace('test', 'TYPO3Fluid\\Fluid\\Tests\\Functional\\Fixtures\\ViewHelpers');
self::assertSame($expected, $view->render(), 'uncached');

$view = new TemplateView();
$view->assignMultiple($variables);
$view->getRenderingContext()->setCache(self::$cache);
$view->getRenderingContext()->getTemplatePaths()->setTemplateSource($source);
$view->getRenderingContext()->getViewHelperResolver()->addNamespace('test', 'TYPO3Fluid\\Fluid\\Tests\\Functional\\Fixtures\\ViewHelpers');
self::assertSame($expected, $view->render(), 'cached');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ private function renderCode(ViewHelperInterface $mutableViewHelper, string $flui
$view = new TemplateView($context);
$view->getRenderingContext()->getTemplatePaths()->setTemplateSource($fluidCode);

return $view->render();
return (string)$view->render();
}

/*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
namespace TYPO3Fluid\Fluid\Tests\Functional\Fixtures\ViewHelpers;

use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper;
use TYPO3Fluid\Fluid\Core\ViewHelper\TagBuilder;

final class TagBasedTestViewHelper extends AbstractTagBasedViewHelper
{
Expand All @@ -20,10 +21,10 @@ public function initializeArguments(): void
$this->registerArgument('registeredBooleanArgument', 'boolean', 'boolean argument', false, false);
}

public function render(): string
public function render(): TagBuilder
{
$this->tag->addAttribute('registeredBooleanArgument', $this->arguments['registeredBooleanArgument']);
$this->tag->setContent($this->renderChildren());
return $this->tag->render();
return $this->tag;
}
}
38 changes: 38 additions & 0 deletions tests/Functional/Fixtures/ViewHelpers/TagMutationViewHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

declare(strict_types=1);

/*
* This file belongs to the package "TYPO3 Fluid".
* See LICENSE.txt that was shipped with this package.
*/

namespace TYPO3Fluid\Fluid\Tests\Functional\Fixtures\ViewHelpers;

use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
use TYPO3Fluid\Fluid\Core\ViewHelper\InvalidArgumentValueException;
use TYPO3Fluid\Fluid\Core\ViewHelper\TagBuilder;

final class TagMutationViewHelper extends AbstractViewHelper
{
public function initializeArguments(): void
{
$this->registerArgument('value', 'mixed', 'Tag to mutate');
$this->registerArgument('attributeValue', 'string', 'Value of the added data-second attribute', true);
}

public function getContentArgumentName(): string
{
return 'value';
}

public function render(): TagBuilder
{
$tag = $this->renderChildren();
if (!$tag instanceof TagBuilder) {
throw new InvalidArgumentValueException('TagMutationViewHelper expects a TagBuilder as input.', 1745483101);
}
$tag->addAttribute('data-second', $this->arguments['attributeValue']);
return $tag;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,8 @@ final class AbstractTagBasedViewHelperTest extends TestCase
public function renderCallsRenderOnTagBuilder(): void
{
$tagBuilder = $this->createMock(TagBuilder::class);
$tagBuilder->expects(self::once())->method('render')->willReturn('foobar');
$subject = new AbstractTagBasedViewHelperTestFixture();
$subject->setTagBuilder($tagBuilder);
self::assertEquals('foobar', $subject->render());
self::assertEquals($tagBuilder, $subject->render());
}
}
Loading