typo3fluid/fluid
typo3fluid/fluid is a standalone PHP templating engine and the rendering core behind TYPO3. It provides secure, logic-light HTML templates with layouts, partials, sections, view helpers, and caching for maintainable server-rendered views.
Fluid is a standalone PHP template engine (the one TYPO3 uses, extracted into a reusable package). If you've used Blade or Twig, the concepts map: templates + layouts + partials + a registry of typed helper objects ("ViewHelpers", the f: namespace).
Install and pick a major version deliberately — the package ships parallel majors with different PHP floors and different argument-validation semantics:
composer require typo3fluid/fluid:^5.3 # new projects, strict argument processing
# ^4.6 for libraries still pinned there, ^2.14 only for legacy stacks
The three objects you interact with daily:
| Object | Role |
|---|---|
TYPO3Fluid\Fluid\View\TemplateView |
Entry point: assign(), assignMultiple(), render() |
RenderingContext |
Config hub: paths, cache, ViewHelper namespaces, escaping |
TemplatePaths |
Template/layout/partial root paths and file resolution |
Smallest working setup:
use TYPO3Fluid\Fluid\View\TemplateView;
$view = new TemplateView();
$context = $view->getRenderingContext();
$context->getTemplatePaths()->setTemplateRootPaths([__DIR__ . '/templates/']);
$context->getTemplatePaths()->setLayoutRootPaths([__DIR__ . '/templates/Layouts/']);
$context->getTemplatePaths()->setPartialRootPaths([__DIR__ . '/templates/Partials/']);
$view->assignMultiple(['user' => $user, 'items' => $items]);
echo $view->render('Index'); // resolves Index.fluid.html or Index.html
<f:layout name="Default" />
<f:section name="Main">
<h1>Hello {user.name}</h1>
<ul>
<f:for each="{items}" as="item" iteration="it">
<li>{it.cycle}. {item.title}</li>
</f:for>
</ul>
</f:section>
Look first at: the ViewHelpers/ directory in the package (that is the real API reference), the TYPO3 Fluid documentation for syntax (the release notes explicitly cross-reference it — the standalone docs are thin), and TemplatePaths if a template "cannot be found".
First real use case: transactional email and HTML fragment rendering in a non-TYPO3 app — assign DTOs, keep markup in .fluid.html files, render to string.
Custom ViewHelper (the 90% task). Declare the API in initializeArguments(); Fluid validates and type-casts inputs before render() is called:
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
final class BadgeViewHelper extends AbstractViewHelper
{
protected $escapeOutput = false; // render() produces trusted HTML
public function initializeArguments(): void
{
$this->registerArgument('label', 'string', 'Badge text', true);
$this->registerArgument('variant', 'string', 'Style variant', false, 'default');
$this->registerArgument('count', 'int|null', 'Optional counter', false, null);
}
public function render(): string
{
return sprintf('<span class="badge badge--%s">%s</span>',
htmlspecialchars($this->arguments['variant']), htmlspecialchars($this->arguments['label']));
}
}
Register a namespace once, at bootstrap:
$context->getViewHelperResolver()
->addNamespace('ui', 'Acme\\Ui\\ViewHelpers');
// template: <ui:badge label="New" variant="info" />
Wrap the engine in one factory/service. Don't scatter path and cache configuration across controllers. Build a FluidRenderer that owns a pre-configured RenderingContext, exposes render(string $template, array $data): string plus renderPartial() / renderSection() for AJAX fragments. In Laravel, bind it as a singleton in a service provider so the compiled-template cache survives across requests.
Composition patterns that scale:
f:section for page chrome; partials for repeated fragments.<f:render partial="Row" arguments="{_all}" /> to pass the whole scope (documented reserved variable in Fluid 5).<f:argument name="title" type="string" /> at the top of partials/templates to get typed, validated contracts instead of guessing what {title} is.<f:variable name="total" value="{items -> f:count()}" /> for derived values; avoid PHP-side pre-formatting that belongs in templates.f:for + iteration.isFirst/isLast/total instead of passing precomputed flags from PHP.Escaping policy as a pattern: variables are escaped by default. Only mark trusted output with f:format.raw or by passing objects that implement the UnsafeHTML marker interface (4.6/5.1+). Make this a code-review rule — it's the single most common security regression in Fluid codebases.
Caching in production, off in dev:
$context->setCache(new \TYPO3Fluid\Fluid\Core\Cache\SimpleFileCache(
__DIR__ . '/storage/cache/fluid'
));
Keep a fluid:clear console command / Makefile target; without a cache configured, Fluid reparses every template on every request.
Testing pattern: point templateRootPaths at a tests/fixtures/ directory, render, assert on the string. This is far cheaper than mocking a ViewHelper-heavy view layer. On 5.3+ also run the shipped CLI template analyzer in CI to catch broken templates without rendering every entry point.
Component libraries (4.6/5.2+): the metadata/annotations API and component-listing API let you generate a styleguide from your ViewHelper set — useful if you're replacing a design-system's Twig components.
$escapeOutput = true ViewHelper that returns markup gets its output escaped again when nested. Set escapeOutput/escapeChildren consciously; audit any ViewHelper that emits tags.StrictArgumentProcessor enforces declared types, supports unions (string|int), iterable, countable, callable. Passing null to a string argument throws — declare ?string/string|null. Stringable objects are cast early (5.1), but that's a convenience, not a license to skip types.<f:argument> does not work for sections rendered via f:render section="..." — a documented limitation. Use it in partials/templates, pass section data via arguments.myTemplate.fluid.html → myTemplate.html → myTemplate → capitalized variants. Lowercase names are now legal, but watch case sensitivity on Linux and be aware that the first match across all template root paths wins — verify your override order when layering package templates under app templates.TagBuilder, ViewHelper argument validation). If you're debugging blind on an older minor, upgrading is the fix.AbstractTemplateView shortcut methods are deprecated (since 2.14) — write against getRenderingContext() now, even if you're on 2.x, or the 4→5 upgrade bites.{foo.bar} resolves getters on objects and array keys on arrays; {foo.bar.baz} on a null intermediate silently yields empty rather than throwing. Use f:if condition="{foo.bar}" guards or f:debug when output is mysteriously blank.f:debug (dump the variable provider), the 5.3 CLI analyzer, and catching TYPO3Fluid\Fluid\Core\Parser\Exception vs Core\ViewHelper\Exception separately — parser failures are template syntax, ViewHelper failures are argument/API misuse.How can I help you explore Laravel packages today?