Do you remember the good old days?
What is TypoScript? Various myths and speculations exist about that, well, programming language? No, it never was a programming language. Though it could be used to express logic. Is it a configuration language then? It's often used to configure settings, but the scope is certainly blurry.
The power of TypoScript - if used well - is the declarative approach of expressing the rendering and execution of various parts of the system. And a declaration in TypoScript is never final, but can be changed at a later time by another extension or template.
One major problem of the TypoScript implementation in TYPO3 CMS is the limited extensibility and the undefined behavior of a given snippet of code. TypoScript is not always evaluated directly, but the final definition of the so called setup are given to PHP code to process it. And most of the time this processing behaviour differed quite a lot.
To prevent confusion, we started with a clear idea of what TypoScript in Neos should be and what it shouldn't be:
TypoScript 2 is a declarative and extensible language to integrate system components.
The main use case in Neos is the rendering declaration using TypoScript 2. With a strict MVC approach a controller finds the relevant node given by a URI and uses a TypoScript view to render a frontend representation of that node.
But we had more than just rendering of websites in our mind. Imagine an e-commerce application that should be re-used and extended by others. Instead of having templates that are used in a fixed way, a TypoScript driven view would be able to control the output in a fine-grained and extensible way.
output = 'Hello world!'
Output:
Hello world!
That looks familiar with a lot of programming languages. You could write the same line in Python, Ruby, JavaScript and other. But what does it mean here? We declare a path output
that is assigned a simple value of a string Hello world
.
Variables in TypoScript 2 are called paths. These are evaluated from the outside. For the examples we just take the name output
as the path we are rendering.
Simple values are a bit simplistic for the problems we have at hand. So TypoScript 2 offers objects as the main building blocks and re-using code. Think of them as components that can be a complete basic page, a menu or the rendering instructions for a node.
output = Array {
hello = 'Hello'
world = 'world!'
}
Output:
Helloworld!
The Array
object will allow nesting and is evaluated by rendering all its children and combining their output. This is done literally, so we miss a space between the words here.
The paths hello
and world
are properties of the object in this case. We can set an individual property with the following syntax:
output.world = ' world!'
This should result in the expected output of Hello world!
.
Whenever you see the brackets "{" and "}" in TypoScript, it's only a shorter form of writing the paths in the full form:
output = Array
output.hello = 'Hello'
output.world = ' world!'
There is only a handful of object types that ship with the TypoScript 2 core. This makes the language comprehensible and easy to learn.
With objects we can declare structures. And with simple values we can declare values like strings, boolean values or numbers.
Yet, sometimes we want to have something more expressive at hands to change a behaviour depending on some condition or to specify the processing of some property.
output = ${Array.join(['Hello', 'world!'], ' ')}
Output:
Hello world!
Wow, that looks like we are actually programming here!
Indeed we introduced an expression language called Eel in TypoScript 2. It's the perfect companion for the tricky and individual cases where we wished for something more than objects and simple values. It's a subset of the JavaScript language that allows to write any kind of expression. That means no manipulation of variables, but anything that expresses a value.
In the example we use a helper with name Array
that comes pre-defined with Neos. These helper objects provide a standard library of functions to work with strings, array, dates and other data in the system. And it's easy to write custom helpers to extend the functions that are available in Eel.
Expressions are really powerful if we have to deal with data coming from nodes or other objects. But where does that data come from?
For the frontend rendering in Neos we have the node that was represented by a URI like /home/about-us.html
. This node is given to TypoScript in the so called context and is accessible in expressions and by the objects themselves.
Context:
node = {Page node: About us}
TypoScript:
output = ${'<title>' + node.properties.title + '</title>'}
Output:
<title>About us</title>
An expression can access public properties and call methods of an object context variable. For security reasons expressions in the Neos TypoScript view have a protected context that does not allow arbitrary method calls on objects. Imagine someone writing a line of TypoScript that destroys a node while rendering it.
In the content repository of Neos everything is a node. The nodes are nested and form a tree. Navigating between the nodes using the node API of the content repository can become complicated.
We wished to have some simple tools for finding, filtering and navigating through the nodes in the repository. With FlowQuery we ported the ideas of the successful jQuery JavaScript library to the server-side. Isn't the node tree like a DOM?
Context:
node = {Page node: About us}
TypoScript:
output = ${q(node).parents().first().property('title')}
Output:
Home
This expression gets the direct parent of the node and reads the title
property. With FlowQuery we can achieve an individual output of the node structure without any PHP programming or writing complex queries in SQL.
Sometimes we're just not happy with what we have. This seems to be the human nature, but TypoScript 2 has a solution. It's called processor and can be applied to any value.
Imagine the expression for rendering the title of the direct parent of a node:
output = ${q(node).parents().first().property('title')}
What if we wanted to wrap this with a <strong>
tag in our site package?
output = ${'<strong>' + q(node).parents().first().property('title') + '</strong>'}
This works, but we need to copy the whole expression. A processor can be applied to a path
to alter it's value:
[email protected] = ${'<strong>' + value + '</strong>'}
The name strongTag
is arbitrary here and could be just a number. It's desirable though to give processors a distinct name.
So how could we declare the page output in Neos? As we have the power of Fluid available with the Template
object, we could just forward the node
context variable to the template.
Context:
node = {Page node: About us}
TypoScript:
page = Template {
templatePath = 'Main.html'
node = ${node}
childNodes = ${q(node).find('main').children()}
}
Main.html:
<html>
<title>{node.properties.title}</title>
<body>
<h1>{node.properties.title}</h1>
<f:for each="{childNodes}" as="childNode">
<h2>{childNode.properties.title}</h2>
<p>{childNode.properties.text}</p>
</f:for>
</body>
</html>
This should render the basic HTML for the page and the nodes in the main
content collection. But of course it only works if all the child nodes have a title
and text
property. What about supporting different node types and rendering them differently?
We need some additional features of TypoScript 2 to have a fully working page rendering.
Every object that is used in TypoScript 2 is based on a prototype definition. We can declare properties with default values on prototypes, which will be used for all the instances of that object:
prototype(Array) {
hello = 'Hello'
}
output = Array {
world = ' world!'
}
Output:
Hello world!
In this example the output path had the hello
property already declared from the Array
prototype, which holds the default declarations for all objects of type Array
.
But changing all the arrays is maybe not so useful. What about templates? Always forwarding the node
context to the template would be annoying.
prototype(Template) {
node = ${node}
}
page = Template {
templatePath = 'Main.html'
}
The page
path will now have a node
property pre-defined with the expression ${node}
. Note, that it's probably not useful to manipulate the default prototypes for the base objects like Array
or Template
in a real project.
This is why you can make up your own objects easily by extending from an existing prototype:
prototype(Footer) < prototype(Template) {
node = ${node}
}
output = Footer
The new Footer
object type extends from Template
and declares a property node
. If we assign the Footer
object to the output
path it will have all the declarations of the Footer
prototype and the Template
prototype applied.
This is especially handy to declare the rendering of node types and to create custom definitions for components of a page that can be re-used easily.
Of course there could be a lot of packages implementing a Teaser
or Text
node type. This is why we implemented namespaces from the beginning. We have the convention to use the package key as the prefix of a node type or object type in Neos to prevent these collisions.
prototype(Acme.Demo:Teaser) < prototype(TYPO3.Neos:Template) {
title = ${q(node).property('title')}
text = ${q(node).property('text')}
}
As we deal with collections often when rendering nodes, TypoScript comes with a Collection
object type that iterates over a list of something and renders the items using
tbd.