Ich pflege phpstan-typo3, die PHPStan-Extension für TYPO3. Statische Analyse prüft Code, ohne ihn auszuführen, und findet dabei Fehler, die sonst erst in Produktion auffallen. Für den PHP-Teil eines TYPO3-Projekts ist das seit Jahren Standard. Für Fluid-Templates gab es bisher nichts Vergleichbares, obwohl dort dieselbe Art von Fehlern steckt.
Bei Flowd bauen wir Frontends aus Components, früher mit sitegeist/fluid-components, seit Fluid 4.3 und TYPO3 13.4 mit den nativen Fluid Components. Eine Component deklariert ihre Argumente per f:argument, inklusive Typ. Ausgewertet wurden diese Deklarationen bisher nur zur Laufzeit. FluidStan wertet sie statisch aus und prüft jede Verwendungsstelle dagegen.
Was zur Laufzeit passiert
Schlägt in Fluid eine Auflösung fehl, hängt das Ergebnis vom Fall ab.
{product.teaserText} auf einem Model, dessen Getter getTeaser() heißt, ergibt einen leeren String. Die Seite rendert, der Teaser fehlt, es gibt weder Exception noch Log-Eintrag. Ein Objekt ohne __toString() in der Ausgabe wirft dagegen eine Exception, und zwar nur auf den Seiten, auf denen die Variable gesetzt ist. Ein f:for über einen String bricht ab. Ein Argument mit falschem Typ verhält sich je nach Fluid-Version unterschiedlich: Fluid 4 konvertiert stillschweigend, Fluid 5 lehnt ab, der Fehler taucht also erst beim Upgrade auf.
Für PHPStan ist keiner dieser Fälle sichtbar, weil Templates kein PHP sind.
Was FluidStan meldet
FluidStan parst Templates, ohne sie zu rendern, und kennt die PHP-Typen hinter den Variablen. Gemeldet werden:
- Property-Pfade, die sich nicht auflösen lassen, in Fluids Lookup-Reihenfolge: Array-Key,
ArrayAccess, PSR-Container, Getter, Isser, Hasser, public Property - Werte, die sich nicht ausgeben lassen, etwa ein Objekt ohne
__toString()oder ein Array f:forüber nicht iterierbare Werte- ViewHelper- und Component-Argumente, deren Typ die Argument-Definition nicht erfüllt, nach den Regeln von Fluids
StrictArgumentProcessor - nicht-numerische Operanden in
{a + b} - undefinierte Variablen in Components, deren Scope geschlossen ist
- unbekannte ViewHelper, unbekannte oder fehlende Argumente, unbekannte Component-Slots,
f:renderauf nicht existierende Sections, veraltete ViewHelper und Syntaxfehler
Die Ausgabe folgt dem Format von PHPStan, mit Template-Pfad und Template-Zeile:
Die Beispiel-Extension aus dem TYPO3-Paket, analysiert mit vendor/bin/fluidstan.
Jeder Fehler trägt einen Identifier (fluidstan.property.unresolvable, fluidstan.argument.type) und lässt sich wie in PHPStan einzeln ignorieren.
Typisierte Components
In einem Template ohne Typangaben sind alle Variablen mixed. FluidStan meldet dort Syntaxfehler, unbekannte ViewHelper und fehlende Argumente, prüft aber keine Property-Pfade.
Components deklarieren ihre Argumente:
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">
<f:argument name="amount" type="float" />
<f:argument name="currency" type="string" optional="{true}" default="€" />
<span class="a-price"><f:format.currency currencySign="{currency}" decimals="2">{amount}</f:format.currency></span>
</html>
Jede Verwendungsstelle von shop:atom.price wird gegen diese Deklaration geprüft.
Steht hinter einem Argument eine PHP-Klasse, werden auch die Property-Zugriffe darauf geprüft:
<f:argument name="product" type="Vendor\Shop\Domain\Model\Product" />
<time>{product.releasedAt}</time>
<shop:atom.price amount="{product.price}" />
<shop:atom.price amount="{product.image}" />
Zeile 2 gibt ein DateTimeImmutable ohne Formatierung aus. Zeile 4 übergibt eine FileReference|null an ein Argument vom Typ float. Beides wird gemeldet.
Der Nutzen der Analyse hängt damit direkt am Grad der Typisierung: Je vollständiger die Argumente deklariert sind, desto mehr wird geprüft.
Templates, Partials und Layouts ohne Component-Definition sowie Variablen aus einem Controller lassen sich per Annotation typisieren:
<f:comment>
@var Vendor\Shop\Domain\Model\Product product
@var list<Vendor\Shop\Domain\Model\Product> products
@var array<string, mixed> data
</f:comment>
PHPDoc-Typen sind erlaubt, Generics und Unions funktionieren.
FluidStan in TYPO3
Das Kernpaket kennt nur Fluid. Namespaces, Extension-Pfade und die EXT:-Schreibweise kommen von TYPO3, dafür gibt es fluidstan/typo3:
- Boot im Failsafe-Modus, ohne Datenbank, damit Package Manager, Extension-Pfade und Konfiguration während des Parsens verfügbar sind.
- Registrierung der Fluid-Namespaces:
fenthältTYPO3\CMS\Fluid\ViewHelpers, dazu alles aus$GLOBALS['TYPO3_CONF_VARS']['SYS']['fluid']['namespaces']. Global registrierte ViewHelper-Namespaces müssen damit nicht erneut aufgeführt werden. - Auflösung von
EXT:-Pfaden in der Konfiguration.
Konfiguriert wird in einer Datei im Projekt-Root:
parameters:
environment: FluidStan\TYPO3\Typo3Environment
bootstrapFiles:
- vendor/autoload.php
paths:
- EXT:my_site/Resources/Private/Components
- EXT:my_site/Resources/Private/Templates
namespaces:
my: My\Site\Components\ComponentCollection
tmpDir: var/cache/fluidstan
phpstan:
level: 8
fluidstan.neon. namespaces wird nur für Component Collections und Namespaces gebraucht, die in TYPO3 nicht global registriert sind.
vendor/bin/fluidstan
vendor/bin/fluidstan --error-format=json
Der Boot liest config/system/settings.php wie jeder TYPO3-CLI-Befehl, der Lauf gehört also in den PHP-Container. Eine Datenbankverbindung wird nicht geöffnet.
Wie die Analyse funktioniert
Parsen übernimmt Fluids eigener TemplateParser, konfiguriert mit den echten ViewHelper-Namespaces, damit die Argument-Definitionen die echten sind. Fluids Syntaxbaum trägt keine Quellpositionen, deshalb erfasst eine Parser-Subklasse eine pro Node. Parse-Fehler werden gesammelt, statt den Lauf abzubrechen.
Transpiliert wird jedes Template zu einer PHP-Funktion. Component-Argumente werden zu typisierten Parametern, f:for zu foreach, f:variable zu einer Zuweisung, f:if zu if. Dynamische Zugriffe werden zu Aufrufen einer Runtime-Fassade:
<f:argument name="product" type="Vendor\Shop\Domain\Model\Product" />
<h2>{product.title}</h2>
function (Product $product): void {
F::output(F::get($product, 'title'));
}
Vereinfachte Darstellung. Die Typinformation ist an dieser Stelle eine PHP-Signatur.
Analysiert wird dieser generierte Code von PHPStan mit der FluidStan-Extension: Return-Type-Extensions lösen die Aufrufe der Fassade auf, eigene Rules bilden Fluids Laufzeitverhalten ab, etwa die Lookup-Reihenfolge und die Argument-Prüfung.
Zurückgerechnet werden die Fehler anschließend auf Template-Datei und Template-Zeile.
Fluids eigener Template-Cache wird dabei nicht verwendet. Er löst jede Variable über einen einzelnen Variable Provider auf und verliert damit die Scope-Information, auf die PHPStan angewiesen ist.
Installation und Status
composer require --dev fluidstan/fluidstan
composer require --dev fluidstan/typo3
Das zweite Paket zieht das erste als Abhängigkeit nach.
Vorausgesetzt werden PHP 8.2 oder neuer, Fluid 4.4 oder 5.x und PHPStan 2; das TYPO3-Paket unterstützt TYPO3 13.4 und 14 im Composer-Modus. Beide Pakete stehen unter MIT.
FluidStan ist in einem frühen Stadium. Die Pipeline ist vollständig, das Regelwerk noch klein, Sections werden derzeit mit mixed-Variablen analysiert. Geplant sind TYPO3s eigene Variablen ({settings}, {data}, {page}, {site}) ab Werk typisiert, Content-Blocks-Felddefinitionen als Typ von {data}, Partials und Layouts über Site Sets und TypoScript aufgelöst, Extbase-Relationen aus ObjectStorage und Lazy-Loading-Proxies ausgepackt, dazu Baseline-Datei und Result-Cache.
Die Argument-Prüfung folgt immer der strikten Validierung aus Fluid 5. Unter Fluid 4 meldet FluidStan damit auch, was ein Upgrade später bemängeln würde.
Code, Issues und die Beispiele aus diesem Artikel: fluidstan/fluidstan und fluidstan/typo3.