Das Framework ist nur ein Detail

Warum eine Zeile in einer Stellenanzeige eine Frage aufwirft, die sich nicht mit einem neuen Framework beantworten lässt

In einer LimeSurvey-Stellenanzeige für einen Full-Stack-Developer (Stand: 20. August 2026) taucht unter „Tech We Use" das „Yii-3 PHP Framework" auf – bemerkenswert, denn der aktuelle LimeSurvey-Core basiert weiterhin auf Yii 1.1. Ob damit der bestehende Core migriert werden soll, ob Yii3 nur für neue Komponenten gedacht ist oder ob lediglich einzelne Yii3-Pakete gemeint sind, wissen wir nicht. Für das Folgende spielt das auch keine große Rolle.

Denn bevor LimeSurvey entscheidet, welches Framework Yii 1.1 irgendwann ersetzt, lohnt sich eine andere Frage: Warum sollte diese Entscheidung für den Anwendungscode überhaupt von Bedeutung sein?

Unsere Arbeitsthese lautet:

Bevor LimeSurvey Yii 1.1 ersetzt, sollte es dafür sorgen, dass es Yii 1.1 nicht mehr braucht.

Ein Countdown, der nicht das eigentliche Problem ist

Yii 1.1 ist alt, aber das allein wäre noch kein Architekturproblem. Das Framework befindet sich offiziell im Maintenance Mode: keine neuen Features mehr, sondern nur noch Security-, Kompatibilitäts- und kritische Bugfixes. Die im Dezember 2025 veröffentlichte Version 1.1.32 brachte noch Unterstützung für PHP 8.4 – ein Beleg dafür, wie ernst dieser eingeschränkte Support genommen wird. Gleichzeitig nennt die offizielle Release-Planung derzeit den 31. Dezember 2026 als End-of-Life-Datum, also gerade einmal vier Monate von heute an gerechnet.

Das ist ein guter Grund, sich mit der Zukunft von LimeSurvey zu beschäftigen. Es ist aber noch kein guter Grund, LimeSurvey einfach auf das nächstbeste Framework zu portieren. Denn das eigentliche Problem ist nicht das Alter von Yii 1.1, sondern die Abhängigkeit davon – und die lässt sich messen.

Was 5.646 Aufrufe über die Kopplung verraten

Eine einfache Suche im application/-Verzeichnis von LimeSurvey findet 5.646 direkte Aufrufe von Yii::app(), verteilt auf 608 PHP-Dateien. Sechs Gruppen machen davon den größten Teil aus:

1077 Yii::app()->getConfig
1017 Yii::app()->db / getDb
 989 Yii::app()->request / getRequest
 522 Yii::app()->session
 371 Yii::app()->getController
 305 Yii::app()->user

Zusammen sind das 4.281 der 5.646 Aufrufe. Der Rest verteilt sich auf eine lange Liste kleinerer Application Components, von createUrl() über setFlashMessage() bis zum Plugin-Manager. Auch die Verteilung über die Verzeichnisse ist aufschlussreich – vor allem in controllers (2.334), helpers (1.203), models (603), views (518) und core (358) konzentriert sich die Kopplung, mit weiteren rund 300 Treffern in kleineren Verzeichnissen wie extensions, commands und libraries.

Diese Zahl bildet die tatsächliche Framework-Kopplung dabei nur teilweise ab, denn gezählt wurden ausschließlich direkte Yii::app()-Aufrufe. Abhängigkeiten durch LSActiveRecord, CActiveRecord, CController, Yii::t() und zahlreiche weitere Yii-Klassen sind darin noch gar nicht enthalten.

Das nahende End-of-Life macht deshalb vor allem eines sichtbar: wie teuer es für LimeSurvey wäre, wenn eine Infrastrukturentscheidung ausgetauscht werden müsste. Wenn die Anwendungslogik vom Framework unabhängig ist, wird das Ende eines Frameworks zu einem Problem an den Rändern der Anwendung. Wenn sie es nicht ist, wird daraus ein Migrationsprojekt für die gesamte Anwendung. Ein auslaufendes Framework sollte ein Infrastrukturproblem sein, kein Anwendungsproblem.

Das Framework gehört an den Rand

Yii::app() ist in LimeSurvey heute weit mehr als ein Router. Es ist Service Locator, Configuration Store und Zugangspunkt zu Datenbank, Request, Session, User, Controller und Cache in einem – und 2012, als LimeSurvey 2.0 komplett auf Yii umgestellt wurde, war das ein völlig nachvollziehbares Architekturmodell. Yii 1.1 war genau dafür gebaut.

Das Problem entsteht erst durch die langfristige Konsequenz: Anwendungscode kann auf Infrastruktur zugreifen, ohne seine Abhängigkeiten jemals explizit zu machen. Ein Survey sollte aber nichts von Yii wissen. Eine Quota auch nicht. Und das Speichern einer Response schon gar nicht. Ein Use Case wie „Activate Survey" braucht weder Yii::app() noch einen HTTP Request noch ein ActiveRecord, um zu funktionieren.

Das Framework gehört deshalb an den Rand:

HTTP / CLI / API → Adapter → Application Core → Ports → Infrastructure

Ob der Adapter dabei mit Yii3, Slim oder etwas anderem implementiert ist, sollte für die Anwendung keine Rolle spielen – nicht weil ein Lehrbuch das so vorsieht, sondern weil daraus eine einfache Leitfrage für jeden Modernisierungsschritt folgt: Macht diese Änderung eine technische Entscheidung austauschbarer, oder verschiebt sie die Kopplung nur an eine andere Stelle?

Warum Yii durch Yii ersetzen das falsche Ziel wäre

Nehmen wir an, LimeSurvey würde tatsächlich von Yii 1.1 auf Yii3 migriert. Dann läge eine Versuchung nahe: bestehende Abhängigkeiten einfach zu übersetzen. Aus Yii::app()->request wird der moderne Request, aus Yii::app()->db die moderne Datenbankkomponente. Das Ergebnis wäre technisch zweifellos moderner – Namespaces, Composer, PSR-Standards und Dependency Injection wären ein erheblicher Fortschritt gegenüber vielen Yii-1.1-Patterns.

Aber eine Frage bliebe unbeantwortet: Warum muss dieser Code überhaupt wissen, welches Framework verwendet wird? Wenn aus einer direkten Abhängigkeit von Yii 1.1 lediglich eine direkte Abhängigkeit von Yii3 wird, ist die Technologie ausgetauscht, aber die Kopplung bleibt bestehen – und mit ihr das Risiko, in zehn oder fünfzehn Jahren wieder vor derselben Frage zu stehen.

Persistence als Beispiel: eine Zeile mit Folgen

Yii 1.1 macht mit Active Record Zugriffe wie diesen sehr bequem:

$survey = Survey::model()->findByPk($surveyId);

Das ist praktisch, bedeutet aber auch, dass der Anwendungscode unmittelbar an den Persistence-Mechanismus gekoppelt ist. Der interessantere Schritt ist deshalb nicht, CActiveRecord durch das ORM-Konzept eines anderen Frameworks zu ersetzen, sondern die Anwendung nur noch ihre fachliche Anforderung ausdrücken zu lassen:

$survey = $surveyRepository->get($surveyId);

interface SurveyRepository {
    public function get(int $surveyId): Survey;
}

Die erste Implementierung darf dabei ruhig weiterhin Yii verwenden – ein YiiSurveyRepository, das intern auf Active Record zugreift. Entkopplung heißt nicht, dass Yii sofort verschwinden muss. Später könnte eine zweite Implementierung hinzukommen, etwa auf Basis von Doctrine DBAL, ohne dass der Anwendungscode dafür geändert werden müsste. Ob Doctrine dabei am Ende die richtige Wahl wäre, oder ob bei einem historisch gewachsenen Datenmodell wie dem von LimeSurvey ein expliziterer Zugriff über DBAL und Repositories die bessere Alternative ist, bleibt eine Detailfrage. Entscheidend ist nicht Doctrine. Entscheidend ist, dass die Anwendung nicht mehr von Active Record abhängt.

Aber bitte keine Architektur-Kathedrale

An dieser Stelle liegt ein berechtigter Einwand nahe: Aus einer 30-Zeilen-Controller-Action müssen nicht zwangsläufig ein Command, ein Handler, ein Repository, ein Interface, ein Adapter und ein DTO werden. Entkopplung ist nicht kostenlos – zusätzliche Grenzen bedeuten zusätzliche Klassen, zusätzliche Indirektion und eine höhere Einstiegshürde für Entwickler, die heute direkt Survey::model()->findByPk() schreiben können. Es wäre genauso ein Fehler, aus jedem Stück Legacy-Code eine Clean-Architecture-Kathedrale zu bauen, wie jedem Stück Legacy-Code Yii aufzuzwingen.

Die Frage ist deshalb nicht, wie viele Interfaces sich einführen lassen, sondern welche technische Entscheidung an dieser Stelle vom Anwendungscode getrennt werden soll. Der Preis dafür ist zusätzliche Indirektion. Dafür gewinnen wir testbarere Anwendungslogik und die Möglichkeit, technische Entscheidungen auszutauschen. Ein Repository lohnt sich, wenn Persistence eine relevante Abhängigkeit ist. Ein Interface nur deshalb einzuführen, weil angeblich jede Klasse eines braucht, lohnt sich nicht.

Request, User, Session und Configuration

Fast tausend der gemessenen Yii::app()-Aufrufe greifen direkt auf den Request zu, etwa so:

$surveyId = Yii::app()->request->getPost('sid');

Eine Migration wäre nicht dadurch abgeschlossen, dass daraus $request->getParsedBody()['sid'] wird. Steht dieser Code tief in einem Application Service, wurde damit lediglich Yii durch PSR-7 ersetzt – der HTTP Request sollte an dieser Stelle überhaupt nicht existieren. Der Rand der Anwendung übersetzt HTTP stattdessen in etwas, das der Use Case versteht, zum Beispiel ein einfaches Command-Objekt wie new ActivateSurvey(surveyId: $surveyId, userId: $userId). Woher diese Werte stammen, ist für den Use Case irrelevant, ob aus einem Yii-Controller, aus Slim, aus einer REST-API oder aus einem Test.

Für Yii::app()->user (305 Treffer) gilt dasselbe Prinzip: ein expliziter Benutzerkontext statt globalem Zugriff. Für Sessions gilt es noch deutlicher, denn ein Use Case muss normalerweise nicht wissen, ob sein Zustand aus einer PHP-Session oder einem Token stammt.

Am interessantesten ist Yii::app()->getConfig() – mit 1.077 Treffern der häufigste Aufruf unserer Messung. Es wäre leicht, ihn durch $config->get('something') zu ersetzen, aber damit wäre wenig gewonnen, weil es letztlich nur ein anderer globaler Zugriff ist. Besser ist es, wenn ein Service explizit deklariert, was er tatsächlich braucht:

final class LoginThrottleService {
    public function __construct(
        private readonly int $maxLoginAttempt,
        private readonly int $timeOutTime,
    ) {}
}

Ob diese Werte aus einer PHP-Datei, einer Datenbank oder Yii::app()->getConfig() stammen, weiß der Service dann nicht mehr. Er kennt nur, was er tatsächlich benötigt – in diesem Fall, wie viele fehlgeschlagene Login-Versuche erlaubt sind (maxLoginAttempt) und nach welcher Zeit eine Session abläuft (timeOutTime).

Dependency Injection allein löst das Problem nicht

LimeSurvey verwendet heute bereits PHP-DI, eine gute Voraussetzung für eine schrittweise Entkopplung. Aber ein DI-Container erzeugt nicht automatisch entkoppelte Architektur: Man kann Yii::app()->db problemlos durch $container->get(Connection::class) ersetzen, und dann heißt der globale Service Locator nur anders und ist moderner implementiert. Die Abhängigkeit bleibt implizit.

Besser ist Constructor Injection, bei der der Container die Objekte zusammenbaut, der Anwendungscode ihn dafür aber nicht selbst befragen muss. Dieselbe Leitfrage hilft auch hier: Ist die Entscheidung dadurch austauschbarer geworden, oder haben wir die Kopplung nur von Yii::app() nach $container->get() verschoben? Moderne Technologie erzeugt eben nicht automatisch moderne Architektur.

2012 war Yii eine pragmatische Entscheidung

2012 fiel die Wahl auf Yii 1.1 nicht allein aus architektonischer Überzeugung, sondern auch wegen einer Lizenzfrage beim eigentlich geplanten Umstieg auf CodeIgniter – die Geschichte dazu haben wir bereits einmal erzählt.

Ausgelöst wurde die Entscheidung also von einer Lizenzfrage, nicht von einem Architekturvergleich. Das Ergebnis war trotzdem ein Framework, das zur damaligen LimeSurvey-Codebasis passte: MVC statt prozeduralem PHP, ActiveRecord statt handgeschriebener SQL-Strings, eine wachsende Community im Rücken. Eine Entscheidung kann pragmatisch erzwungen und gleichzeitig technisch tragfähig sein – genau das war 2012 der Fall.

Interessanter als die Frage, wie bewusst diese Entscheidung damals getroffen wurde, ist deshalb eine andere: Was hat sie seither gekostet, und wie verhindern wir, dass die nächste Frameworkentscheidung genauso tief in die Anwendung einwächst?

Modernisierung ohne Big Bang

Niemand muss LimeSurvey deshalb neu schreiben. Yii 1.1 darf bleiben, bestehende Controller dürfen weiterlaufen, bestehende ActiveRecords dürfen weiterhin Daten laden. Das Ziel ist nicht, grep "Yii::app()" auf null zu bringen – das wäre eine kosmetische Metrik und könnte sogar zu schlechterem Code führen, wenn Yii::app() nur durch einen anderen globalen Zugriff ersetzt wird. Interessanter ist ein anderes Ziel: null Yii-Abhängigkeiten im neuen Application Code.

Dieses Ziel lässt sich sogar technisch absichern. Ein Werkzeug wie Deptrac könnte beispielsweise verhindern, dass neue Application- oder Domain-Klassen wieder von Yii abhängen. Der Legacy-Code darf seine bestehenden Abhängigkeiten behalten – aber die Architektur bekommt eine Ratsche: Neue Abhängigkeiten in die falsche Richtung kommen nicht mehr hinzu.

Neue Funktionalität wird also nicht mehr unmittelbar an Yii gekoppelt. Wird bestehende Funktionalität ohnehin verändert, entsteht an dieser Stelle eine Grenze – ein alter Yii-Controller kann dabei problemlos einen neuen Application Service aufrufen, über einen Port, hinter dem zunächst weiterhin ein Yii-Adapter steckt. Der Use Case bleibt derselbe, wenn später ein zweiter Adapter dazukommt, etwa auf Basis von Slim. Dasselbe Muster funktioniert für Helper: Eine globale Legacy-Funktion bleibt bestehen und delegiert intern an einen neuen Service, während alte Plugins weiter funktionieren und neuer Code die globale Funktion irgendwann gar nicht mehr kennt.

So wird Legacy-Code nicht auf einmal ersetzt, sondern nach und nach ausgehöhlt – Strangler Architecture im eigentlichen Sinne des Wortes.

Yii3? Vielleicht.

Damit sind wir zurück bei der Stellenanzeige. Yii3 kann für LimeSurvey durchaus eine legitime Entscheidung sein – vielleicht für einzelne Pakete, vielleicht für neue HTTP-Komponenten, vielleicht ist in fünf Jahren ohnehin wieder Slim die bessere Wahl. Genau das wäre der Erfolg: nicht heute das Framework auszuwählen, das LimeSurvey die nächsten vierzehn Jahre tragen soll.

2012 war das eine nachvollziehbare Entscheidung. Vierzehn Jahre später diskutieren wir darüber, wie tief sie inzwischen in der Anwendung verankert ist. Eine Modernisierung ist die Chance, diesen Zyklus zu durchbrechen – nicht indem Yii 1.1 durch das vermeintlich richtige Framework für die nächsten vierzehn Jahre ersetzt wird, sondern indem LimeSurvey so gebaut wird, dass diese Entscheidung irgendwann keine besonders wichtige mehr ist. Dann steht in der nächsten Stellenanzeige vielleicht wieder ein neues Framework, und niemand muss sich mehr fragen, was das für den Code bedeutet.

Das Framework ist nur ein Detail.