Creating ExpressionNodes
The Expression
concept is the most profound way you can manipulate the
Fluid language itself, adding to it new syntax options that can be used inside
the shorthand {...}
syntax. Normally you are confined to using ViewHelpers
when you want such special processing in your templates - but using
ExpressionNodes allows you to add these processings as actual parts of the
templating language itself; avoiding the need to include a ViewHelper namespace.
Fluid itself provides the following types of Expression
:
Math
which scans for and evaluates simple mathematical expressions likeExpression Node {variable + 1}
.Ternary
which implements a ternary condition in Fluid syntax likeExpression Node {ifsomething ? thenoutputthis : elsethis}
Casting
which casts variables to a certain type, e.g.Expression Node {suspect
,Type as integer} {my
.Integer as boolean}
An Expression
basically consists of one an expression matching pattern
(regex), one non-static method to evaluate the expression
public function evaluate
and a mirror of this function which can be called statically:
public static evalute
.
The non-static method should then simply delegate to the static method and use
the expression stored in $this->expression
as second parameter for the static
method call.
ExpressionNodes automatically support compilation and will generate compiled
code which stores the expression and calls the static evaluate
method with the rendering context and the stored expression.
You should create your own ExpressionNodes if:
- You want a custom syntax in your Fluid templates (theoretical example:
casting variables using
{
).(integer)variablename} - You want to replace either of the above mentioned
Expression
with ones using the same, or an expanded version of their regular expression patterns to further extend the strings they capture and process.Nodes
Implementation
An ExpressionNode is always one PHP class. Where you place it is
completely up to you - but to have the class actually be detected and used by
Fluid, it needs to be added to the rendering context by calling set
.
In Fluid's default Rendering
, the following code is responsible for
returning expression node class names:
/**
* List of class names implementing ExpressionNodeInterface
* which will be consulted when an expression does not match
* any built-in parser expression types.
*
* @var string
*/
protected $expressionNodeTypes = [
'TYPO3Fluid\\Fluid\\Core\\Parser\\SyntaxTree\\Expression\\CastingExpressionNode',
'TYPO3Fluid\\Fluid\\Core\\Parser\\SyntaxTree\\Expression\\MathExpressionNode',
'TYPO3Fluid\\Fluid\\Core\\Parser\\SyntaxTree\\Expression\\TernaryExpressionNode',
];
/**
* @return string
*/
public function getExpressionNodeTypes()
{
return $this->expressionNodeTypes;
}
You may or may not want the listed expression nodes included, but if you change the available expression types you should of course document this difference about your implementation.
The following class is the math ExpressionNode from Fluid itself
which detects the {a + 1}
and other simple mathematical operations.
To get this behavior, we need a (relatively
simple) regular expression and one method to evaluate the expression while being
aware of the rendering context (which stores all variables, controller name,
action name etc).
<?php
namespace TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\Expression;
use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
class MathExpressionNode extends AbstractExpressionNode
{
/**
* Pattern which detects the mathematical expressions with either
* object accessor expressions or numbers on left and right hand
* side of a mathematical operator inside curly braces, e.g.:
*
* {variable * 10}, {100 / variable}, {variable + variable2} etc.
*/
public static string $detectionExpression = '/
(
{ # Start of shorthand syntax
\s* # Allow whitespace before expression
(?: # Math expression is composed of...
[_a-zA-Z0-9\.]+(?:[\s]*[*+\^\/\%\-]{1}[\s]*[_a-zA-Z0-9\.]+)+ # Various math expressions left and right sides with any spaces
|(?R) # Other expressions inside
)+
\s* # Allow whitespace after expression
} # End of shorthand syntax
)/x';
public static function evaluateExpression(RenderingContextInterface $renderingContext, string $expression, array $matches): int|float
{
// Split the expression on all recognized operators
$matches = [];
preg_match_all('/([+\-*\^\/\%]|[_a-zA-Z0-9\.]+)/s', $expression, $matches);
$matches[0] = array_map('trim', $matches[0]);
// Like the BooleanNode, we dumb down the processing logic to not apply
// any special precedence on the priority of operators. We simply process
// them in order.
$result = array_shift($matches[0]);
$result = static::getTemplateVariableOrValueItself($result, $renderingContext);
$result = ($result == (int)$result) ? (int)$result : (float)$result;
$operator = null;
$operators = ['*', '^', '-', '+', '/', '%'];
foreach ($matches[0] as $part) {
if (in_array($part, $operators)) {
$operator = $part;
} else {
$part = static::getTemplateVariableOrValueItself($part, $renderingContext);
$part = ($part == (int)$part) ? (int)$part : (float)$part;
$result = self::evaluateOperation($result, $operator, $part);
}
}
return $result;
}
// ...
}
Taking from this example class the following are the rules you must observe:
- You must either subclass the
Abstract
class or implement theExpression Node Expression
(subclassing the right class will automatically implement the right interface).Node Interface - You must provide the class with an exact property called
public static $detection
which contains a string that is a perl regular expression which will result in at least one match when run against expressions you type in Fluid. It is vital that the property be both static and public and have the right name - it is accessed without instantiating the class.Expression - The class must have a
public static function evaluate
taking exactly the arguments above - nothing more, nothing less. The method must be able to work in a static context (it is called this way once templates have been compiled).Expression - The
evaluate
method may return any value type you desire, but like ViewHelpers, returning a non-string-compatible value implies that you should be careful about how you then use the expression; attempting to render a non-string-compatible value as a string may cause PHP warnings.Expression