Im ersten Artikel haben wir uns angesehen, was Yii::app()->loadHelper() eigentlich macht, warum die common_helper.php architektonisch ein Problem ist und wie der Weg heraus aussehen könnte: Composer Autoloading, Service-Klassen, saubere Abhängigkeiten.
Was wir damals nicht hatten: Zahlen.
Wie oft wird eine Funktion aus common_helper.php eigentlich aufgerufen? Welche Funktionen sind so gut wie tot und können sofort gelöscht werden? Und welche sind so tief im Code verwurzelt, dass eine Migration echte Arbeit bedeutet?
Genau das wollte ich wissen. Also habe ich PHPStan nicht als Qualitäts-Gate eingesetzt – sondern als Analysewerkzeug.
Die Ausgangslage
LimeSurvey 6 lädt die common_helper.php über Yii::app()->loadHelper('common'). Die Datei liegt unter:
application/helpers/common_helper.php
Sie enthält über 130 globale PHP-Funktionen – von gt() und et() über createfieldmap() bis rmdirr().
Keine Klassen, keine Namespaces, kein Autoloading. Einfach prozeduraler Code, der seit Jahren mitgeschleppt wird.
Das Problem ist bekannt. Aber ohne Zahlen bleibt die Diskussion abstrakt: "Da muss man irgendwann mal was machen."
Die Idee: PHPStan als Analyse-Lupe
PHPStan kennt man primär als statischen Typprüfer. Was viele nicht wissen: PHPStan ist erweiterbar. Man kann eigene Custom Rules schreiben, die den AST (Abstract Syntax Tree) des analysierten Codes traversieren und bei bestimmten Mustern anschlagen.
Unsere Regel ist denkbar einfach:
Für jeden Funktionsaufruf im Code: Ist der aufgerufene Name in der Liste der Funktionen aus
common_helper.php? Wenn ja – melden.
Und weil wir keine Fehlerliste wollen, sondern einen Report mit Häufigkeiten, schreiben wir zusätzlich einen Custom Formatter, der die Meldungen aggregiert und aufbereitet.
Das Setup
Das Projekt liegt in zwei Ordnern nebeneinander:
limesurvey/
├── ls6/ ← die LimeSurvey-Instanz
└── stan/ ← unser PHPStan-Projekt
├── composer.json
├── phpstan.neon
└── src/
├── DeprecatedHelperRegistry.php
├── DeprecatedHelperFunctionRule.php
└── DeprecatedHelperFrequencyFormatter.php
Die composer.json im stan-Ordner ist minimal:
{
"require": {
"php": "^8.1",
"phpstan/phpstan": "^2.0"
},
"autoload": {
"psr-4": {
"LSStan\\": "src/"
}
}
}
Die drei Klassen
1. DeprecatedHelperRegistry
Die Registry ist der gemeinsame Zustand. Sie liest beim Start die common_helper.php per Regex aus und extrahiert alle Funktionsnamen – ohne die Datei zu inkludieren oder auszuführen.
private function parseFunctionNames(string $filePath): array
{
$content = file_get_contents($filePath);
if (preg_match_all(
'/^\s*function\s+([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)\s*\(/m',
$content,
$matches
)) {
foreach ($matches[1] as $name) {
$functions[] = strtolower($name);
}
}
return array_unique($functions);
}
Alle gefundenen Funktionen werden mit dem Startwert 0 initialisiert. So tauchen auch Funktionen, die nirgendwo aufgerufen werden, später im Report auf.
2. DeprecatedHelperFunctionRule
Die Rule implementiert PHPStans Rule<FuncCall>-Interface. Bei jedem Funktionsaufruf im analysierten Code prüft sie, ob der Name in der Registry bekannt ist:
public function processNode(Node $node, Scope $scope): array
{
if (!$node->name instanceof Name) {
return []; // dynamische Aufrufe ($fn()) überspringen
}
$functionName = $node->name->toString();
if (!$this->registry->isDeprecated($functionName)) {
return [];
}
return [
RuleErrorBuilder::message(
sprintf(
'[DEPRECATED_HELPER] Function %s() is deprecated (defined in common_helper.php)',
$functionName
)
)
->identifier('limesurvey.deprecatedHelper')
->build(),
];
}
Das [DEPRECATED_HELPER]-Prefix ist kein Zufall. Es dient dem Formatter später als zuverlässiges Erkennungsmerkmal.
3. DeprecatedHelperFrequencyFormatter
PHPStan verarbeitet Dateien parallel in Worker-Prozessen. Die Rule läuft in diesen Workern – der Formatter läuft im Hauptprozess und kennt alle gesammelten Ergebnisse. Das ist der richtige Ort zum Aggregieren.
Der Formatter zählt die [DEPRECATED_HELPER]-Meldungen nach Funktionsname, ergänzt die Nullen aus der Registry und gibt eine sortierte Tabelle aus:
// Alle bekannten Funktionen mit 0 initialisieren
foreach (array_keys($this->registry->all()) as $fnName) {
$counts[$fnName] = 0;
}
// Treffer aus den Fehlermeldungen zählen
foreach ($analysisResult->getFileSpecificErrors() as $error) {
if (preg_match('/Function (.+?)\(\) is deprecated/', $error->getMessage(), $m)) {
$key = strtolower($m[1]);
$counts[$key]++;
}
}
Die Konfiguration
Die phpstan.neon hält alles zusammen. Wichtig: Eigene Parameter direkt unter parameters erlaubt PHPStan nicht. Der Pfad zur Helper-Datei kommt direkt als Service-Argument. Der Formatter wird über einen benannten Service-Key registriert.
parameters:
paths:
- %currentWorkingDirectory%/../ls6/application
phpVersion: 80100
level: 0
# Alle anderen Fehler unterdrücken – nur unsere Regel soll zählen
ignoreErrors:
- '#^(?!\[DEPRECATED_HELPER\])#'
services:
deprecatedHelperRegistry:
class: LSStan\DeprecatedHelperRegistry
arguments:
helperFilePath: %currentWorkingDirectory%/../ls6/application/helpers/common_helper.php
-
class: LSStan\DeprecatedHelperFunctionRule
arguments:
registry: @deprecatedHelperRegistry
tags:
- phpstan.rules.rule
errorFormatter.deprecatedHelper:
class: LSStan\DeprecatedHelperFrequencyFormatter
arguments:
registry: @deprecatedHelperRegistry
Ausgeführt wird die Analyse mit:
vendor/bin/phpstan analyse --error-format=deprecatedHelper
Die Ergebnisse
Nach dem Durchlauf über den kompletten application-Ordner von LimeSurvey 6:
Function Count
──────────────────────────────────────────────────────────
gt() 7741
et() 2231
flattentext() 168
returnglobal() 98
getsurveyinfo() 84
tableexists() 63
...
checkmovequestionconstraintsforconditions() 0 ✂ can be deleted
csvunquote() 0 ✂ can be deleted
dofooter() 0 ✂ can be deleted
...
──────────────────────────────────────────────────────────
Total usages : 11339
Still used : 126 function(s)
Can delete : 13 function(s) ✂
Was die Zahlen bedeuten
gt() und et() dominieren mit zusammen ~88% aller Aufrufe. Beide sind Wrapper um Yii 1.1's CGettextMessageSource – LimeSurvey übersetzt über .mo/.po-Dateien, und gt() ist die zentrale Funktion dafür. Ein Blick in den Quellcode zeigt, warum eine Migration alles andere als trivial ist:
function gT($sToTranslate, $sEscapeMode = 'html', $sLanguage = null)
{
if ($sToTranslate == '') {
return '';
}
return quoteText(Yii::t('', $sToTranslate, array(), null, $sLanguage), $sEscapeMode);
}
gt() macht zwei Dinge über Yii::t() hinaus: einen Early Return bei leerem String und – wichtiger – die Ausgabe läuft durch quoteText(), das je nach $sEscapeMode (html, js, unescaped, ...) unterschiedlich escaped. Yii::t() ist also kein Drop-in-Ersatz. Bevor gt() migriert werden kann, muss quoteText() selbst in eine saubere Abstraktion gewandert sein – ein eigener Service, ein Twig-Filter, oder eine Methode in einer Hilfsklasse.
et() ist schlicht echo gt(...) – fällt damit in dieselbe Kategorie.
13 Funktionen werden nirgendwo aufgerufen und können ohne Risiko aus der common_helper.php gelöscht werden. Darunter dofooter(), doheader(), sendemailmessage() – Relikte, die irgendwann ersetzt wurden, aber nie entfernt wurden.
126 Funktionen sind noch aktiv im Einsatz. Für viele davon gibt es in LimeSurvey 6 bereits Alternativen in Form von Service-Klassen – sie werden aber aus Gewohnheit oder wegen fehlender Dokumentation noch über den globalen Namen aufgerufen.
Was PHPStan dabei nicht sieht
Eine Einschränkung ist wichtig zu nennen: PHPStan analysiert statischen Code. Dynamische Funktionsaufrufe über Strings – etwa call_user_func('gt', $text) oder Variablen wie $fn = 'gt'; $fn($text) – werden von unserer Rule nicht erfasst. In der Praxis sind solche Muster in LimeSurvey selten, aber sie existieren.
Außerdem analysiert PHPStan standardmäßig nur .php-Dateien. Views, die als .twig oder .html vorliegen und PHP-Funktionsaufrufe enthalten, bleiben außen vor.
Die Zahlen sind also eine Untergrenze – die tatsächliche Verwendung kann höher liegen.
Fazit
PHPStan als reines Qualitäts-Gate zu verwenden ist naheliegend. Es als Analyse- und Inventurwerkzeug einzusetzen ist mindestens genauso wertvoll – und kostet wenig Aufwand.
Mit drei Klassen und einer handvoll Konfiguration haben wir aus einer vagen Annahme ("da sind viele Legacy-Aufrufe") eine konkrete Zahl gemacht: 11.339 Aufrufe, 126 betroffene Funktionen, 13 sofort löschbar.
Das ist die Grundlage, auf der eine Migration geplant werden kann. Nicht aus dem Bauch heraus, sondern datenbasiert.