Developing a custom ViewHelper

This chapter will demonstrate how to write a custom Fluid ViewHelper in TYPO3.

A "Gravatar" ViewHelper is created, which uses an email address as parameter and shows the picture from gravatar.com if it exists.

The official documentation of Fluid for writing custom ViewHelpers can be found within the Fluid documentation: Creating ViewHelpers.

Fluid

The custom ViewHelper is not part of the default distribution. Therefore a namespace import is necessary to use this ViewHelper. In the following example, the namespace \MyVendor\MyExtension\ViewHelpers is imported with the prefix m. Now, all tags starting with m: are interpreted as ViewHelper from within this namespace:

EXT:my_extension/Resources/Private/Templates/SomeTemplate.html
{namespace m=MyVendor\MyExtension\ViewHelpers}

For further information about namespace import, see Import ViewHelper namespaces.

The ViewHelper should be given the name "gravatar" and only take an email address as a parameter. The ViewHelper is called in the template as follows:

EXT:my_extension/Resources/Private/Templates/SomeTemplate.html
<m:gravatar emailAddress="username@example.org" />

AbstractViewHelper implementation

Every ViewHelper is a PHP class. For the Gravatar ViewHelper, the name of the class is \MyVendor\MyExtension\ViewHelpers\GravatarViewHelper.

EXT:my_extension/Classes/ViewHelpers/GravatarViewHelper.php
 1<?php
 2
 3declare(strict_types=1);
 4
 5namespace MyVendor\MyExtension\ViewHelpers;
 6
 7use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
 8use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
 9use TYPO3Fluid\Fluid\Core\ViewHelper\Traits\CompileWithRenderStatic;
10
11final class GravatarViewHelper extends AbstractViewHelper
12{
13    use CompileWithRenderStatic;
14
15    protected $escapeOutput = false;
16
17    public function initializeArguments(): void
18    {
19        // registerArgument($name, $type, $description, $required, $defaultValue, $escape)
20        $this->registerArgument('emailAddress', 'string', 'The email address to resolve the gravatar for', true);
21    }
22
23    public static function renderStatic(
24        array $arguments,
25        \Closure $renderChildrenClosure,
26        RenderingContextInterface $renderingContext,
27    ): string {
28        // this is improved with the TagBasedViewHelper (see below)
29        return sprintf('<img src="https://www.gravatar.com/avatar/%s" />', md5($arguments['emailAddress']));
30    }
31}

AbstractViewHelper

line 11

Every ViewHelper must inherit from the class \TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper.

A ViewHelper can also inherit from subclasses of AbstractViewHelper, e.g. from \TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper. Several subclasses are offering additional functionality. The TagBasedViewHelper will be explained later on in this chapter in detail.

Escaping of output

line 15

By default, all output is escaped by htmlspecialchars to prevent cross site scripting.

Setting the property $escapeOutput to false is necessary to prevent escaping of ViewHelper output.

By setting the property $escapeChildren to false, escaping of the tag content (its child nodes) can be disabled. If this is not set explicitly, the value will be determined automatically: If $escapeOutput: is true, $escapeChildren will be disabled to prevent double escaping. If $escapeOutput: is false, $escapeChildren will be enabled unless disabled explicitly.

Passing in children is explained in Prepare ViewHelper for inline syntax.

initializeArguments()

line 17

The Gravatar ViewHelper must hand over the email address which identifies the Gravatar. Every ViewHelper has to declare which parameters are accepted explicitly. The registration happens inside initializeArguments().

In the example above, the ViewHelper receives the argument emailAddress of type string. These arguments can be accessed through the array $arguments, which is passed into the renderStatic() method (see next section).

Tip

Sometimes arguments can take various types. In this case, the type mixed should be used.

renderStatic()

line 23

The method renderStatic() is called once the ViewHelper is rendered. The return value of the method is rendered directly.

line 13

The trait CompileWithRenderStatic must be used if the class implements renderStatic().

Creating HTML/XML tags with the AbstractTagBasedViewHelper

For ViewHelpers which create HTML/XML tags, Fluid provides an enhanced base class: \TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper. This base class provides an instance of \TYPO3Fluid\Fluid\Core\ViewHelper\TagBuilder that can be used to create HTML-tags. It takes care of the syntactically correct creation and, for example, escapes single and double quotes in attribute values.

Attention

Correctly escaping the attribute values is mandatory as it affects security and prevents cross-site scripting attacks.

Because the Gravatar ViewHelper creates an img tag the use of the \TYPO3Fluid\Fluid\Core\ViewHelper\TagBuilder is advised:

EXT:my_extension/Classes/ViewHelpers/GravatarViewHelper.php
 1<?php
 2
 3declare(strict_types=1);
 4
 5namespace MyVendor\MyExtension\ViewHelpers;
 6
 7use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper;
 8
 9final class GravatarViewHelper extends AbstractTagBasedViewHelper
10{
11    protected $tagName = 'img';
12
13    public function initializeArguments(): void
14    {
15        parent::initializeArguments();
16        $this->registerUniversalTagAttributes();
17        $this->registerTagAttribute('alt', 'string', 'Alternative text for the image');
18        $this->registerArgument('emailAddress', 'string', 'The email address to resolve the gravatar for', true);
19    }
20
21    public function render(): string
22    {
23        $this->tag->addAttribute(
24            'src',
25            'https://www.gravatar.com/avatar/' . md5($this->arguments['emailAddress']),
26        );
27        return $this->tag->render();
28    }
29}

What is different in this code?

The attribute $escapeOutput is no longer necessary.

AbstractTagBasedViewHelper

line 6

The ViewHelper does not inherit directly from AbstractViewHelper but from AbstractTagBasedViewHelper, which provides and initializes the tag builder.

$tagName

line 9

There is a class property $tagName which stores the name of the tag to be created (<img>).

$this->tag->addAttribute()

line 23

The tag builder is available at property $this->tag. It offers the method addAttribute() to add new tag attributes. In our example the attribute src is added to the tag.

$this->tag->render()

line 27

The GravatarViewHelper creates an img tag builder, which has a method named render(). After configuring the tag builder instance, the rendered tag markup is returned.

Note

As $this->tag is an object property, render() is used to generate the output. renderStatic() would have no access. For further information take a look at The different render methods.

$this->registerTagAttribute()

Furthermore the TagBasedViewHelper offers assistance for ViewHelper arguments that should recur directly and unchanged as tag attributes. These must be registered with the method $this->registerTagAttribute() within initializeArguments. If support for the <img> attribute alt should be provided in the ViewHelper, this can be done by initializing this in initializeArguments() in the following way:

EXT:my_extension/Classes/ViewHelpers/GravatarViewHelper.php
public function initializeArguments(): void
{
   // registerTagAttribute($name, $type, $description, $required = false)
   $this->registerTagAttribute('alt', 'string', 'Alternative Text for the image');
}

For registering the universal attributes id, class, dir, style, lang, title, accesskey and tabindex there is a helper method registerUniversalTagAttributes() available.

If support for universal attributes should be provided and in addition to the alt attribute in the Gravatar ViewHelper the following initializeArguments() method will be necessary:

EXT:my_extension/Classes/ViewHelpers/GravatarViewHelper.php
public function initializeArguments(): void
{
   parent::initializeArguments();
   $this->registerUniversalTagAttributes();
   $this->registerTagAttribute('alt', 'string', 'Alternative Text for the image');
}

Insert optional arguments

An optional size for the image can be provided to the Gravatar ViewHelper. This size parameter will determine the height and width in pixels of the image and can range from 1 to 512. When no size is given, an image of 80px is generated.

The render() method can be improved like this:

EXT:my_extension/Classes/ViewHelpers/GravatarViewHelper.php
public function initializeArguments(): void
{
   $this->registerArgument('emailAddress', 'string', 'The email address to resolve the gravatar for', true);
   $this->registerArgument('size', 'integer', 'The size of the gravatar, ranging from 1 to 512', false, 80);
}

public function render(): string
{
   $this->tag->addAttribute(
      'src',
      'http://www.gravatar.com/avatar/' .
          md5($this->arguments['emailAddress']) .
          '?s=' . urlencode($this->arguments['size'])
   );
   return $this->tag->render();
}

With this setting of a default value and setting the fourth argument to false, the size attribute becomes optional.

Prepare ViewHelper for inline syntax

So far, the Gravatar ViewHelper has focused on the tag structure of the ViewHelper. The call to render the ViewHelper was written with tag syntax, which seemed obvious because it itself returns a tag:

EXT:my_extension/Resources/Private/Templates/SomeTemplate.html
<m:gravatar emailAddress="{post.author.emailAddress}" />

Alternatively, this expression can be written using the inline notation:

EXT:my_extension/Resources/Private/Templates/SomeTemplate.html
{m:gravatar(emailAddress: post.author.emailAddress)}

One should see the Gravatar ViewHelper as a kind of post-processor for an email address and would allow the following syntax:

EXT:my_extension/Resources/Private/Templates/SomeTemplate.html
{post.author.emailAddress -> m:gravatar()}

This syntax places focus on the variable that is passed to the ViewHelper as it comes first.

The syntax {post.author.emailAddress -> m:gravatar()} is an alternative syntax for <m:gravatar>{post.author.emailAddress}</m:gravatar>. To support this, the email address comes either from the argument emailAddress or, if it is empty, the content of the tag should be interpreted as email address.

This is typically used with formatting ViewHelpers. These ViewHelpers all support both tag mode and inline syntax.

Depending on the implemented method for rendering, the implementation is different:

With renderStatic()

To fetch the content of the ViewHelper, the argument $renderChildrenClosure is available. This returns the evaluated object between the opening and closing tag.

Lets have a look at the new code of the renderStatic() method:

EXT:my_extension/Classes/ViewHelpers/GravatarViewHelper.php
 1<?php
 2
 3declare(strict_types=1);
 4
 5namespace MyVendor\MyExtension\ViewHelpers;
 6
 7use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
 8use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
 9use TYPO3Fluid\Fluid\Core\ViewHelper\Traits\CompileWithContentArgumentAndRenderStatic;
10
11final class GravatarViewHelper extends AbstractViewHelper
12{
13    use CompileWithContentArgumentAndRenderStatic;
14
15    protected $escapeOutput = false;
16
17    public function initializeArguments(): void
18    {
19        $this->registerArgument('emailAddress', 'string', 'The email address to resolve the gravatar for');
20    }
21
22    public static function renderStatic(
23        array $arguments,
24        \Closure $renderChildrenClosure,
25        RenderingContextInterface $renderingContext,
26    ): string {
27        $emailAddress = $renderChildrenClosure();
28
29        return sprintf('<img src="https://www.gravatar.com/avatar/%s" />', md5($emailAddress));
30    }
31}

With render()

To fetch the content of the ViewHelper the method renderChildren() is available in the AbstractViewHelper. This returns the evaluated object between the opening and closing tag.

Lets have a look at the new code of the render() method:

EXT:my_extension/Classes/ViewHelpers/GravatarViewHelper.php
 1<?php
 2
 3declare(strict_types=1);
 4
 5namespace MyVendor\BlogExample\ViewHelpers;
 6
 7use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper;
 8
 9final class GravatarViewHelper extends AbstractTagBasedViewHelper
10{
11    protected $tagName = 'img';
12
13    public function initializeArguments(): void
14    {
15        $this->registerArgument('emailAddress', 'string', 'The email address to resolve the gravatar for', false, null);
16    }
17
18    public function render(): string
19    {
20        $emailAddress = $this->arguments['emailAddress'] ?? $this->renderChildren();
21
22        $this->tag->addAttribute(
23            'src',
24            'https://www.gravatar.com/avatar/' . md5($emailAddress),
25        );
26
27        return $this->tag->render();
28    }
29}

Handle additional arguments

If a ViewHelper allows further arguments which have not been explicitly configured, the handleAdditionalArguments() method can be implemented.

The \TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper makes use of this, to allow setting any data- argument for tag based ViewHelpers.

For example, the AbstractTagBasedViewHelper implements the following:

EXT:fluid/Classes/ViewHelpers/AbstractTagBasedViewHelper.php
public function handleAdditionalArguments(array $arguments)
{
    $this->additionalArguments = $arguments;
    parent::handleAdditionalArguments($arguments);
}

To keep the default behavior, all unwanted arguments should be passed to the parent method call parent::handleAdditionalArguments($unassigned);, to throw exceptions accordingly.

The different render methods

ViewHelpers can have one or more of the following three methods for implementing the rendering. The following section will describe the differences between all three implementations.

compile()-Method

This method can be overwritten to define how the ViewHelper should be compiled. That can make sense if the ViewHelper itself is a wrapper for another native PHP function or TYPO3 function. In that case, the method can return the call to this function and remove the need to call the ViewHelper as a wrapper at all.

The compile() has to return the compiled PHP code for the ViewHelper. Also the argument $initializationPhpCode can be used to add further PHP code before the execution.

Note

The renderStatic() method still has to be implemented for the non compiled version of the ViewHelper. In the future, this should no longer be necessary.

Example implementation:

EXT:my_extension/Classes/ViewHelpers/StrtolowerViewHelper.php
<?php

declare(strict_types=1);

namespace MyVendor\BlogExample\ViewHelpers;

use TYPO3Fluid\Fluid\Core\Compiler\TemplateCompiler;
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\ViewHelperNode;
use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
use TYPO3Fluid\Fluid\Core\ViewHelper\Traits\CompileWithRenderStatic;

final class StrtolowerViewHelper extends AbstractViewHelper
{
    use CompileWithRenderStatic;

    public function initializeArguments(): void
    {
        $this->registerArgument('string', 'string', 'The string to lowercase.', true);
    }

    public static function renderStatic(
        array $arguments,
        \Closure $renderChildrenClosure,
        RenderingContextInterface $renderingContext,
    ): string {
        return strtolower($arguments['string']);
    }

    public function compile(
        $argumentsName,
        $closureName,
        &$initializationPhpCode,
        ViewHelperNode $node,
        TemplateCompiler $compiler,
    ): string {
        return 'strtolower(' . $argumentsName . '[\'string\'])';
    }
}

renderStatic()-Method

Most of the time, this method is implemented. It's the one that is called by default from within the compiled Fluid.

It is, however, not called on AbstractTagBasedViewHelper implementations. With these classes you still need to use the render() method since that is the only way you can access $this->tag which contains the tag builder that generates the actual XML tag.

As this method has to be static, there is no access to object properties such as $this->tag (in a subclass of AbstractTagBasedViewHelper) from within renderStatic.

Note

This method can not be used when access to child nodes is necessary. This is the case for ViewHelpers like if or switch which need to access their children like then or else. In that case, render() has to be used.

render()-Method

This method is the slowest one. Only use this method if it is necessary, for example if access to properties is necessary.