Inside Fusion
In this chapter, Fusion will be explained in a step-by-step fashion, focusing on the different internal parts, the syntax of these and the semantics.
Fusion is fundamentally a hierarchical, prototype based processing language:
It is hierarchical because the content it should render is also hierarchically structured.
It is prototype based because it allows to define properties for all instances of a certain Fusion object type. It is also possible to define properties not for all instances, but only for instances inside a certain hierarchy. Thus, the prototype definitions are hierarchically-scoped as well.
It is a processing language because it processes the values in the context into a single output value.
In the first part of this chapter, the syntactic and semantic features of the Fusion, Eel and FlowQuery languages are explained. Then, the focus will be on the design decisions and goals of Fusion, to provide a better understanding of the main objectives while designing the language.
Goals of Fusion
Fusion should cater to both planned and unplanned extensibility. This means it should provide ways to adjust and extend its behavior in places where this is to be expected. At the same time it should also be possible to adjust and extend in any other place without having to apply dirty hacks.
Fusion should be usable in standalone, extensible applications outside of Neos. The use of a flexible language for configuration of (rendering) behavior is beneficial for most complex applications.
Fusion should make out-of-band rendering easy to do. This should ease content generation for technologies like AJAX or edge-side includes (ESI).
Fusion should make multiple renderings of the same content possible. It should allow placement of the same content (but possibly in different representations) on the same page multiple times.
Fusion’s syntax should be familiar to the user, so that existing knowledge can be leveraged. To achieve this, Fusion takes inspiration from CSS selectors, jQuery and other technologies that are in widespread use in modern frontend development.
Fusion files
Fusion is read from files. In the context of Neos, some of these files are loaded automatically, and Fusion files can be split into parts to organize things as needed.
Automatic Fusion file inclusion
All Fusion files are expected to be in the package subfolder Resources/Private/Fusion. Neos will automatically include the file Root.fusion for the current site package (package which resides in Packages/Sites and has the type “neos-site” in its composer manifest).
To automatically include Root.fusion files from other packages, you will need to add those packages to
the configuration setting Neos.Neos.fusion.autoInclude
:
# Settings.yaml
Neos:
Neos:
fusion:
autoInclude:
Your.Package: true
Neos will then autoinclude Root.fusion files from these packages in the order defined by package management. Files with a name other than Root.fusion will never be auto-included even with that setting. You will need to include them manually in your Root.fusion.
Manual Fusion file inclusion
In any Fusion file further files can be included using the include
statement. The path is either
relative to the current file or can be given with the resource
wrapper:
include: NodeTypes/CustomElements.fusion
include: resource://Acme.Demo/Private/Fusion/Quux.fusion
In addition to giving exact filenames, globbing is possible in two variants:
# Include all .fusion files in NodeTypes
include: NodeTypes/*
# Include all .fusion files in NodeTypes and it's subfolders recursively
include: NodeTypes/**/*
The first includes all Fusion files in the NodeTypes folder, the latter will recursively include all Fusion files in NodeTypes and any folders below.
The globbing can be combined with the resource
wrapper:
include: resource://Acme.Demo/Private/Fusion/NodeTypes/*
include: resource://Acme.Demo/Private/Fusion/**/*
Fusion Objects
Fusion is a language to describe Fusion objects. A Fusion object has some properties which are used to configure the object. Additionally, a Fusion object has access to a context, which is a list of variables. The goal of a Fusion object is to take the variables from the context, and transform them to the desired output, using its properties for configuration as needed.
Thus, Fusion objects take some input which is given through the context and the properties, and produce a single output value. Internally, they can modify the context, and trigger rendering of nested Fusion objects: This way, a big task (like rendering a whole web page) can be split into many smaller tasks (render a single image, render some text, …): The results of the small tasks are then put together again, forming the final end result.
Fusion object nesting is a fundamental principle of Fusion. As Fusion objects call nested Fusion objects, the rendering process forms a tree of Fusion objects.
Fusion objects are implemented by a PHP class, which is instantiated at runtime. A single PHP class is the basis for many Fusion objects. We will highlight the exact connection between Fusion objects and their PHP implementations later.
A Fusion object can be instantiated by assigning it to a Fusion path, such as:
foo = Page
# or:
my.object = Text
# or:
my.image = Neos.Neos.ContentTypes:Image
The name of the to-be-instantiated Fusion prototype is listed without quotes.
By convention, Fusion paths (such as my.object
) are written in lowerCamelCase
, while
Fusion prototypes (such as Neos.Neos.ContentTypes:Image
) are written in UpperCamelCase
.
It is possible to set properties on the newly created Fusion objects:
foo.myProperty1 = 'Some Property which Page can access'
my.object.myProperty1 = "Some other property"
my.image.width = ${q(node).property('foo')}
Property values that are strings have to be quoted (with either single or double quotes). A property can also be an Eel expression (which are explained in Eel, FlowQuery and Fizzle.)
To reduce typing overhead, curly braces can be used to “abbreviate” long Fusion paths:
my {
image = Image
image.width = 200
object {
myProperty1 = 'some property'
}
}
Instantiating a Fusion object and setting properties on it in a single pass is also possible. All three examples mean exactly the same:
someImage = Image
someImage.foo = 'bar'
# Instantiate object, set property one after each other
someImage = Image
someImage {
foo = 'bar'
}
# Instantiate an object and set properties directly
someImage = Image {
foo = 'bar'
}
Fusion Objects are Side-Effect Free
When Fusion objects are rendered, they are allowed to modify the Fusion context (they can add or override variables); and can invoke other Fusion objects. After rendering, however, the parent Fusion object must make sure to clean up the context, so that it contains exactly the state it had before the rendering.
The API helps to enforce this, as the Fusion context is a stack: The only thing the developer of a Fusion object needs to make sure is that if he adds some variable to the stack, effectively creating a new stack frame, he needs to remove exactly this stack frame after rendering again.
This means that a Fusion object can only manipulate Fusion objects below it, but not following or preceding it.
In order to enforce this, Fusion objects are furthermore only allowed to communicate through the Fusion Context; and they are never allowed to be invoked directly: Instead, all invocations need to be done through the Fusion Runtime.
All these constraints make sure that a Fusion object is side-effect free, leading to an important benefit: If somebody knows the exact path towards a Fusion object together with its context, it can be rendered in a stand-alone manner, exactly as if it was embedded in a bigger element. This enables, for example, rendering parts of pages with different cache life- times, or the effective implementation of AJAX or ESI handlers reloading only parts of a website.
Fusion Prototypes
When a Fusion object is instantiated (i.e. when you type someImage = Image
) the
Fusion Prototype for this object is copied and is used as a basis for the new object.
The prototype is defined using the following syntax:
prototype(MyImage) {
width = '500px'
height = '600px'
}
When the above prototype is instantiated, the instantiated object will have all the properties of the copied prototype. This is illustrated through the following example:
someImage = MyImage
# now, someImage will have a width of 500px and a height of 600px
someImage.width = '100px'
# now, we have overridden the height of "someImage" to be 100px.
Prototype- vs. class-based languages
There are generally two major “flavours” of object-oriented languages. Most languages (such as PHP, Ruby, Perl, Java, C++) are class-based, meaning that they explicitly distinguish between the place where behavior for a given object is defined (the “class”) and the runtime representation which contains the data (the “instance”).
Other languages such as JavaScript are prototype-based, meaning that there is no distinction between classes and instances: At object creation time, all properties and methods of the object’s prototype (which roughly corresponds to a “class”) are copied (or otherwise referenced) to the instance.
Fusion is a prototype-based language because it copies the Fusion Prototype to the instance when an object is evaluated.
Prototypes in Fusion are mutable, which means that they can easily be modified:
prototype(MyYouTube) {
width = '100px'
height = '500px'
}
# you can change the width/height
prototype(MyYouTube).width = '400px'
# or define new properties:
prototype(MyYouTube).showFullScreen = ${true}
Defining and instantiating a prototype from scratch is not the only way to define and
instantiate them. You can also use an existing Fusion prototype as basis
for a new one when needed. This can be done by inheriting from a Fusion prototype
using the <
operator:
prototype(MyImage) < prototype(Neos.Neos:Content)
# now, the MyImage prototype contains all properties of the Template
# prototype, and can be further customized.
This implements prototype inheritance, meaning that the “subclass” (MyImage
in the example
above) and the “parent class (Content
) are still attached to each other: If a property
is added to the parent class, this also applies to the subclass, as in the following example:
prototype(Neos.Neos:Content).fruit = 'apple'
prototype(Neos.Neos:Content).meal = 'dinner'
prototype(MyImage) < prototype(Neos.Neos:Content)
# now, MyImage also has the properties "fruit = apple" and "meal = dinner"
prototype(Neos.Neos:Content).fruit = 'Banana'
# because MyImage *extends* Content, MyImage.fruit equals 'Banana' as well.
prototype(MyImage).meal = 'breakfast'
prototype(Neos.Fusion:Content).meal = 'supper'
# because MyImage now has an *overridden* property "meal", the change of
# the parent class' property is not reflected in the MyImage class
Prototype inheritance can only be defined globally, i.e. with a statement of the following form:
prototype(Foo) < prototype(Bar)
It is not allowed to nest prototypes when defining prototype inheritance, so the following examples are not valid Fusion and will result in an exception:
prototype(Foo) < some.prototype(Bar)
other.prototype(Foo) < prototype(Bar)
prototype(Foo).prototype(Bar) < prototype(Baz)
While it would be theoretically possible to support this, we have chosen not to do so in order to reduce complexity and to keep the rendering process more understandable. We have not yet seen a Fusion example where a construct such as the above would be needed.
Hierarchical Fusion Prototypes
One way to flexibly adjust the rendering of a Fusion object is done through
modifying its Prototype in certain parts of the rendering tree. This is possible
because Fusion prototypes are hierarchical, meaning that prototype(...)
can be part of any Fusion path in an assignment; even multiple times:
prototype(Foo).bar = 'baz'
prototype(Foo).some.thing = 'baz2'
some.path.prototype(Foo).some = 'baz2'
prototype(Foo).prototype(Bar).some = 'baz2'
prototype(Foo).left.prototype(Bar).some = 'baz2'
prototype(Foo).bar
is a simple, top-level prototype property assignment. It means: For all objects of type Foo, set property bar. The second example is another variant of this pattern, just with more nesting levels inside the property assignment.some.path.prototype(Foo).some
is a prototype property assignment inside some.path. It means: For all objects of type Foo which occur inside the Fusion path some.path, the property some is set.prototype(Foo).prototype(Bar).some
is a prototype property assignment inside another prototype. It means: For all objects of type Bar which occur somewhere inside an object of type Foo, the property some is set.This can both be combined, as in the last example inside
prototype(Foo).left.prototype(Bar).some
.
Internals of hierarchical prototypes
A Fusion object is side-effect free, which means that it can be rendered deterministically knowing only its Fusion path and the context. In order to make this work with hierarchical prototypes, we need to encode the types of all Fusion objects above the current one into the current path. This is done using angular brackets:
a1/a2<Foo>/a3/a4<Bar>
When this path is rendered, a1/a2
is rendered as a Fusion object of type Foo
– which is needed
to apply the prototype inheritance rules correctly.
Those paths are rarely visible on the “outside” of the rendering process, but might at times appear in exception messages if rendering fails. For those cases it is helpful to know their semantics.
Bottom line: It is not important to know exactly how the a rendering Fusion object’s Fusion path is constructed. Just pass it on, without modification to render a single element out of band.
Namespaces of Fusion objects
The benefits of namespacing apply just as well to Fusion objects as they apply to other languages. Namespacing helps to organize the code and avoid name clashes.
In Fusion the namespace of a prototype is given when the prototype is declared. The
following declares a YouTube
prototype in the Acme.Demo
namespace:
prototype(Acme.Demo:YouTube) {
width = '100px'
height = '500px'
}
The namespace is, by convention, the package key of the package in which the Fusion resides.
Fully qualified identifiers can be used everywhere an identifier is used:
prototype(Neos.Neos:ContentCollection) < prototype(Neos.Neos:Collection)
In Fusion a default
namespace of Neos.Fusion
is set. So whenever Value
is used in
Fusion, it is a shortcut for Neos.Fusion:Value
.
Custom namespace aliases can be defined using the following syntax:
namespace: Foo = Acme.Demo
# the following two lines are equivalent now
video = Acme.Demo:YouTube
video = Foo:YouTube
Warning
These declarations are scoped to the file they are in and have to be declared in every fusion file where they shall be used.
Setting Properties On a Fusion Object
Although the Fusion object can read its context directly, it is good practice to instead use properties for configuration:
# imagine there is a property "foo=bar" inside the Fusion context at this point
myObject = MyObject
# explicitly take the "foo" variable's value from the context and pass it into the "foo"
# property of myObject. This way, the flow of data is more visible.
myObject.foo = ${foo}
While myObject
could rely on the assumption that there is a foo
variable inside the Fusion
context, it has no way (besides written documentation) to communicate this to the outside world.
Therefore, a Fusion object’s implementation should only use properties of itself to determine its output, and be independent of what is stored in the context.
However, in the prototype of a Fusion object it is perfectly legal to store the mapping between the context variables and Fusion properties, such as in the following example:
# this way, an explicit default mapping between a context variable and a property of the
# Fusion object is created.
prototype(MyObject).foo = ${foo}
To sum it up: When implementing a Fusion object, it should not access its context variables directly, but instead use a property. In the Fusion object’s prototype, a default mapping between a context variable and the prototype can be set up.
Default Context Variables
Neos exposes some default variables to the Fusion context that can be used to control page rendering in a more granular way.
node
can be used to get access to the current node in the node tree and read its properties. It is of typeNodeInterface
and can be used to work with node data, such as:# Make the node available in the template node = ${node} # Expose the "backgroundImage" property to the rendering using FlowQuery backgroundImage = ${q(node).property('backgroundImage')}
To see what data is available on the node, you can expose it to the template as above and wrap it in a debug view helper:
{node -> f:debug()}
documentNode
contains the closest parent document node - broadly speaking, it is the page the current node is on. Just likenode
, it is aNodeInterface
and can be provided to the rendering in the same way:# Expose the document node to the template documentNode = ${documentNode} # Display the document node path nodePath = ${documentNode.path}
documentNode
is in the end just a shorthand to get the current document node faster. It could be replaced with:# Expose the document node to the template using FlowQuery and a Fizzle operator documentNode = ${q(node).closest('[instanceof Neos.Neos:Document]').get(0)}
request
is an instance ofNeos\Flow\Mvc\ActionRequest
and allows you to access the current request from within Fusion. Use it to provide request variables to the template:# This would provide the value sent by an input field with name="username". userName = ${request.arguments.username} # request.format contains the format string of the request, such as "html" or "json" requestFormat = ${request.format}
Another use case is to trigger an action, e.g. a search, via a custom Eel helper:
searchResults = ${Search.query(site).fulltext(request.arguments.searchword).execute()}
A word of caution: You should never trigger write operations from Fusion, since it can be called multiple times (or not at all, because of caching) during a single page render. If you want a request to trigger a persistent change on your site, it’s better to use a Plugin.
Manipulating the Fusion Context
The Fusion context can be manipulated directly through the use of the @context
meta-property:
myObject = MyObject
myObject.@context.bar = ${foo * 2}
In the above example, there is now an additional context variable bar
with twice the value
of foo
.
This functionality is especially helpful if there are strong conventions regarding the Fusion context variables. This is often the case in standalone Fusion applications, but for Neos, this functionality is hardly ever used.
Warning
In order to prevent unwanted side effects, it is not possible to access context variables from within @context
on the same level. This means that the following will never return the string Hello World!
@context.contextOne = ‘World!’ @context.contextTwo = ${‘Hello ‘ + contextOne} output = ${contextTwo}
Processors
Processors allow the manipulation of values in Fusion properties. A processor is applied to
a property using the @process
meta-property:
myObject = MyObject {
property = 'some value'
property.@process.1 = ${'before ' + value + ' after'}
}
# results in 'before some value after'
Multiple processors can be used, their execution order is defined by the numeric position given
in the Fusion after @process
. In the example above a @process.2
would run on the results of @process.1
.
Additionally, an extended syntax can be used as well:
myObject = MyObject {
property = 'some value'
property.@process.someWrap {
expression = ${'before ' + value + ' after'}
@position = 'start'
}
}
This allows to use string keys for the processor name, and support @position
arguments as explained for Arrays.
Processors are Eel Expressions or Fusion objects operating on the value
property of the context. Additionally,
they can access the current Fusion object they are operating on as this
.
Conditions
Conditions can be added to all values to prevent evaluation of the value. A condition is applied to
a property using the @if
meta-property:
myObject = Menu {
@if.1 = ${q(node).property('showMenu') == true}
}
# results in the menu object only being evaluated if the node's showMenu property is not ``false``
# the php rules for mapping values to boolean are used internally so following values are
# considered beeing false: ``null, false, '', 0, []``
Multiple conditions can be used, and if one of them doesn’t return true
the condition stops evaluation.
Apply
@apply
allows to override multiple properties of a fusion-prototype with a single expression. This is useful
when complex data structures are mapped to fusion prototypes.
The example shows the rendering of a teaserList
-array by using a Teaser-Component and passing all keys from each
teaser to the fusion Object:
teasers = Neos.Fusion:Collection {
collection = ${teaserList}
itemName = 'teaser'
itemRenderer = Vendor.Site:Teaser {
@apply.teaser = ${teaser}
}
}
The code avoids passing down each fusion-property explicitly to the child component. A similar concept with different syntax from the JavaScript world is known as ES6-Spreads.
Another use-case is to use Neos.Fusion:Renderer
to render a prototype while type and properties are based on data
from the context:
example = Neos.Fusion:Renderer {
type = ${data.type}
element.@apply.properties = ${data.properties}
}
That way some meta-programming can used in fusion and both prototype and properties are decided late in the rendering by the fusion runtime.
How it works
The keys below @apply
are evaluated before the fusion-object and the @context
and are initialized.
Each key below @apply
must return a key-value map (values other than an array it are ignored). During
the evaluation of each fusion-path the values from @apply
are always checked first.
If a property is defined via @apply
this value is returned without evaluating the fusionPath.
The @process
and @if
-rules of the original fusion-key are still applied even if a value from @apply
is returned.
Since @apply
is evaluated first the overwritten values are already present during the evaluation of @context
and will overlay the properties of this
if they are accessed.
@apply
supports the same extended syntax and ordering as fusion processors and supports multiple keys.
The evaluation order is defined via @position
, the keys that are evaluated last will override previously defined keys.
This is also similar to the rules for @process
:
test = Vendor.Site:Prototype {
@apply.contextValue {
@position = 'start'
expression = ${ arrayValueFromContext }
}
@apply.fusionObject {
@position = 'end'
expression = Neos.Fusion:RawArray {
value = "altered value"
}
}
}
Other than @context
@apply
is only valid for a single fusion path, so when subpathes or children are
rendered they are not affected by the parents @apply
unless they are explicitly passed down.
Debugging
To show the result of Fusion Expressions directly you can use the Neos.Fusion:Debug Fusion-Object:
debugObject = Neos.Fusion:Debug {
# optional: set title for the debug output
# title = 'Debug'
# optional: show result as plaintext
# plaintext = TRUE
# If only the "value"-key is given it is debugged directly,
# otherwise all keys except "title" and "plaintext" are debugged.
value = "hello neos world"
# Additional values for debugging
documentTitle = ${q(documentNode).property('title')}
documentPath = ${documentNode.path}
}
# the value of this object is the formatted debug output of all keys given to the object
Domain-specific languages in Fusion
Fusion allows the implementation of domain-specific sublanguages. Those DSLs can take a piece of code, that is optimized to express a specific class of problems, and return the equivalent fusion-code that is cached and executed by the Fusion-runtime afterwards.
Fusion-DSLs use the syntax of tagged template literals from ES6 and can be used in all value assignments:
value = dslIdentifier`... the code that is passed to the dsl ...`
If such a syntax-block is detected fusion will:
Lookup the key
dslIdentifier
in the SettingNeos.Fusion.dsl
to find the matching dsl-implementation.Instantiate the dsl-implementation class that was found registered.
Check that the dsl-implementation satisfies the interface
\Neos\Fusion\Core\DslInterface
Pass the code between the backticks to the dsl-implementation.
Finally parse the returned Fusion-code
Fusion DSLs cannot extend the fusion-language and -runtime itself, they are meant to enable a more efficient syntax for specific problems.