TYPO3.Fluid
provides a standard implementation which works great on simple MVC frameworks and as standalone rendering engine.
However, the standard implementation may lack certain features needed by the product into which you are integrating TYPO3.Fluid
.
To make sure you are able to override key behaviors of TYPO3.Fluid
the package will delegate much of the resolving,
instantiation, argument mapping and rendering of ViewHelpers to special classes which can be both manipulated and overridden by
the user. These special classes and their use cases are:
A fairly standard View implementation. The default object expects TemplatePaths
as constructor argument and has a handful of
utility methods like $view->assign('variablename', 'value');
. Custom View types can be implemented by simply subclassing the
default class - but in order to avoid problems, make sure you also call the original class' constructor method.
Creating a custom View allows you to change just a few aspects, mainly about composition: which implementations of TemplatePaths
the View requires, if it needs a custom ViewHelperResolver
, if it must have some default variables, if it should have a default
cache, etc.
Note: the special variable layoutName
is reserved and can be assigned to a template to set its Layout instead of using <f:layout name="LayoutName" />
.
In the default TemplatePaths
object included with TYPO3.Fluid
we provide a set of conventions for resolving the template files
that go into rendering a Fluid template - the templates themselves, plus partials and layouts.
You should use the default TemplatePaths
object if:
- You are able to place your template files in folders that match the
TYPO3.Fluid
conventions, including the convention of subfolders named the same as your controllers. - You are able to provide the template paths that get used as an array with which
TemplatePaths
can be initialized. - Or you are able to individually set each group of paths.
- You are able to rely on standard format handling (
format
simply being the file extension of template files).
And you should replace the TemplatePaths
with your own subclass if:
- You answered no to any of the above.
- You want to be able to deliver template content before parsing, from other sources than files.
- You want the resolving of template files for controller actions to happen in a different way.
- You want to create other (caching-) identifiers for your partials, layouts and templates than defaults.
Whether you use your own class or the default, the TemplatePaths
instance must be provided as first argument for the View.
Because TYPO3.Fluid
was created in an MVC context it supports MVC behaviors, including setting a "context" for your rendering
process - to associate the rendering with a controller and an action. The default RenderingContext
provided by TYPO3.Fluid
has
limited support: it supports a controller name and an action name.
Should you require additional context variables - for example a package name, a sub-controller identification, a user validation,
the HTTP request object, a Response object, or whatever - you can create your own type of RenderingContext
and pass that to the
View. Doing this allows you to access this RenderingContext
from within ViewHelpers. One obvious purpose for this is to create
custom links to controller actions.
You should use the default RenderingContext
object if:
- You don't use an MVC context - you just render single templates possibly with partials and layouts.
- You use MVC and are able to rely on just a controller name and action name for your implementation.
- You can rely on the default way of storing template- and ViewHelper variables.
You should replace the RenderingContext
with your own if:
- You answered no to any of the above.
- You require additional (framework/implementation-specific) attributes on the
RenderingContext
. - You require dynamic ways of returning the controller name, action name (or other custom attributes).
- You wish to modify or replace the special
VariableContainer
objects that store variables to implement things like reserved variable names, persistent and auto-added variables and similar container-related operations.
Whether you use your own class or the default, the RenderingContext
instance must be passed as second argument for the View.
If you do not pass a RenderingContext
, the default one will automatically be used.
The caching of Fluid templates happens by compiling the templates to PHP files which execute much faster than a parsed template
ever could. These compiled templates can only be stored if a FluidCacheInterface
-implementing object is provided. TYPO3.Fluid
provides one such caching implementation: the SimpleFileCache
which just stores compiled PHP code in a designated directory.
Should you need to store the compiled templates in other ways you can implement FluidCacheInterface
in your caching object.
Whether you use your own cache class or the default, the FluidCache
must be passed as third parameter for the View or it
must be assigned using $view->setCache($cacheInstance)
before calling $view->render()
.
While custom TemplatePaths
also allows sources of template files to be modified before they are given to the TemplateParser,
a custom TemplatePaths
implementation is sometimes overkill - and has the drawback of completely overruling the reading of
template file sources and making it up to the custom class how exactly this processing happens.
In order to allow a more readily accessible and flexible way of pre-processing template sources and affect key aspects of the
parsing process, a TemplateProcessorInterface
is provided. Implementing this interface and the methods it designates allows your
class to be passed to the TemplateView
and be triggered every time a template source is parsed, right before parsing starts:
$myTemplateProcessor = new MyTemplateProcessor();
$myTemplateProcessor->setDoMyMagicThing(TRUE);
$templateView->setTemplateProcessors(array(
$myTemplateProcessor
));
The registration method requires an array - this is to let you define multiple processors without needing to wrap them in a single class as well as reuse validation/manipulation across frameworks and only replace the parts that need to be replaced.
This makes the method preProcessSource($templateSource)
be called on this class every time the TemplateParser is asked to parse
a Fluid template. Modifying the source and returning it makes that new template source be used. Inside the TemplateProcessor
method you have access to the TemplateParser and ViewHelperResolver instances which the View uses.
The result is that TemplateProcessor instances are able to, for example:
- Validate template sources and implement reporting/logging of errors in for example a framework.
- Fix things like character encoding issues in template sources.
- Process Fluid code from potentially untrusted sources, for example doing XSS removals before parsing.
- Extract legacy namespace definitions and assign those to the ViewHelperResolver for active use.
- Extract legacy escaping instruction headers and assign those to the TemplateParser's Configuration instance.
- Enable the use of custom template code in file's header, extracted and used by a framework.
Note again: these same behaviors are possible using a custom TemplatePaths
implementation - but even with such a custom
implementation this TemplateProcessor pattern can still be used to manipulate/validate the sources coming from TemplatePaths
,
providing a nice way to decouple paths resolving from template source processing.
The ViewHelperInvoker
is a class dedicated to validating current arguments of and if valid, calling the ViewHelper's render
method. The default object supports only the arguments added via initializeArguments
and (register|override)Argument
on
the ViewHelper - and it does not use internal instance caching; it creates and renders new ViewHelpers for every node.
You should replace the ViewHelperInvoker
if:
- You must support different ways of calling ViewHelpers such as alternative
setArguments
names. - You wish to change the way the invoker uses and stores ViewHelper instances, for example to use an internal cache.
- You wish to change the way ViewHelper arguments are validated, for example changing the Exceptions that are thrown.
- You wish to perform processing on the output of ViewHelpers, for example to remove XSS attempts according to your own rules.
Note: ViewHelper instance creation and argument retrieval is handled by the ViewHelperResolver.
If you wish to use a custom ViewHelperInvoker
you must do so via a custom ViewHelperResolver
. You are given the class
name of the ViewHelper to resolve a ViewHelperInvoker
- which means you can also use different invokers for different classes.
In TYPO3.Fluid
most of your options for extending the language - for example, adding new ways to format strings, to make special
condition types, custom links and such - are connected to ViewHelpers. These are the special classes that are called using for
exampel <f:format.htmlentities>{somestring}</f:format.htmlentities>
.
A ViewHelper is essentially referenced by the namespace and the path to the ViewHelper, in this case f
being the namespace and
format.htmlentities
being the path.
The ViewHelperResolver
is the class responsible for turning these two pieces of information into an expected class name and when
this class is resolved, to retrieve from it the arguments you can use for each ViewHelper.
You should use the default ViewHelperResolver
if:
- You can rely on the default way of turning a namespace and path of a ViewHelper into a class name.
- You can rely on the default way ViewHelpers return the arguments they support.
- You can rely on instantiation of ViewHelpers happening through a simple
new $class()
. - You can rely on the default
ViewHelperInvoker
.
You should replace the ViewHelperResolver
if:
- You answered no to any of the above.
- You want to make ViewHelper namespaces available in templates without importing.
- You want to change which class is resolved from a given namespace and ViewHelper path, for example allowing you to add your own ViewHelpers to the default namespace or replace default ViewHelpers with your own.
- You want to change the argument retrieval from ViewHelpers or you want to manipulate the arguments (for example, giving them a default value, making them optional, changing their data type).
- You have to use a custom
ViewHelperInvoker
to actually render your ViewHelpers.
The default ViewHelperResolver
can be replaced in one way only: calling $view->setViewHelperResolver($resolverInstance);
on
the TemplateView. However, a custom View class can of course replace this and other aspects of the View such as TemplatePaths
.
The ExpressionNode
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.
TYPO3.Fluid
itself provides the following types of ExpressionNodes
:
MathExpressionNode
which scans for and evaluates simple mathematical expressions like{variable + 1}
.TernaryExpressionNode
which implements a ternary condition in Fluid syntax like{ifsomething ? thenoutputthis : elsethis}
CastingExpressionNode
which casts variables to a certain type, e.g.{suspectType as integer}
,{myInteger as boolean}
.
An ExpressionNode
basically consists of one an expression matching pattern (regex), one non-static method to evaluate the
expression public function evaluate(RenderingContextInterface $renderingContext)
and a mirror of this function which can be called
statically: public static evaluteExpression(RenderingContextInterface $renderingContext, $expression)
. 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 evaluateExpression
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
ExpressionNodes
with ones using the same, or an expanded version of their regular expression patterns to further extend the strings they capture and process.
Limitations
- Contrary to other nodes in Fluid,
ExpressionNodes
cannot be used in tag form. Only the shorthand/inline syntax is supported. ExpressionNodes
are not recursive unless they have recursive behavior internally (this is for example different from array nodes which match sub-arrays recursively). In other words:ExpressionNodes
are intended for simple syntaxes and variables.
To create a new type of ExpressionNode
- perhaps one that fits your framework:
- Make sure you subclass
TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\Expression\AbstractExpressionNode
and implementTYPO3Fluid\Fluid\Core\Parser\SyntaxTree\Expression\ExpressionNodeInterface
in your class. - Make sure your class pass
public static $detectionExpression = '%s';
where%s
is a perl regular expression which returns at least one match if the expression can be handled by yourExpressionNode
class. - Make sure your class implements a
public static evaluateExpression(RenderingContextInterface $renderingContext, $expression)
method which will be able to process the expression in a statically called context.
Any ExpressionNode
types added this way are also compilable off-the-bat. The one thing you can't change is how this compiling
happens - so if your ExpressionNode
does some heavy processing you may consider implementing a dedicated cache for it.
Additional ExpressionNode
class names can be returned from a custom ViewHelperResolver
(see above) by overriding the
getExpressionNodeTypes()
method and/or the protected $expressionTypes
property to append your class names to the list.
Each ExpressionNode
is consulted in the order they appear in this list - and only the first one that matches will be used.