What's the best way to render nodes inside a Plugin (using TypoScript)?

Imagine a plugin in Neos that references or evaluates nodes that should be rendered in the view. It would be desirable to re-use the content rendering of Neos / the site package to prevent duplicate code in Fluid templates.

2 Likes

I’m listing the possible solutions (in Neos 1.2) here:

1. Use the \TYPO3\TypoScript\View\TypoScriptView in the Plugin controller

So the default view needs to be changed:

class MyPluginController extends \TYPO3\Flow\Mvc\Controller\ActionController {

    protected $defaultViewObjectName = 'TYPO3\TypoScript\View\TypoScriptView';

    protected function initializeView(\TYPO3\Flow\Mvc\View\ViewInterface $view) {
        $view->setTypoScriptPath('typoScriptPathPatterns', array('resource://@package/Private/TypoScript/MyPlugin'));
    }

    ...
}

This also needs a view option for the TypoScript include since the TYPO3.TypoScript view does the inclusion of TypoScript completely different (recursively) than the Neos TypoScript view.

Downside: This doesn’t include the Neos TypoScript, especially the auto-generated TypoScript for Node types. So even by including everything by hand (I tried that) this doesn’t solve the problem of rendering content in a convenient way. :disappointed:

2. Use the TYPO3\Neos\View\TypoScriptView in the Plugin controller

So again the default view needs to be changed:

class MyPluginController extends \TYPO3\Flow\Mvc\Controller\ActionController {

    protected $defaultViewObjectName = 'TYPO3\Neos\View\TypoScriptView';

    protected function initializeView(\TYPO3\Flow\Mvc\View\ViewInterface $view) {
        $view->setTypoScriptPath('plugins/myPlugin/' . $this->request->getControllerActionName());
    }

    ...
}

This approach sets the TypoScript path that should be rendered by the view dependent on the action name. This is needed so every action can get it’s own template.

Positive: It’s easy to render content with this approach.

Downside: There’s currently no support to pass custom variables from the controller to the view (into the TypoScript context).This makes plugins basically useless because no data can be passed from the controller to the view. :disappointed:

3. Roll your own view

So let’s combine the flexibility of passing variables to the context and re-using the Neos TypoScript in a custom TypoScript view:

class PluginTypoScriptView extends \TYPO3\Flow\Mvc\View\AbstractView {

	/**
	 * @Flow\Inject
	 * @var \TYPO3\Neos\Domain\Service\TypoScriptService
	 */
	protected $typoScriptService;

	/**
	 * @var \TYPO3\TypoScript\Core\Runtime
	 */
	protected $typoScriptRuntime;

	/**
	 * @Flow\Inject
	 * @var \TYPO3\Flow\Security\Context
	 */
	protected $securityContext;

	/**
	 * Renders the view
	 *
	 * @return string The rendered view
	 * @throws \Exception if no node is given
	 * @api
	 */
	public function render() {
		$currentNode = $this->getCurrentNode();
		$currentSiteNode = $currentNode->getContext()->getCurrentSiteNode();
		$typoScriptRuntime = $this->getTypoScriptRuntime($currentSiteNode);

		$contextVariables = array(
			'node' => $currentNode,
			'documentNode' => $this->getClosestDocumentNode($currentNode),
			'site' => $currentSiteNode,
			'account' => $this->securityContext->canBeInitialized() ? $this->securityContext->getAccount() : NULL,
			'editPreviewMode' => isset($this->variables['editPreviewMode']) ? $this->variables['editPreviewMode'] : NULL
		);
		$contextVariables = array_merge($contextVariables, $this->variables);
		$typoScriptRuntime->pushContextArray($contextVariables);
		try {
			$output = $typoScriptRuntime->render('plugins/' . $this->controllerContext->getRequest()->getControllerName() . '/' . $this->controllerContext->getRequest()->getControllerActionName());
		} catch (\TYPO3\TypoScript\Exception\RuntimeException $exception) {
			throw $exception->getPrevious();
		}
		$typoScriptRuntime->popContext();

		return $output;
	}

	/**
	 * @return NodeInterface
	 * @throws \TYPO3\Neos\Exception
	 */
	protected function getCurrentNode() {
		$currentNode = isset($this->variables['node']) ? $this->variables['node'] : NULL;
		if (!$currentNode instanceof NodeInterface) {
			throw new \TYPO3\Neos\Exception('TypoScriptView needs a variable \'node\' set with a Node object.', 1432647766);
		}
		return $currentNode;
	}

	/**
	 * @param NodeInterface $currentSiteNode
	 * @return \TYPO3\TypoScript\Core\Runtime
	 */
	protected function getTypoScriptRuntime(NodeInterface $currentSiteNode) {
		if ($this->typoScriptRuntime === NULL) {
			$this->typoScriptRuntime = $this->typoScriptService->createRuntime($currentSiteNode, $this->controllerContext);
		}
		return $this->typoScriptRuntime;
	}

	/**
	 * @param NodeInterface $node
	 * @return NodeInterface
	 */
	protected function getClosestDocumentNode(NodeInterface $node) {
		while ($node !== NULL && !$node->getNodeType()->isOfType('TYPO3.Neos:Document')) {
			$node = $node->getParent();
		}
		return $node;
	}

}

This is basically a copy of the Neos TS view without some unneeded code. To use it inside the plugin just specify the view object name in the controller:

class MyPluginController extends \TYPO3\Flow\Mvc\Controller\ActionController {

    protected $defaultViewObjectName = 'MyVendor\MyPackage\View\PluginTypoScriptView';
    ...
    public function indexAction() {
        // Access the current node (document or plugin content node) from internal arguments
        $node = $this->request->getInternalArgument('__node');
        // Do your plugin logic here
        $mySpecialValue = ...;
        $this->assign('node', $node);
        $this->assign('mySpecialValue', $mySpecialValue);
    }
}

Each action needs its own TypoScript definition, make sure to include that in your site Root.ts2:

plugins.MyPlugin.index = TYPO3.TypoScript:Template {
	templatePath = 'resource://MyVendor.MyPackage/Private/Templates/MyPlugin/Index.html'
	mySpecialValue = ${mySpecialValue}

	someContent = TYPO3.Neos:ContentCollection {
		// This expects a ContentCollection child node with path "myContent" for the current node
		nodePath = 'myContent'
	}
}

So this works as expected and allows to render both data from the plugin and content as already defined for the Neos content rendering. Let’s see if we want to have this in the Neos core. :smiley:

2 Likes

Hey Christopher,

I’d suggest that we allow this in the Neos TypoScript View. IMHO there is no reason to forbid that :smile:

Greets,
Sebastian

Yes I think so too. The only thing we need to solve then is how to have a good convention from plugin/controller/action to a TypoScript path. So either we create a new PluginTypoScriptView for that or implement it inside the Neos TypoScriptView.

And we should think about deprecating the TYPO3.TypoScript TypoScriptView because that’s totally out of sync and full of inconsistencies.

Also the most obvious way should be stated: write a FlowQuery operation and render your custom stuff in normal TS. Just for the sake of completeness.

That’s only feasible for simple dynamic requirements like filtering / sorting. I’m thinking / writing about Plugins with session support, multiple actions, etc… So more like in a shop system.

1 Like

Mh, I think for standalone Flow applications the TYPO3.TypoScript View still makes sense.

Can we somehow refactor it in a way that “common functionality” is moved to there?

All the best, Sebastian

Especially the way of parsing TS files is pretty naive (just doing it recursively given an array of paths). So we’d need to change that. The biggest difference to Neos view is that we have the TypoScriptService in Neos to create the merged, package specific and auto-generated TS (a.k.a the magic). But this could be overridable as a template method in the TYPO3.TypoScript view.

big + to sebastians comment on using the TYPO3.TypoScript view in Flow applications, I actually did some simple custom views for rendering stuff in an application and I like it very much.

I now simply used a case object with conditions on the request, like:

startPage {
	@position = 'start'
	type = 'My.Package:StartPage'
	condition = ${request.controllerName == 'Standard'}
}

Okay, but did you have problems with the TS inclusion? I think there’s definitely a use-case for the TypoScript view for pure Flow applications and I’d really like to advertise that more. So having this as a good base and extending it in Neos for the Document view (and maybe a separate Plugin view) would be the best way to go for me.