LimeSurvey und die Helper-Funktionen

Wie composer autoloading Yii::app()->loadHelper() obsolet macht

Wer LimeSurvey als Plugin-Entwickler nutzt oder in der Codebasis arbeitet, begegnet früher oder später diesem Muster:

Yii::app()->loadHelper('common');
Yii::app()->loadHelper('surveytranslator');
Yii::app()->loadHelper('replacements');

Dahinter steckt ein einfacher Mechanismus: loadHelper('common') lädt die Datei application/helpers/common_helper.php und macht ihre Funktionen im globalen Scope verfügbar. Dasselbe Muster findet sich auch bei loadLibrary(), das auf application/libraries/ zeigt:

public function loadHelper($helper)
{
    Yii::import('application.helpers.' . $helper . '_helper', true);
}

public function loadLibrary($library)
{
    Yii::import('application.libraries.' . $library, true);
}

Der konzeptuelle Unterschied zwischen Helper und Library ist dabei in der LimeSurvey-Codebasis kaum greifbar – beide Mechanismen lösen dasselbe Problem auf dieselbe Art. Das war in einer Zeit sinnvoll, als PHP noch kein Autoloading kannte und Funktionen manuell eingebunden werden mussten. PHP 5.1 war 2005. Composer erschien 2012.

Es ist 2026.


Was loadHelper() eigentlich ist

loadHelper() ist manuelles, imperatives Dependency-Management. Der Entwickler ist selbst dafür verantwortlich zu wissen:

  1. Welche Helper-Datei existiert
  2. Welche Funktionen sie enthält
  3. Ob sie bereits geladen wurde oder nicht
  4. In welcher Reihenfolge Helper voneinander abhängen

Unter der Haube delegiert loadHelper() an Yii’s eigenes Modulsystem:

public function loadHelper($helper)
{
    Yii::import('application.helpers.' . $helper . '_helper', true);
}

Yii::import() löst den Dot-Notation-Pfad auf und inkludiert die Datei – mit einem internen Cache, der verhindert, dass dieselbe Datei zweimal geladen wird. Das ist etwas eleganter als ein nacktes require_once, aber das grundlegende Problem bleibt dasselbe: Funktionen landen im globalen Scope, und der Entwickler trägt die Verantwortung dafür, wann und wo geladen wird.


Das Problem aus Entwicklersicht

Als Plugin-Entwickler steht man vor einer unsichtbaren API. Man weiß nicht:

  1. Welche Funktionen bereits im globalen Scope verfügbar sind, weil sie von LimeSurvey selbst geladen wurden
  2. Welche Funktionen man selbst laden muss
  3. Ob eine Funktion, die heute verfügbar ist, nach einem LimeSurvey-Update noch existiert

Das führt zu defensivem Code:

// Ist common schon geladen? Besser nochmal, sicher ist sicher.
Yii::app()->loadHelper('common');

$fieldMap = createFieldMap($surveyId, 'full', false, 0, $language);

Und zu stiller Kopplung: Das Plugin hängt von einer globalen Funktion ab, die irgendwo, irgendwann, von irgendwem geladen worden sein muss.


Das Problem aus Core-Entwicklersicht

Globale Funktionen sind schwer zu testen, schwer zu refactorn und schwer zu dokumentieren. Ein paar konkrete Konsequenzen:

Kein Composer Autoloading. Composer unterstützt durchaus das automatische Laden von prozeduralen Funktionsdateien:

{
    "autoload": {
        "files": ["application/helpers/common_helper.php"]
    }
}

LimeSurvey macht davon keinen Gebrauch. Stattdessen bleibt es bei Yii::import() – was bedeutet, dass der Entwickler weiterhin manuell entscheidet, wann welcher Helper geladen wird, anstatt dass Composer das zuverlässig und deklarativ übernimmt.

Keine Namespaces. Funktionen wie sanitize_paranoid_string() oder flattenText() leben im globalen Namespace. Namenskollisionen mit anderen Bibliotheken oder Plugins sind möglich.

Keine Typensicherheit. Ohne Klassen keine Typ-Hints auf Objektebene, kein Interface, kein Mocking in Tests.

Keine Testbarkeit. Eine globale Funktion mit Datenbankzugriff, die 650 Zeilen lang ist, ist faktisch nicht unit-testbar.


Der Weg heraus: Composer Autoloading und Service-Klassen

Die Migration von Helper-Funktionen zu modernem PHP läuft in drei Schritten, die unabhängig voneinander und schrittweise durchgeführt werden können.

Schritt 1: Funktionen in Klassen gruppieren

Der einfachste erste Schritt ist, zusammengehörige Funktionen in eine Klasse zu überführen – zunächst als statische Methoden. Das ändert nichts an der Logik, aber es gibt den Funktionen einen Kontext und einen Namespace.

// Vorher: application/helpers/common_helper.php
function sanitize_paranoid_string(string $input): string
{
    // ...
}

function flattenText(string $text, bool $stripTags = true): string
{
    // ...
}

// Nachher: app/Services/Sanitizer.php
namespace LimeSurvey\Services;

class Sanitizer
{
    public static function paranoid(string $input): string
    {
        // ...
    }

    public static function flattenText(string $text, bool $stripTags = true): string
    {
        // ...
    }
}

Bestehende Aufrufe können zunächst als Wrapper weiterleben, sodass keine Änderungen an allen Verwendungsstellen nötig sind:

// Kompatibilitäts-Wrapper in common_helper.php – temporär
function sanitize_paranoid_string(string $input): string
{
    return \LimeSurvey\Services\Sanitizer::paranoid($input);
}

Schritt 2: Composer Autoloading einrichten

Damit PHP die neuen Klassen automatisch findet, ohne manuelles Laden, trägt man den Namespace in composer.json ein:

{
    "autoload": {
        "psr-4": {
            "LimeSurvey\\": "application/"
        }
    }
}

Nach composer dump-autoload ist jede Klasse unter application/ mit dem Namespace LimeSurvey\ automatisch verfügbar – ohne loadHelper(), ohne require_once, ohne manuelle Verwaltung.

// Vorher
Yii::app()->loadHelper('common');
$clean = sanitize_paranoid_string($input);

// Nachher – kein loadHelper(), kein globaler Scope
use LimeSurvey\Services\Sanitizer;

$clean = Sanitizer::paranoid($input);

Schritt 3: Von statischen Methoden zu injizierbaren Services

Statische Methoden sind besser als globale Funktionen – aber sie sind immer noch schwer zu testen, weil man sie nicht mocken kann. Der saubere nächste Schritt ist Dependency Injection:

// Vorher: statische Methode, nicht mockbar
$clean = Sanitizer::paranoid($input);

// Nachher: injizierbarer Service
class SurveyImportService
{
    public function __construct(
        private readonly Sanitizer $sanitizer
    ) {}

    public function import(array $data): void
    {
        $clean = $this->sanitizer->paranoid($data['title']);
        // ...
    }
}

Jetzt kann Sanitizer in Tests durch ein Mock ersetzt werden. Die Abhängigkeit ist explizit, sichtbar und austauschbar.


Was Plugin-Entwickler sofort davon haben

Sobald LimeSurvey-Funktionalität in benannte, namespaced Klassen überführt ist, verändert sich die Plugin-Entwicklung grundlegend:

Discovery durch Autocompletion. Eine IDE wie PhpStorm schlägt LimeSurvey\Services\ vor und zeigt alle verfügbaren Klassen – kein Durchsuchen von Helper-Dateien mehr.

Stabile API. Eine Klasse mit definierten public Methoden ist ein Versprechen. Eine globale Funktion in einer Helper-Datei ist eine Hoffnung.

Keine versteckten Abhängigkeiten mehr. Statt zu hoffen, dass common_helper.php bereits geladen wurde, deklariert das Plugin seine Abhängigkeiten explizit:

use LimeSurvey\Services\Sanitizer;
use LimeSurvey\Services\SurveyHelper;

class MyPlugin extends PluginBase
{
    public function init(): void
    {
        $sanitizer = new Sanitizer();
        $surveyHelper = new SurveyHelper();
        // ...
    }
}

Migration ist kein Big Bang

Ein häufiges Missverständnis bei solchen Refactorings: Man müsse alles auf einmal umstellen. Das stimmt nicht.

Die drei Schritte – Klassen extrahieren, Autoloading einrichten, Dependency Injection einführen – können unabhängig voneinander und Helper-Datei für Helper-Datei durchgeführt werden. Die Kompatibilitäts-Wrapper sorgen dafür, dass bestehender Code nicht bricht.

Eine sinnvolle Reihenfolge wäre:

  1. Neue Funktionalität ausschließlich in Klassen schreiben – kein neuer Code in Helper-Dateien
  2. Häufig genutzte Helper schrittweise extrahieren, beginnend mit den am wenigsten verflochtenen
  3. Wrapper nach und nach entfernen, sobald alle Aufrufer migriert sind

Fazit

Yii::app()->loadHelper() und Yii::app()->loadLibrary() sind keine Features – sie sind technische Schuld aus einer Zeit, in der PHP keine besseren Werkzeuge hatte. Composer Autoloading, Namespaces und Dependency Injection sind seit Jahren Standard in der PHP-Welt.

Für Plugin-Entwickler bedeutet die Migration: eine klare, auffindbare API statt globaler Funktionen im Nebel. Für Core-Entwickler: testbarer, wartbarer, strukturierter Code.

Der erste Schritt ist klein: eine Helper-Datei, eine Klasse, ein Namespace. Der Rest folgt.