How to Encapsulate Complex Data Extraction Logic in Neos Fusion

07.03.2017

I've recently discovered a very useful bit of Fusion trickery which I'd like to share with you. It is a very common situation in Neos and Fusion to have a complex Node Type, which you want rendered differently at different places throughout your site. However, you want the data extraction from the node into Fusion and Fluid to always happen in the same way. Fusion provides a very nifty solution for this issue, which I would like to demonstrate.

The Starting Point: A Complex Node Type

Let's assume your Neos instance contains products, which are modelled as document node types so you can browse through them on your site. Your products have the following properties:

  • A weight. Unfortunately, it's saved in 1/100 pounds because you pulled it from the Amazon API. You want it displayed as kilograms.
  • A list price, in cents. You want to display it in Euros.
  • A sale price, which should be displayed instead of the list price everywhere, but only if it is filled. Also in cents.
  • A category, which is the product's parent node, and you need the category name for the rendering.

Of course, Fusion is the ideal place to do the necessary data extraction and transformation. On your product page, you would use the following Fusion logic to extract these properties and pass them on to the Fluid template.

// ProductPage.fusion

body {
content {
// Pull the "weight" from the node and transform it from
// 1/100th pounds into kilograms
weight = ${q(node).property('weight') * 0.00453592}

// Get the display price for the product.
// The sale price should be displayed if it exists.
displayPrice = ${q(node).property('salePrice')
? q(node).property('salePrice') / 100
: q(node).property('listPrice') / 100}

// Get the category name from the product's parent node
categoryName = ${q(node).parent().property('title')}
}
}

// ProductPage.html
<div class="content">
<ul>
<li>Weight: {content.weight}</li>
<li>Price: {content.displayPrice -> f:format.number(decimals:2)}</li>
<li>Category: {content.categoryName}</li>
</ul>
</div>

So far, so good. You corresponding Fluid template will now be able to render these properties, just adding number formatting. However, now you additionally want to render some of your products in a sidebar to direct more attention towards them. So you create a Node Type, inheriting from Neos.Neos:Content, where you simply reference your product.

Sandstorm.Example:ProductBox:
superTypes:
Neos.Neos:Content: true
ui:
label: 'Product Box'
inspector:
groups:
product:
label: Product
tab: default
properties:
product:
type: reference
ui:
label: Product
inspector:
group: product
position: 1
reloadIfChanged: true

Now, when you render this product box, of course you also want to display the correct price, weight, and category in that box. So normally, you would have to duplicate the extraction logic in the corresponding Fusion prototype.

// ProductBox.fusion
// A lot of duplication here! Isn't there a better way?

prototype(Sandstorm.Example:ProductBox) < prototype(Neos.Neos:Content) {

templatePath = 'resource://Sandstorm.Example/Private/Templates/NodeTypes/ProductBox.html'
    @context.product = ${q(node).property('product')}

// Pull the "weight" from the linked product and transform it from
// 1/100th pounds into kilograms
weight = ${q(product).property('weight') * 0.00453592}

// Get the display price for the product.
// The sale price should be displayed if it exists.
displayPrice = ${q(product).property('salePrice')
? q(product).property('salePrice') / 100
: q(product).property('listPrice') / 100}

// Get the category name from the product's parent node
categoryName = ${q(product).parent().property('title')}
}


// ProductBox.html
<div class="productbox">
<p>Weight: {weight}</p>
<p>Price: {displayPrice -> f:format.number(decimals:2)}</p>
<p>Category: {categoryName}</p>
</div>

Yes, that's some obvious duplicate code. All that changes is the node that is used, but the extraction logic stays exactly the same. Now, imagine if you had several dozen product properties that you need to render in many different templates, but needed to make sure that the extraction logic stayed the same. You could, of course, create some Fusion prototype that you'd inherit from which contains the extraction logic. However, there's another way to encapsulate complex logic in fusion.

Encapsulating Fusion Extraction Logic with Neos.Fusion:RawArray

The problem that presents itself when we try to move that logic into a prototype is that usually, Fusion prototypes are designed to "return" just one value - basically the string or template they render. What we want to do is something else, however. We need to simply extract some bits of logic into a single place, which can be re-used from wherever we need it.

The headline gave it away already: There's an exception to that "one return value only" rule, and that exception is the prototype Neos.Fusion:RawArray. Let's have a look at its PHP implementation to see what it does.

/**
* Evaluate sub objects to an array (instead of a string as ArrayImplementation does)
*/
class RawArrayImplementation extends ArrayImplementation
{
/**
* {@inheritdoc}
*
* @return array
*/
public function evaluate()
{
$sortedChildTypoScriptKeys = $this->sortNestedFusionKeys();

if (count($sortedChildTypoScriptKeys) === 0) {
return array();
}

$output = array();
foreach ($sortedChildTypoScriptKeys as $key) {
$value = $this->fusionValue($key);
if ($value === null && $this->runtime->getLastEvaluationStatus() === Runtime::EVALUATION_SKIPPED) {
continue;
}
$output[$key] = $value;
}

return $output;
}
}

This is exactly what need. The docs even say it. Instead of concatenating all its keys into a string, Neos.Fusion:RawArray simply evaluates all child keys and returns an array which we can re-use in Fusion. So let's encapsulate that extraction logic using RawArray.

// ProductDataExtractor.fusion

prototype(Sandstorm.Example:ProductDataExtractor) < prototype(Neos.Fusion:RawArray) {
weight = ${q(product).property('weight') * 0.00453592}
displayPrice = ${q(product).property('salePrice')
? q(product).property('salePrice') / 100
: q(product).property('listPrice') / 100}
categoryName = ${q(product).parent().property('title')}
}

Looks good already. Now all we need is something to provide the product in the context. This will be done from the places where we originally had the extraction logic. Our page fusion would look like this now.

// ProductPage.fusion

body {
content = Sandstorm.Example:ProductDataExtractor {
@context.product = ${node}
}
}

Easy, right? The template doesn't have to change at all (at least in this simple example - usually, your product page would maybe have a bit more content then that ;)). 

The ProductBox prototype from before would become this.  The template only needs an additional key because all product data is now in the productData path.

// ProductBox.fusion

prototype(Sandstorm.Example:ProductBox) < prototype(Neos.Neos:Content) {

templatePath = 'resource://Sandstorm.Example/Private/Templates/NodeTypes/ProductBox.html'
productData = Sandstorm.Example:ProductDataExtractor {
@context.product = ${q(node).property('product')}
}
}

// ProductBox.html
<div class="productbox">
<p>Weight: {productData.weight}</p>
<p>Price: {productData.displayPrice -> f:format.number(decimals:2)}</p>
<p>Category: {productData.categoryName}</p>
</div>

Perfect! The complex abstraction logic is encapsulated in one re-usable place, reducing code duplication and improving the consistency of variable naming in Fusion and Fluid. Our ProductBox prototypes and page rendering logic are only providing the correct product into the context and will have the correct properties set by the DataExtractor on the body.content or productData paths.

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