The Definitive Guide to Conditional Logic in Neos Fusion

06.03.2017

Fusion is the core of the Neos rendering layer. It’s a powerful language which makes many rendering-related tasks extremely simple. Some other things, however, require a certain degree of understanding to get right. Conditional logic is one of these things. Since I see the same kind of questions pop up again and again on Slack, there is clearly a demand for more information regarding this topic. This blog post is intended as the definite resource to clear up every last question about Fusion conditional logic. I will keep updating this with useful bits of information and different use cases regarding Fusion conditionals.

You should have a solid basic understanding of Fusion and its basic functionality, especially of how the context works and what FlowQuery does. If you don’t have that yet, please have a quick look at the Fusion documentation or at the Learn-Neos blog and its postings on TypoScript2, which is Fusion’s old name. Another very good option would be to watch the Neos Fusion Rendering Deep Dive video by Sebastian Kurfürst, which explains the Fusion layer in detail.

Language Constructs and Prototypes for Conditional Logic in Fusion

Fusion provides functionality for different kinds of conditional logic. Some of this functionality is implemented as a first-class language construct, other parts are simply Fusion objects that are shipped with the language itself.

Option 1: The Eel Ternary Operator

The first and most simple form of conditional logic in Neos Fusion is the ternary operator, which is, in reality, a part of Eel which can be used in Fusion. This is its syntax:

// MyExample.fusion someFusionPath = ${condition ? 'thenStatement' : 'elseStatement'}

There’s really not all that much to say about this one. It’s intended for very simple cases where you want something to evaluate to a different result depending of a condition, usually based on the context. The result can be stored in a fusion path, as shown above, or in another context variable:

// MyExample.fusion @context.someVariable = ${q(node).property('foo') ? 'bar' : 'baz'}

In the example shown above, ${someVariable} would have the value "bar" if the property of the current context node evaluated to true, and "baz" if it evaluated to false. Of course, the ternary operator can be nested to allow for more complex conditions.

// MyExample.fusion someFusionPath = ${condition ? 'thenStatement' : (otherCondition ? 'then2' : 'else2')}

You can go to as many different levels of nesting the ternary operator as you like. However, more than 2 levels of nesting quickly become a hassle to maintain. In these cases, using the Case or ContentCase Fusion objects is the better way to go.

Option 2: The Case Object

Case is Fusion’s way to implement if or switch constructs known from other languages. We’ll have a very close look at Case and how it works, since it is the most important conditional in Fusion. It is implemented as a Fusion prototype itself, which seems a bit weird at first („Why is there no elemental language construct for if / switch?“). However, once you have a closer look, it starts to make a lot of sense because it directly leverages the way Fusion works. I will show you an example of the full Case syntax first. Then we will go into detail about its parts and finally we’ll have a look at some examples. Here’s a Case object that implements a typical if / elseif / else condition in Fusion.

// MyExample.fusion myPath = Neos.Fusion:Case { if { condition = ${q(node).property('something')} renderer = 'myPath will have this value if the nodes „something“ property evaluates to true.' } elseif { condition = ${foo == 'bar'} renderPath = '/some/fusion/path' } else { condition = TRUE type = 'Sandstorm.Website:Content.Text' } }

You can see that within the Case object, we’ve added three Fusion paths that need to have certain sub-paths themselves. Please note that their naming (if, else and elseif) here is completely arbitrary - they could be named foo, bar and baz just as well. These sub-paths are called Matchers, and there is actually a Fusion implementation of that name to evaluate these paths. This is the reason why a Matcher object needs to have certain sub-paths itself to work. A matcher needs either a "renderer", a "renderPath" or a  "type" property. We’ll have a closer look at these paths and what exactly they do further down in this section, but first let’s check out the actual implementation of the Case object, in order to understand what’s going on at that level. The Fusion prototype of Case is extremely simple - it consists of just one line.

// Root.fusion (in Neos.Fusion package) prototype(Neos.Fusion:Case).@class = 'Neos\\Fusion\\FusionObjects\\CaseImplementation'

That’s it - there’s really not more to it. The Case prototype is nothing but a wrapper around a PHP class. Its evaluate() method, which is called by Fusion, iterates over all sub-paths from top to bottom (after sorting them with the positional array syntax, which is documented here), and evaluates them until it finds one that matches.

public function evaluate() { $matcherKeys = $this->sortNestedFusionKeys(); foreach ($matcherKeys as $matcherName) { $renderedMatcher = $this->renderMatcher($matcherName); if ($this->matcherMatched($renderedMatcher)) { return $renderedMatcher; } } return null; }

For each Fusion property of the Case object, the renderMatcher() function is called. We’ll have a look at what this function does as well. I removed some error checking code below for more reading clarity.

protected function renderMatcher($matcherKey) { $renderedMatcher = null; if (isset($this->properties[$matcherKey]['__objectType'])) { // object type already set, so no need to set it return $this->runtime->render( sprintf('%s/%s', $this->path, $matcherKey) ); } else { // No object type has been set, so we're using Neos.Fusion:Matcher as fallback return $this->runtime->render( sprintf('%s/%s<Neos.Fusion:Matcher>', $this->path, $matcherKey) ); } }

The Case object, by default, evaluates your sub-paths as objects of type Neos.Fusion:Matcher - unless you give them another object type, in which case Fusion will use that type to evaluate your sub-path. If the Case object would not have that fallback, my Case example from above would need to look like this.

myPath = Neos.Fusion:Case { if = Neos.Fusion:Matcher { condition = ${q(node).property('something')} renderer = 'myPath will have this value if the nodes „something“ property evaluates to true.' } elseif = Neos.Fusion:Matcher { condition = ${foo == 'bar'} renderPath = '/some/fusion/path' } else = Neos.Fusion:Matcher { condition = TRUE type = 'Sandstorm.Website:Content.Text' } }

Before I show you an example of that, let’s have a look at the last missing part of the Case implementation, which is the matcherMatched() function. Again, some error checking / debugging code was removed for clarity.

protected function matcherMatched($renderedMatcher) { return $renderedMatcher !== self::MATCH_NORESULT; }

Unless a Matcher returns the class constant MATCH_NORESULT, it will return true and therefore its renderer, renderPath or type will be evaluated as the result of the entire Case object. This is all a Case object does - it iterates over its sub-paths, checks if they evaluate to something other than CaseImplementation::MATCH_NORESULT, and, if so, evaluates them. We’ll now investigate how the default Fusion Matcher works to understand how to use it correctly.

prototype(Neos.Fusion:Matcher).@class = 'Neos\\Fusion\\FusionObjects\\MatcherImplementation'

And here’s the corresponding Matcher implementation’s evaluate() method:

public function evaluate() { if ($this->getCondition()) { return parent::evaluate(); } else { return CaseImplementation::MATCH_NORESULT; } }

The getCondition() method evaluates the "condition" path of our matcher. If it is false, MATCH_NORESULT is returned and therefore the Case object will try the next matcher. If it is true however, the Matcher implementation calls its parent’s evaluate() method. The Neos\Fusion\FusionObjects\MatcherImplementation PHP class inherits from Neos\Fusion\FusionObjects\RendererImplementation, and this class is responsible for making sense of the "renderer", "renderPath" and "type" sub-paths you saw earlier in my case example. Here is its evaluate() method.

public function evaluate() { $rendererPath = sprintf('%s/renderer', $this->path); $canRenderWithRenderer = $this->runtime->canRender($rendererPath); $renderPath = $this->getRenderPath(); if ($canRenderWithRenderer) { $renderedElement = $this->runtime->evaluate($rendererPath, $this); } elseif ($renderPath !== null) { if (substr($renderPath, 0, 1) === '/') { $renderedElement = $this->runtime->render(substr($renderPath, 1)); } else { $renderedElement = $this->runtime->render($this->path . '/' . str_replace('.', '/', $renderPath)); } } else { $renderedElement = $this->runtime->render( sprintf('%s/element<%s>', $this->path, $this->getType()) ); } return $renderedElement; }

In plain English, the RendererImplementation does the following:

  • If "renderer" contains something that Fusion can render (which means it exists and has a Fusion implementation or is a simple value), evaluate and return it.
  • Else, if "renderPath" exists, render the Fusion object or value at that - absolute or relative - Fusion path.
  • If none of the above worked, render the Matcher’s "element" sub-path, using the Fusion prototype given in "type".

This means that you could, theoretically, use "renderer", "renderPath" and "type" in the same matcher. They would be evaluated in that order until one worked. In practice however, you’ll usually want to do only one of these things. To make it very clear what the three sub-paths a Matcher (or Renderer) object can have actually do, let’s go through them one by one again, using my example from above.

Matcher Option A: The "renderer" property

This first matcher uses the "renderer" sub-path to return a result.

if { condition = ${q(node).property('something')} renderer = 'myPath will have this value if the nodes „something“ property evaluates to true.' }

The naming here is a bit confusing since the Matcher itself inherits from a RendererImplementation, and I believe that is the single biggest source of confusion regarding conditional logic in Fusion. "renderer" basically means that this path should be rendered if the related condition matches. It doesn’t have any magic functionality - it only renders a path when the condition that goes with it matches. Simply replace "renderer" with "then" in your head and you’re fine.

Matcher Option B: The "renderPath" property

If you use the "renderPath" option, Fusion will render the object at a certain Fusion path if the condition matches.

elseif { condition = ${foo == 'bar'} renderPath = '/some/fusion/path' }

This is useful if you have an object that you want to re-render in multiple places without defining it again. If your path starts with "/" as in my example, Fusion will treat it as absolute path. If it doesn’t start with "/", Fusion will interpret it as relative to the Matcher itself. The following example  would render "baz", since the condition evaluates to true.

elseif { condition = TRUE renderPath = 'myPath' myPath = 'baz' }

ATTENTION:

If you use "renderPath", you should be aware that you lose all Fusion hierarchy and context for the object at the target path. This will have implications e.g. if you re-defined a prototype nested within another prototype. If you use renderPath, this will no longer work as expected, since we dispatch a completely standalone Fusion rendering task that knows nothing about the Matcher's context or node hierarchy. The only place where Neos itself uses renderPath is the root Matcher. Extracting the Fusion object at the target path into its own prototype and rendering that (via "type") is usually the safer bet.

Matcher Option C: The "type" property

Using the "type" path, you can render a Fusion prototype at the respective path dynamically, just by providing its name as a string to the "type" property. The power of this will become clear when we look at the ContentCase prototype that ships with Neos.

else { condition = TRUE type = 'Sandstorm.Website:Content.Text' element = ${node} }

Understanding the Neos Fusion ContentCase Prototype

Since you now know how all of this works, understanding what ContentCase is for should be child’s play. Let’s have a look at the prototype.

prototype(Neos.Neos:ContentCase) < prototype(Neos.Fusion:Case) { default { @position = 'end' condition = TRUE type = ${q(node).property('_nodeType.name')} } }

ContentCase adds a default fallback at the end of the condition chain (using positional array sorting), which automatically renders a Fusion prototype with the same name as the Node Type of the current node when no other condition matches. This means that, when you use ContentCase, Fusion will automagically know which Node Type you want to render, and select the appropriate Fusion prototype for you.

Surely you’ve heard before that Fusion prototypes should have the same name as the Node Type they render, and this is the reason for it. There is no other place where this convention is enforced except here. The default Neos ContentCollection uses ContentCase as its item renderer (have a look at ContentCollection.fusion), which shows how central this concept is to Neos. I will go into deeper detail on how to use the Case object in the Use Cases section of this blog post, but before we do that, let’s shine some light on the third option for conditional logic in Fusion: the @if meta-property.

Option 3: The @if meta-property

@if, like @context, @process, @position, @ignoreProperties, @exceptionHandler or @cache, is a so-called Fusion meta-property. Meta-properties do not set paths or attributes on Fusion objects, but influence their processing by the Fusion runtime in some way. The @if meta-property can be added to any Fusion path to indicate that its rendering should be dependent on some condition. Here’s an example.

myPath = 'foo' myPath.@if.cond1 = ${q(node).property('bar') == 'baz'}

In this code snippet, „myPath“ would only be evaluated if the current context node had the property "bar" with a value of "baz". Please note that, just like with the Case object, the name of the conditions (cond1 in my example) does not matter, but only serves as a key. @if allows you to add multiple conditions. All conditions need to return true in order for the path to be rendered:

myPath = 'foo' myPath.@if.cond1 = ${q(node).property('bar') == 'baz'} myPath.@if.anotherCondition = ${site.name == 'MyCoolSite'}

@if, however, can not be used to construct a if/else situation on a Fusion path - you would need to use the Case object for that.

// THIS CODE DOES NOT WORK! myPath = 'value1' myPath.@if.1 = ${node.name == 'condition1'} myPath = 'value2' myPath.@if.2 = ${node.name == 'condition2'}

The code above does not work because you cannot assign multiple values to a single path, even if you add a rendering condition to it. @if can only be used to prevent or allow the evaluation of a certain node path depending on a property, but cannot be used to influence its output. It’s basically just an on/off switch for a Fusion path.

Summary

This concludes the "theoretical" part on conditional logic in Neos Fusion. We’ve looked at the simple ternary operator, the very flexible and powerful Fusion Case prototype, and the @if meta-property. All of them have specific use-cases which can sometimes overlap slightly. I hope that by now, you have a good overview about the different possibilities. I’lll now show a variety of examples and use cases which should help further illuminate the various things you can do with conditional logic in Fusion.

Fusion Conditional Logic Use Cases and Examples

This section contains several examples for common conditional logic use cases in Neos Fusion. I will keep adding things I find relevant and/or helpful.

Example 1: Implementing a Switch Statement as Syntactic Sugar in Fusion

Remember how I said earlier that using Fusion itself to implement the conditional logic seems weird, but is actually really clever? Here’s one of the reasons why I said that. One of the credos of Neos is „Everything in place, everything replaceable“. This also applies to the implementation of the matcher object. Since the Case implementation checks if there’s a Fusion type set before using Matcher as a fallback, you can implement your own Matchers which work completely different from the default ones, and you can even combine the two.

Armed with this information, we can now go about implementing our own Case and Matcher prototypes - simply to show you what’s possible, and to gain a deeper understanding of how the Case object works. Here is the target API we want to create.

page = Sandstorm.Example:Switch { @context.switch = 'FOO' case1 { case = 'BAR' then = 'The "case1" matcher matched.' } case2 { case = 'FOO' then = 'The "case2" matcher matched.' } default { case = ${switch} then = 'This default case matched.' } }

We're aiming for a typical switch statement which declares its switch variable at the top and then tries to match each case against that switch variable. We would expect the "page" path to render the last case matcher. We're setting the @context.switch variable manually here; in practice you would probably use something that already exists under another name in the context, such as a node property.

The first thing we need is our own prototype for the Switch object. We will make it just as simple as the default one. Also, because we're going to need it later, we will also define the prototype for the SwitchMatcher objects.

prototype(Sandstorm.Example:Switch).@class = 'Sandstorm\\Example\\FusionObjects\\SwitchImplementation' prototype(Sandstorm.Example:SwitchMatcher).@class = 'Sandstorm\\Example\\FusionObjects\\SwitchMatcherImplementation'

As a next step, we create our implementation of the Switch prototype. Here's the full code with explanatory comments.

/** * Switch Fusion Object * * Just like "Case", the "Switch" object evaluates its children as Matchers until * one matches. However, the "Switch" object expects a valid fusion expression in the * "switch" context. It will match all "case" properties of its children against * the resulting value. */ class SwitchImplementation extends CaseImplementation { /** * Render the given matcher * * A result value of MATCH_NORESULT means that the condition of the matcher did not match and the case should * continue. * * @param string $matcherKey * @return string * @throws UnsupportedObjectTypeAtPathException */ protected function renderMatcher($matcherKey) { $renderedMatcher = null; if (isset($this->properties[$matcherKey]['__objectType'])) { // object type already set, so no need to set it $renderedMatcher = $this->runtime->render( sprintf('%s/%s', $this->path, $matcherKey) ); return $renderedMatcher; } elseif (!is_array($this->properties[$matcherKey])) { throw new UnsupportedObjectTypeAtPathException('"Case" Fusion object only supports nested Fusion objects; no simple values.', 1372668062); } elseif (isset($this->properties[$matcherKey]['__eelExpression'])) { throw new UnsupportedObjectTypeAtPathException('"Case" Fusion object only supports nested Fusion objects; no Eel expressions.', 1372668077); } else { // No object type has been set, so we're using Sandstorm.Example:SwitchMatcher as fallback $renderedMatcher = $this->runtime->render( sprintf('%s/%s<Sandstorm.Example:SwitchMatcher>', $this->path, $matcherKey) ); return $renderedMatcher; } } }

As you can see, we're inheriting from the Case object and only override the renderMatcher() function to replace the default Matcher prototype with our own SwitchMatcher implementation. For each property, we render a SwitchMatcher. Let's look at its implementation as well.

/** * SwitchMatcher object for use inside a "Switch" object */ class SwitchMatcherImplementation extends AbstractFusionObject { /** * @return boolean */ public function getCondition() { // Here, we pull the switch value from the rendering context and execute the comparison. return $this->fusionValue('case') === $this->getRuntime()->getCurrentContext()['switch']; } /** * If the switch condition matches, render the then property and return it. Else, return MATCH_NORESULT. * * @return mixed */ public function evaluate() { if ($this->getCondition()) { $thenPath = sprintf('%s/then', $this->path); return $this->runtime->evaluate($thenPath, $this); } else { return CaseImplementation::MATCH_NORESULT; } } }

As you can see, it's a good deal less complicated than the default MatcherImplementation, because we don't need to inherit from renderer. We only have one sub-property of the SwitchMatcher which gets evaluated, which is the "then" path. So, we simply inherit from AbstractFusionObject and implement the getCondition() method so that it compares the value in the "case" path of every SwitchMatcher with the switch value taken from the context. If it matches, we evaluate the "then" path and return the result. That's it! We can now use our Switch object as in the following example. Its output will be "The "case2" matcher matched.".

page = Sandstorm.Example:Switch { @context.switch = 'FOO' case1 { case = 'BAR' then = 'The "case1" matcher matched.' } case2 { case = 'FOO' then = 'The "case2" matcher matched.' } default { case = ${switch} then = 'The "default" matcher matched.' } }

Example 2: How to Switch a Template Path Depending on a Fusion Variable

Let's assume you are running a multi-site environment, and you want to render logically similar, but different-looking landing pages for each site. You might want to switch the body templatePath in Fusion depending on the site's name. Here's how you could achieve that using the Fusion Case object.

body { templatePath = Neos.Fusion:Case { site1 { condition = ${site.name == 'site1'} renderer = 'resource://Your.SitePackage/Private/Templates/Page/TemplateForSite1.html' } site2 { condition = ${site.name == 'site2'} renderer = 'resource://Your.SitePackage/Private/Templates/Page/TemplateForSite2.html' } fallback { condition = TRUE renderer = 'resource://Your.SitePackage/Private/Templates/Page/DefaultFallback.html' } } }

Example 3: How to Evaluate a Fusion Path Depending on the Current Rendering Context

Sometimes you want to evaluate a Fusion path only if the user is in the Neos backend (or frontend, or preview, or whatever rendering context you need). This is a perfect example for the @if meta-property, which is actually used by Neos itself in the ContentCollection prototype to show deleted objects only in the backend. Here's how to do it.

// The "myPath" path contains the content you want to render in the backend only myPath = 'This string will only be rendered in the Neos backend.' myPath.@if.onlyInBackend = ${node.context.inBackend}

Example 4: How to Evaluate a Fusion Processor Conditionally

Interestingly, @if also works with other meta-properties such as @process. You can conditionally apply a Fusion processor like this.

myPath = 'This string will be wrapped in a div element in the Neos backend.' myPath.@process.wrap = ${'<div class="backend-wrapper">' + value + '</div>'} myPath.@process.wrap.@if.onlyInBackend = ${node.context.inBackend}

Dein Besuch auf unserer Website produziert laut der Messung auf websitecarbon.com nur 0,28 g CO₂.