From 7b26b88463125fc16ef6f57a402448ad6719f911 Mon Sep 17 00:00:00 2001 From: Sybille Peters Date: Wed, 13 Jul 2022 11:24:45 +0200 Subject: [PATCH] Move page "Create custom ViewHelper" from Extbase book (#1953) (#1954) * [TASK] Move Develop Custom ViewHelper from Extbase book The page "Developing a custom ViewHelper" was copied from the Extbase book. The text will be optimized in subsequent pull requests Related: https://github.com/TYPO3-Documentation/TYPO3CMS-Book-ExtbaseFluid#536 * Reduce content for custom ViewHelper The previous text for developing a custom ViewHelper used several steps to add more and more functionality to the ViewHelper. While such an approach often makes sense, here this was applied very fine granular, with many steps. This resulted in a lot of code duplication. So, the multiple step approach was kept, but the first few steps were merged into one code snippet with the various parts explained in the following sections. Having the complete code snippet in one place, makes it shorter and easier to read (and has less code duplication). * Further optimization of custom ViewHelper page --- .../Fluid/DevelopCustomViewhelper.rst | 600 ++++++++++++++++++ Documentation/ApiOverview/Fluid/Index.rst | 3 +- 2 files changed, 602 insertions(+), 1 deletion(-) create mode 100644 Documentation/ApiOverview/Fluid/DevelopCustomViewhelper.rst diff --git a/Documentation/ApiOverview/Fluid/DevelopCustomViewhelper.rst b/Documentation/ApiOverview/Fluid/DevelopCustomViewhelper.rst new file mode 100644 index 0000000000..887c74a690 --- /dev/null +++ b/Documentation/ApiOverview/Fluid/DevelopCustomViewhelper.rst @@ -0,0 +1,600 @@ +.. include:: /Includes.rst.txt +.. index:: + ViewHelpers; Custom + Fluid; Custom ViewHelpers +.. _fluid-custom-viewhelper: + +============================== +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 Fluid documentation at +https://github.com/TYPO3/Fluid/blob/master/doc/FLUID_CREATING_VIEWHELPERS.md. + +.. contents:: Contents of this page + :local: + :depth: 2 + + +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 :php:`\MyVendor\BlogExample\ViewHelpers` is imported with the +prefix `blog`. Now, all tags starting with `blog:` are interpreted as +ViewHelper from within this namespace: + +.. code-block:: html + :caption: EXT:blog_example/Resources/Private/Templates/SomeTemplate.html + + {namespace blog=MyVendor\BlogExample\ViewHelpers} + +For further information about namespace import, see +:ref:`fluid-syntax-viewhelpers-import-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: + +.. code-block:: html + :caption: EXT:blog_example/Resources/Private/Templates/SomeTemplate.html + + + +See :ref:`global-namespace-import` for information how to import +namespaces globally. + +AbstractViewHelper implementation +================================= + +Every ViewHelper is a PHP class. For the Gravatar ViewHelper, the name of the +class is :php:`\MyVendor\BlogExample\ViewHelpers\GravatarViewHelper`. + +.. code-block:: php + :caption: EXT:blog_example/Classes/ViewHelpers/GravatarViewHelper.php + :linenos: + + registerArgument('emailAddress', 'string', 'The email address to resolve the gravatar for', true); + } + + public static function renderStatic( + array $arguments, + \Closure $renderChildrenClosure, + RenderingContextInterface $renderingContext + ) { + // this is improved with the TagBasedViewHelper (see below) + return ''; + } + } + +:php:`AbstractViewHelper` +------------------------- + +*line 7* + +Every ViewHelper must inherit from the class +:php:`\TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper`. + +A ViewHelper can also inherit from subclasses of :php:`AbstractViewHelper`, e.g. +from :php:`\TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper`. +Several subclasses are offering additional functionality. The +:php:`TagBasedViewHelper` will be explained :ref:`later on in this chapter +` in detail. + +.. _fluid-viewhelper-custom-escaping-of-output: + +Escaping of output +------------------ + +*line 11* + +Setting the property :php:`$escapeOutput` to false is necessary to prevent +escaping of output. + +By default, all output is escaped by :php:`htmlspecialchars` to prevent cross +site scripting. + +If escaping of children is disabled, no nodes passed with inline +syntax or values used as tag content will be escaped. Note that +:php:`$escapeOutput` takes priority: if it is disabled, escaping child nodes +is also disabled unless explicitly enabled. + +Passing in children is explained in :ref:`prepare-viewhelper-for-inline-syntax`. + +.. _fluid-viewhelper-custom-initializeArguments: + +:php:`initializeArguments()` +---------------------------- + +*line 13* + +The :php:`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 :php:`initializeArguments()`. + +In the example above, the ViewHelper receives the argument `emailAddress` of +type `string`. These arguments can be accessed +through the array :php:`$arguments`, which is passed into the :php:`renderStatic()` +method (see :ref:`next section `). + +.. tip:: + + Sometimes arguments can take various types. In this case, the type `mixed` + should be used. + +.. _fluid-viewhelper-custom-renderStatic: + +:php:`renderStatic()` +--------------------- + +*line 19* + +The method :php:`renderStatic()` is called once the ViewHelper is rendered. The +return value of the method is rendered directly. + +* line 9* + +The trait :php:`CompileWithRenderStatic` must be used if the class implements +:php:`renderStatic()`. + +.. _creating-xml-tags-using-tagbasedviewhelper: +.. _creating-html-tags-using-tagbasedviewhelper: + +Creating HTML/XML tags with the :php:`AbstractTagBasedViewHelper` +================================================================= + +For ViewHelpers which create HTML/XML tags, Fluid provides an enhanced base +class: :php:`\TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper`. This +base class provides an instance of +:php:`\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 :html:`img` tag the use of the +:php:`\TYPO3Fluid\Fluid\Core\ViewHelper\TagBuilder` is advised: + +.. code-block:: php + :caption: EXT:blog_example/Classes/ViewHelpers/GravatarViewHelper.php + :linenos: + + registerUniversalTagAttributes(); + $this->registerTagAttribute('alt', 'string', 'Alternative Text for the image'); + $this->registerArgument('emailAddress', 'string', 'The email address to resolve the gravatar for', true); + } + + public function render() + { + $this->tag->addAttribute( + 'src', + 'http://www.gravatar.com/avatar/' . md5($this->arguments['emailAddress']) + ); + return $this->tag->render(); + } + } + +What is different in this code? + +The attribute :php:`$escapeOutput` is no longer necessary. + +:php:`AbstractTagBasedViewHelper` +--------------------------------- + +*line 6* + +The ViewHelper does not inherit directly from :php:`AbstractViewHelper` but +from :php:`AbstractTagBasedViewHelper`, which provides and initializes the tag builder. + +:php:`$tagName` +--------------- + +*line 8* + +There is a class property :php:`$tagName` which stores the name of the tag to be +created (:html:``). + +:php:`$this->tag->addAttribute()` +--------------------------------- + +*line 20* + +The tag builder is available at property :php:`$this->tag`. It offers the method +:php:`addAttribute()` to add new tag attributes. In our example the attribute +`src` is added to the tag. + +:php:`$this->tag->addAttribute()` +--------------------------------- + +*line 24* + +The GravatarViewHelper creates an img tag builder, which has a method named +:php:`render()`. After configuring the tag builder instance, the rendered tag +markup is returned. + +.. note:: + + As :php:`$this->tag` is an object property, :php:`render()` is used to + generate the output. :php:`renderStatic()` would have no access. For further + information take a look at :ref:`the-different-render-methods`. + +:php:`$this->registerTagAttribute()` +------------------------------------ + +Furthermore the :php:`TagBasedViewHelper` offers assistance for ViewHelper +arguments that should recur directly and unchanged as tag attributes. These +must be registered with the method :php:`$this->registerTagAttribute()` +within :php:`initializeArguments`. +If support for the :html:`` attribute :html:`alt` +should be provided in the ViewHelper, this can be done by initializing this in +:php:`initializeArguments()` in the following way: + +.. code-block:: php + :caption: EXT:blog_example/Classes/ViewHelpers/GravatarViewHelper.php + + public function initializeArguments() + { + // 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 +:php:`registerUniversalTagAttributes()` available. + +If support for universal attributes should be provided and in addition to the +`alt` attribute in the Gravatar ViewHelper the following +:php:`initializeArguments()` method will be necessary: + +.. code-block:: php + :caption: EXT:blog_example/Classes/ViewHelpers/GravatarViewHelper.php + + public function initializeArguments() + { + parent::initializeArguments(); + $this->registerUniversalTagAttributes(); + $this->registerTagAttribute('alt', 'string', 'Alternative Text for the image'); + } + +.. _insert-optional-arguments: + +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 :php:`render()` method can be improved like this: + +.. code-block:: php + :caption: EXT:blog_example/Classes/ViewHelpers/GravatarViewHelper.php + + public function initializeArguments() + { + $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() + { + $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: + +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: + +.. code-block:: html + :caption: EXT:blog_example/Resources/Private/Templates/SomeTemplate.html + + + +Alternatively, this expression can be written using the inline notation: + +.. code-block:: html + :caption: EXT:blog_example/Resources/Private/Templates/SomeTemplate.html + + {blog: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: + +.. code-block:: html + :caption: EXT:blog_example/Resources/Private/Templates/SomeTemplate.html + + {post.author.emailAddress -> blog:gravatar()} + +This syntax places focus on the variable that is passed to the ViewHelper as it +comes first. + +The syntax `{post.author.emailAddress -> blog:gravatar()}` is an alternative +syntax for `{post.author.emailAddress}`. 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: + +With :php:`renderStatic()` +-------------------------- + +To fetch the content of the ViewHelper, the argument +:php:`$renderChildrenClosure` is available. This returns the evaluated object +between the opening and closing tag. + +Lets have a look at the new code of the :php:`renderStatic()` method: + +.. code-block:: php + :caption: EXT:blog_example/Classes/ViewHelpers/GravatarViewHelper.php + + registerArgument('emailAddress', 'string', 'The email address to resolve the gravatar for'); + } + + public static function renderStatic( + array $arguments, + \Closure $renderChildrenClosure, + RenderingContextInterface $renderingContext + ) { + $emailAddress = $renderChildrenClosure(); + + return ''; + } + } + +.. _with-render: + +With :php:`render()` +-------------------- + +To fetch the content of the ViewHelper the method :php:`renderChildren()` is +available in the :php:`AbstractViewHelper`. This returns the evaluated object +between the opening and closing tag. + +Lets have a look at the new code of the :php:`render()` method: + +.. code-block:: php + :caption: EXT:blog_example/Classes/ViewHelpers/GravatarViewHelper.php + + registerArgument('emailAddress', 'string', 'The email address to resolve the gravatar for', false, null); + } + + public function render() + { + $emailAddress = $this->arguments['emailAddress'] ?? $this->renderChildren(); + + $this->tag->addAttribute( + 'src', + 'http://www.gravatar.com/avatar/' . md5($emailAddress) + ); + + return $this->tag->render(); + } + } + + + +.. _handle-additional-arguments: + +Handle additional arguments +=========================== + +If a ViewHelper allows further arguments which have not been explicitly +configured, the :php:`handleAdditionalArguments()` method can be implemented. + +The :php:`\TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper` makes use +of this, to allow setting any `data-` argument for tag based ViewHelpers. + +The method will receive an array of all arguments, which are passed in addition +to the registered arguments. The array uses the argument name as the key and the +argument value as the value. Within the method, these arguments can be handled. + +For example, the :php:`AbstractTagBasedViewHelper` implements the following: + +.. code-block:: php + :caption: EXT:fluid/Classes/ViewHelpers/AbstractTagBasedViewHelper.php + + public function handleAdditionalArguments(array $arguments) + { + $unassigned = []; + foreach ($arguments as $argumentName => $argumentValue) { + if (strpos($argumentName, 'data-') === 0) { + $this->tag->addAttribute($argumentName, $argumentValue); + } else { + $unassigned[$argumentName] = $argumentValue; + } + } + parent::handleAdditionalArguments($unassigned); + } + +To keep the default behavior, all unwanted arguments should be passed to the +parent method call :php:`parent::handleAdditionalArguments($unassigned);`, to +throw exceptions accordingly. + +.. _the-different-render-methods: + +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: + +:php:`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 :php:`compile()` has to return the compiled PHP code for the ViewHelper. +Also the argument :php:`$initializationPhpCode` can be used to add further PHP +code before the execution. + +.. note:: + + The :php:`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: + +.. code-block:: php + :caption: EXT:blog_example/Classes/ViewHelpers/StrtolowerViewHelper.php + + registerArgument('string', 'string', 'The string to lowercase.', true); + } + + public static function renderStatic( + array $arguments, + \Closure $renderChildrenClosure, + RenderingContextInterface $renderingContext + ) { + return strtolower($arguments['string']); + } + + public function compile( + $argumentsName, + $closureName, + &$initializationPhpCode, + ViewHelperNode $node, + TemplateCompiler $compiler + ) { + return 'strtolower(' . $argumentsName. '[\'string\'])'; + } + } + +.. _renderstatic-method: + +:php:`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 :php:`render()` method since that is the only way you can access :php:`$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 +:php:`$this->tag` (in a subclass of :php:`AbstractTagBasedViewHelper`) from within +:php:`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, :php:`render()` has to be used. + +.. _render-method: + +:php:`render()`-Method +---------------------- + +This method is the slowest one. Only use this method if it is necessary, for +example if access to properties is necessary. diff --git a/Documentation/ApiOverview/Fluid/Index.rst b/Documentation/ApiOverview/Fluid/Index.rst index ae8a14a2c4..c457469eba 100644 --- a/Documentation/ApiOverview/Fluid/Index.rst +++ b/Documentation/ApiOverview/Fluid/Index.rst @@ -43,4 +43,5 @@ You can use Fluid in TYPO3 to do one of the following: Introduction Syntax - ViewHelper reference + DevelopCustomViewhelper + ViewHelper reference