9. März 2018

Conditional view helper in Flow 4.0

With the new TYPO3Fluid Package the behavior of conditional view helpers changed drastically. Overriding the render() method is not enough. Now there is a static function evaluateCondition(). When updating from a Flow Version prior to 4.0 you need to rewrite self implemented conditional view helpers by implementing the new static method and adapt the render method.

An easy case is this example:

<?php
namespace Networkteam\Customer\ViewHelpers;

class IfContainsChangeViewHelper extends \Neos\FluidAdaptor\Core\ViewHelper\AbstractConditionViewHelper {

	/**
	 * @param array $change
	 * @return string
	 */
	public function render($change) {
		if (!empty($change['previous']) || !empty($change['current'])) {
			return $this->renderThenChild();
		} else {
			return $this->renderElseChild();
		}
	}
}

As you one can see this is a quite simple conditional view helper. Updating this one to Flow >= 4.0 works by implementing the static method and adapt the render call.

<?php
namespace Networkteam\Customer\ViewHelpers;

use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;

class IfContainsChangeViewHelper extends \Neos\FluidAdaptor\Core\ViewHelper\AbstractConditionViewHelper {

	/**
	 * @param array $arguments
	 * @param RenderingContextInterface $renderingContext
	 * @return bool
	 */
	protected static function evaluateCondition($arguments = null, RenderingContextInterface $renderingContext)
	{
		$change = $arguments['change'];
		return !empty($change['previous']) || !empty($change['current']);
	}

	/**
	 * Returns true if the change has a previous or current item
	 *
	 * @param array $change
	 * @return string
	 */
	public function render($change) {
		if (self::evaluateCondition(['change' => $change], $this->renderingContext)) {
			return $this->renderThenChild();
		} else {
			return $this->renderElseChild();
		}
	}
}

Keep an eye on the render() method, it now contains the static method call to keep the logic of the view helper in one place. The arguments to the static method will be passed as an array containing the names als key, like $change => ['change' => $change].

In the static method the arguments are fetched from the $arguments parameter.

This is a really simple one. Let's take a look into a more advanced example.

Using dependencies and implementing renderStatic()

The following view helper has a dependency to the DocumentRepository and also writes variables into the template variable container to make them accessible in the template.

<?php
namespace Networkteam\Customer\ViewHelpers;

/**
 * ViewHelper to check whether the resource attached to the given document already exists in a different document
 */
class IfResourceExistsViewHelper extends AbstractConditionViewHelper {

	/**
	 * @Flow\Inject
	 * @var DocumentRepository
	 */
	protected $documentRepository;

	/**
	 * Returns true if $document has a non-unique resource
	 *
	 * @param Document $document
	 * @return string The rendered child nodes
	 */
	public function render(Document $document = NULL) {
		if ($document === NULL || $document->getResource() === NULL) {
			return $this->renderElseChild();
		}
		$existingDocument = $this->documentRepository->findOneWithSameResource($document);
		if ($existingDocument !== NULL) {
			$this->renderingContext->getTemplateVariableContainer()->add('existingDocument', $existingDocument);
			$content = $this->renderThenChild();
			$this->templateVariableContainer->remove('existingDocument');
			return $content;
		} else {
			return $this->renderElseChild();
		}
	}
}

The view helper keeps track of duplicated resources when uploading them. To give the user a hint which and where this resource can be found, the document containing the resource already, is passed to the template to print the title for example.

Rewriting this is a little bit more work and has some pitfalls to drive around. First of all it is not possible to use @Flow\Inject annotations in the view helpers anymore as there is nothing injected when the view helpers are called statically.

Second: the first call to the view helper differs from the next calls then the template is rendered. This is why you need to duplicate some logic in the render() and in the renderStatic() methods.

Let's see what we need to do:

<?php
namespace Networkteam\Customer\ViewHelpers;

use Networkteam\Customer\Domain\Model\Document;
use Networkteam\Customer\Domain\Repository\DocumentRepository;
use Neos\FluidAdaptor\Core\ViewHelper\AbstractConditionViewHelper;
use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface

/**
 * ViewHelper to check whether the resource attached to the given document already exists in a different document
 */
class IfResourceExistsViewHelper extends AbstractConditionViewHelper {

	/**
	 * @param array $arguments
	 * @param \TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface $renderingContext
	 * @return bool
	 */
	protected static function evaluateCondition($arguments = null, \TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface $renderingContext) {
		if ($arguments['document'] === null || $arguments['document']->getResource() === null) {
			return false;
		}

		return $arguments['existingDocument'] !== null;
	}

	/**
	 * Returns true if $document has a non-unique resource
	 *
	 * @param Document $document
	 * @return string The rendered child nodes
	 */
	public function render(Document $document = null) {
		/** @var \Neos\FluidAdaptor\Core\Rendering\RenderingContext $renderingContext */
		$renderingContext = $this->renderingContext;

		$documentRepository = self::getDocumentRepository($renderingContext);
		$existingDocument = $documentRepository->findOneWithSameResource($document);
		if (self::evaluateCondition(['existingDocument' => $existingDocument, 'document' => $document], $this->renderingContext)) {
			$renderingContext->getVariableProvider()->add('existingDocument', $existingDocument);
			$content = $this->renderThenChild();
			$renderingContext->getVariableProvider()->remove('existingDocument');
			return $content;
		} else {
			return $this->renderElseChild();
		}
	}

	public static function renderStatic(array $arguments, \Closure $renderChildrenClosure, RenderingContextInterface $renderingContext)
	{
		/** @var \Neos\FluidAdaptor\Core\Rendering\RenderingContext $renderingContext */

		$documentRepository = self::getDocumentRepository($renderingContext);
		$existingDocument = $documentRepository->findOneWithSameResource($arguments['document']);
		$arguments['existingDocument'] = $existingDocument;

		if (static::evaluateCondition($arguments, $renderingContext)) {
			if (isset($arguments['then'])) {
				$renderingContext->getVariableProvider()->add('existingDocument', $existingDocument);
				$content = $arguments['then'];
				$renderingContext->getVariableProvider()->remove('existingDocument');
				return $content;
			}
			if (isset($arguments['__thenClosure'])) {
				$renderingContext->getVariableProvider()->add('existingDocument', $existingDocument);
				$content = $arguments['__thenClosure']();
				$renderingContext->getVariableProvider()->remove('existingDocument');
				return $content;
			}
		} elseif (!empty($arguments['__elseClosures'])) {
			$elseIfClosures = isset($arguments['__elseifClosures']) ? $arguments['__elseifClosures'] : [];

			return static::evaluateElseClosures($arguments['__elseClosures'], $elseIfClosures, $renderingContext);
		} elseif (array_key_exists('else', $arguments)) {
			return $arguments['else'];
		}

		return '';
	}

	/**
	 * @param \Neos\FluidAdaptor\Core\Rendering\RenderingContext $renderingContext
	 * @return DocumentRepository
	 */
	protected static function getDocumentRepository(\Neos\FluidAdaptor\Core\Rendering\RenderingContext $renderingContext)
	{
		return $renderingContext->getObjectManager()->get(DocumentRepository::class);
	}
}

The dependencies normally injected need to be fetched form the rendering context containing the objectManager, and the fetching needs to be done in a static method to be reusable, see getDocumentRepository() for an example.

Take a look at the render() method to get a feeling where to find the RenderingContext in each case.

Before the change it was easy to add a variable to the rendering context' variable container. Now there are two ways which needs to be handled. This is caused by the static call of the view helper from a template, passing the then and else cases in different manner.

Writing of view helpers has become more tricky but this is outperformed by the performance gain with this approach. Downside is that you need a detailed insight into how these view helpers work and the lack of dependency injection normally used in the framework.

9. März 2018