fluidstan / fluidstan
Static analysis for TYPO3 Fluid templates and components, powered by PHPStan
Requires
- php: ^8.2
- composer-runtime-api: ^2.2
- ext-ctype: *
- nette/neon: ^3.4
- nikic/php-parser: ^5.0
- phpstan/phpstan: ^2.1.30
- psr/container: ^1.0 || ^2.0
- symfony/console: ^7.0
- symfony/process: ^7.0
- typo3fluid/fluid: ^4.4 || ^5.0
Requires (Dev)
- ergebnis/composer-normalize: ^2.54
- friendsofphp/php-cs-fixer: ^3.95
- infection/infection: ^0.32 || ^0.35
- php-parallel-lint/php-parallel-lint: ^1.4
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-phpunit: ^2.1
- phpstan/phpstan-strict-rules: ^2.1
- phpunit/phpunit: ^11.5
- rector/rector: ^2.7
- shipmonk/composer-dependency-analyser: ^1.8
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-14 15:34:52 UTC
README
Static analysis for TYPO3 Fluid templates and components, powered by PHPStan. Developed and maintained by Flowd GmbH.
FluidStan knows the PHP types behind your template variables and reports what Fluid would only reveal at
runtime. The screenshot shows the analysis of docs/example, a product card component
with a typo in a property path, a wrong argument type, an object rendered without __toString(), a loop
over a string, an undefined variable and an unknown slot. Run it yourself with composer fluidstan:example.
It reports:
- property paths that cannot be resolved (
{item.nonexistent}), following Fluid's lookup order: array key,ArrayAccess, PSR container, getter, isser, hasser, public property - values that cannot be rendered (
{item.money}whereMoneyhas no__toString(), arrays) f:forover something that is not iterable- ViewHelper and component arguments whose type does not satisfy the argument definition, with the
same rules Fluid's
StrictArgumentProcessorapplies at runtime. Note that Fluid casts scalars before validating: a string passed to anintargument, or a float to astring, is accepted at runtime and therefore not reported; objects, arrays andnullfor required arguments are. SeeStrictArgumentProcessor::process()and its use inViewHelperInvoker. - non-numeric operands in
{a + b} - undefined variables in components (their scope is closed)
- unknown ViewHelpers, unknown or missing arguments, syntax errors (Fluid's parser), unknown component
slots,
f:renderof undefined sections, deprecated ViewHelper usage
Fluid components (Fluid 4.3+, TYPO3 13.4+) declare their argument types with f:argument, so they are
analysed out of the box. Plain templates have mixed variables unless you declare them (see below).
Requires PHP 8.2+, Fluid 4.4 or 5.x, PHPStan 2. Argument checks always follow Fluid 5's strict validation; on Fluid 4, whose runtime is lenient, they report what would fail after an upgrade.
Status: early. The pipeline is complete, the rule set is small and the design is settled; see "How it works" and "Roadmap".
Roadmap
- Layouts, partials and sections analysed with the types of their call sites (sections are currently
analysed with
mixedvariables) - Type inference from
assign()/assignMultiple()calls in PHP code - Narrowing through
f:ifconditions - Opt-in strict mode for argument types that reports what Fluid silently casts, such as a
non-numeric string passed to an
intargument - Baseline file, result cache, more output formats
- Upstream proposal: positions on Fluid syntax tree nodes
Installation
composer require --dev fluidstan/fluidstan
# TYPO3 projects:
composer require --dev fluidstan/typo3
Usage
vendor/bin/fluidstan # uses ./fluidstan.neon vendor/bin/fluidstan path/to/Components -l 6 # explicit paths, PHPStan level vendor/bin/fluidstan --error-format=json vendor/bin/fluidstan --debug # prints the directory of the generated PHP code
fluidstan.neon:
parameters: paths: - packages/my_site/Resources/Private/Components - packages/my_site/Resources/Private/Templates suffixes: [.html] namespaces: # identifier => PHP namespaces or component collection classes; "f" defaults to TYPO3Fluid\Fluid\ViewHelpers my: My\Site\Components\ComponentCollection bootstrapFiles: - vendor/autoload.php tmpDir: var/cache/fluidstan phpstan: level: 8 includes: [] # extra PHPStan config files for the generated code parameters: {} # extra PHPStan parameters
TYPO3 projects use the adapter, which boots TYPO3 without a database, registers the namespaces from
$GLOBALS['TYPO3_CONF_VARS']['SYS']['fluid']['namespaces'] and resolves EXT: paths:
parameters: environment: FluidStan\TYPO3\Typo3Environment bootstrapFiles: - vendor/autoload.php paths: - EXT:my_site/Resources/Private/Components
Declaring variable types
Components: <f:argument name="item" type="My\Model\Item" />. Fluid's type grammar applies:
scalars, mixed, object, iterable, class names, Item[], unions.
Plain templates (and overrides in components): a @var annotation in a comment, PHPDoc types allowed.
<f:comment> @var My\Model\Item item @var list<My\Model\Item> items </f:comment>
How it works
- Templates are parsed with Fluid's own
TemplateParser, configured with the real ViewHelper namespaces so argument definitions are real. Fluid's nodes carry no positions, so a parser subclass records one per node. Parser errors are collected instead of aborting. - Each template is transpiled to a PHP function: component arguments become parameters,
f:forbecomesforeach,f:variablean assignment,f:ifanif. Everything dynamic becomes a call onFluidStan\Runtime\F, e.g.{item.title}→F::get($item, 'title'). - PHPStan analyses the generated code with the FluidStan extension (
extension.neon): return type extensions resolve the facade calls, rules implement Fluid's runtime semantics. - Errors are mapped back to template lines and printed in PHPStan's style.
Fluid's compiled template cache is not used: it resolves every variable through one opaque variable provider, which erases the scope information PHPStan needs.
What comes from Fluid, what is mirrored
FluidStan reuses Fluid wherever Fluid exposes the behaviour: the parser and syntax tree, namespace
detection, ViewHelper resolution and argument definitions, the parser's own argument validation
(unknown and missing arguments), component definitions and slots, section registration, template
discovery (TemplateFinder, Fluid 5), deprecation reporting (as TemplateValidator does) and the
unwrapping of wrapped parse exceptions.
Some runtime behaviour has no static counterpart in Fluid and is mirrored in the PHPStan extension or the transpiler. When Fluid changes one of these, FluidStan must follow:
| Behaviour | Source of truth in Fluid |
|---|---|
| Property path lookup order | StandardVariableProvider::getByPath() |
| Argument type validation | StrictArgumentProcessor (process() then isValid()) |
| Casting of output and of multiple children | AbstractNode::evaluateChildNodes() / castToString() |
| Ternary, math and casting expression syntax | TernaryExpressionNode, MathExpressionNode, CastingExpressionNode |
| Fragment to slot mapping | ComponentAdapter (private helpers) |
| Argument parsing with non-fatal validation | TemplateParser::parseArguments() and abortIfRequiredArgumentsAreMissing() |
| Iteration variable keys | ForViewHelper |
Candidates for upstream contributions that would remove mirrored code: source positions on syntax
tree nodes, a public fragment extraction on ComponentAdapter, and a reusable type grammar helper
for argument definitions.
Development
composer install composer check # everything below composer lint # syntax, composer.json, php-cs-fixer, PHPStan (max, strict), Rector, dependency analysis composer fix # apply Rector, php-cs-fixer and composer normalize composer test # PHPUnit composer test:coverage # PHPUnit with coverage (Xdebug or pcov) composer test:mutation # Infection, full run, minimum MSI 70 % (CI runs it on main and weekly; pull requests only mutate changed lines) composer fluidstan # analyse the fixture templates
The runtime facade src/Runtime/F.php is excluded from Rector: its stubs' signatures are the contract with
the generated code. Fluid's parser and syntax tree are @internal; PHPStan's internal-usage checks are
disabled for the TYPO3Fluid namespace only. CI runs the suite on PHP 8.2 to 8.5 against Fluid 4 and 5,
with lowest and highest dependencies.