typo3/cms-fluid
Fluid is the templating engine for TYPO3 CMS, providing a readable, HTML-friendly syntax with layouts, sections, partials, and extensible ViewHelpers. It helps developers build dynamic, secure, maintainable frontend templates and integrates tightly with TYPO3's rendering pipeline.
Installing the package directly in Laravel is possible, but be aware what you're actually installing:
composer require typo3/cms-fluid
This pulls in typo3fluid/fluid (the templating engine itself) plus TYPO3 CMS dependencies (typo3/cms-core, typo3/cms-extbase, etc.). For a pure Laravel app, you almost always want just the engine:
composer require typo3fluid/fluid
Use typo3/cms-fluid only if you are:
$GLOBALS['TYPO3_CONF_VARS'] and TYPO3's ObjectManager).typo3fluid/fluid — the actual rendering engine. Start at Typo3Fluid\Fluid\View\TemplateView.TemplatePaths — controls where templates, layouts, and partials are discovered.ViewHelper\ViewHelperResolver — the extension point for adding your own f:*-style helpers.Cache\SimpleFileCache (or FluidCacheInterface) — Fluid compiles templates to PHP; you need a cache backend.vendor/typo3/cms-fluid/Resources/Private/Templates/ — example templates you can crib patterns from (layout, section, partial usage).use Typo3Fluid\Fluid\View\TemplateView;
use Typo3Fluid\Fluid\Core\Cache\SimpleFileCache;
$view = new TemplateView();
$paths = $view->getRenderingContext()->getTemplatePaths();
$paths->setTemplateRootPaths([resource_path('fluid/templates')]);
$paths->setLayoutRootPaths([resource_path('fluid/layouts')]);
$paths->setPartialRootPaths([resource_path('fluid/partials')]);
$view->getRenderingContext()->setCache(
new SimpleFileCache(storage_path('framework/fluid-cache'))
);
$view->assignMultiple([
'title' => 'Hello',
'items' => ['one', 'two', 'three'],
]);
return response($view->render('Index'));
With resources/fluid/templates/Index.html:
<f:layout name="Default" />
<f:section name="Main">
<h1>{title}</h1>
<ul>
<f:for each="{items}" as="item">
<li>{item}</li>
</f:for>
</ul>
</f:section>
Register Fluid as a custom view engine so view('fluid::index') works alongside Blade:
// app/Providers/FluidServiceProvider.php
public function register()
{
$this->app['view']->addExtension('fluid.html', 'fluid');
$this->app['view']->addExtension('html', 'fluid');
}
public function boot()
{
$this->app['view']->resolver->addNamespace('fluid', resource_path('fluid'));
}
Then a FluidEngine implementing Illuminate\View\Engines\Engine wrapping TemplateView. This lets you drop Fluid into existing controllers and keep $this->view(...) ergonomics. Because Fluid's assignMultiple is already array-based, this fits Laravel's view($name, $data) cleanly.
Fluid's real day-to-day value:
<f:layout name="Default" />) = Blade's layout + @section.<f:render partial="Card" arguments="{item: item}" />) = Blade's @include / anonymous components.<f:section name="Main">) = @yield blocks.Pattern: keep one layout per domain (e.g., Admin.html, Email.html), and treat every reusable UI block as a partial that takes explicit arguments. Fluid forces you to pass data explicitly rather than reaching into globals — that's a net win for component hygiene.
Do not try to use f:link.page, f:uri.action, f:form, or f:be.* inside Laravel — they will throw because they need a TYPO3 request/context. Instead:
f:link.page → write app:route calling route().f:uri.resource → write app:asset calling asset() / Vite::asset().f:translate → wrap __().ViewHelperResolver — you don't want a stray f:form in a template blowing up at render time.$resolver = $view->getRenderingContext()->getViewHelperResolver();
$resolver->addNamespace('app', 'App\\Fluid\\ViewHelpers');
Realistic migration pattern: use Fluid for content-heavy, author-edited, or email templates (where its strictness pays off) and Blade for admin/dashboard UI. Set up a dedicated fluid/ view namespace so the two never collide. Migrate partial-by-partial, not template-by-template.
Fluid templates want arrays/objects with public getters. Eloquent models work for simple properties but:
$model->posts) need to be pre-loaded and often mapped to plain arrays to keep templates predictable.->toArray() or a presenter so f:for iteration and {item.title} access work without surprises.Pattern: introduce a Presenter/Resource layer (JsonResource is close) that gives Fluid a flat, deterministic shape.
Wire Fluid's compiled-template cache to Laravel's storage and clear it with a view:clear override:
// in a deploy/Aritsan command
File::deleteDirectory(storage_path('framework/fluid-cache'));
For high-traffic pages, pre-warm the cache during deploy by rendering critical templates once with dummy data.
typo3/cms-fluid is GPL-2.0. That is a distribution-time concern, and it can bite you hard in commercial/proprietary products. typo3fluid/fluid (the standalone engine) is also GPL-2.0. If you're shipping closed-source software, confirm with legal before adopting. For internal or open-source Laravel apps this is usually a non-issue.
typo3/cms-fluid drags in TYPO3 core (TYPO3\CMS\Core, Extbase, ObjectManager, $GLOBALS['TYPO3_CONF_VARS']). This is a large surface area for a Laravel app and can:
symfony/* versions that conflict with your Laravel install.Tip: If you're not sharing a live TYPO3 instance, depend on typo3fluid/fluid only. You get the same template syntax, ViewHelper API, layouts, partials, and cache — without the CMS.
Fluid compiles each template to a PHP class whose name encodes the template contents. Editing a partial does not automatically invalidate a layout that renders it, and clearing storage/framework/views (Blade's cache dir) does nothing. Point Fluid's cache at its own directory and clear it explicitly in CI/deploy. Stale-cache bugs look like "my change didn't take" and waste hours.
Typo3Fluid\Fluid\Core\Parser\Exception and include template name + line. They do not map to Blade's line-numbered exception page.ViewHelperResolver::getNamespaces().render() in a helper that catches Typo3Fluid\Fluid\View\Exception\InvalidTemplateResourceException and logs the resolved template paths; 90% of "template not found" is a path misconfiguration, not a filename typo.Fluid searches TemplateRootPaths in reverse numeric order ([10, 20] searches 20 first). This lets you override a base template with a project-specific one — same idea as Laravel's package view overrides, but the ordering trips up first-timers.
Fluid escapes {variable} by default at the ViewHelper boundary, but not the same way Blade's {{ }} does. Double-braces {{ ... }} in Fluid mean no escaping (raw HTML). If you ported Blade habits into Fluid, audit for accidental unescaped output. Always use {var} or explicit <f:format.raw> only when intended.
Fluid templates are testable in-process: instantiate a TemplateView, assign fixture data, call renderToString('Index'), and assert on the HTML. This is easier than it looks and worth a small base test case:
public function renderFluid(string $template, array $data): string
{
$view = new TemplateView();
// ... configure paths/cache once via test setUp
$view->assignMultiple($data);
return $view->render($template);
}
Snapshot-test partials with fixed data — regressions in component output get caught fast.
ViewHelperResolver — add, replace, or block namespaces. Your single most important hook when integrating with Laravel.TemplatePaths — per-view override paths for theming/multi-tenancy.FluidCacheInterface — swap in a Redis or Laravel-cache-backed implementation for distributed deploys.RenderingContext — attach a controller name/action, variable container, and error handler. Use it to inject a Laravel-aware f:link implementation without touching templates.If the goal is "Blade with different syntax," Fluid isn't worth the cost — the GPL license, TYPO3 dependencies, and learning curve are real. Fluid earns its place when you have existing TYPO3 templates/ViewHelpers to share, or you want strict, argument-based, no-magic partials enforced by the engine. For new Laravel apps without a TYPO3 tie-in, stick with Blade + components — you'll get the same server-side rendering outcomes with far less friction.
How can I help you explore Laravel packages today?