newDirectRequest – vollständige Webanwendungen im LimeSurvey-Plugin

Wenn ein Plugin einen eigenen HTTP-Request-Lifecycle braucht

Der direct-Event ist der CLI-Einstiegspunkt für LimeSurvey-Plugins. newDirectRequest ist sein HTTP-Pendant: ein Plugin wird über den Browser angesprochen, bekommt einen vollständigen HTTP-Request, und kann eine vollständige HTTP-Response zurückgeben.

Was LimeSurvey dabei nicht mitliefert, ist Routing. Der Event landet im Plugin – was danach passiert, ist vollständig in der Verantwortung des Plugin-Entwicklers.


Wie newDirectRequest funktioniert

Die URL, unter der ein Plugin erreichbar ist, folgt diesem Schema:

https://example.com/index.php/plugins/direct/plugin/MeinPlugin

Das Plugin abonniert den Event in init() und prüft das Target – analog zu direct, aber case-insensitiv, da URL-Parameter in der Praxis nicht immer exakt geschrieben werden:

public function init(): void
{
    $this->subscribe('newDirectRequest');
}

public function newDirectRequest(): void
{
    $event = $this->getEvent();
    $target = $event->get('target');
    if (strtolower($target) !== strtolower(get_class($this))) {
        return;
    }

    // ab hier gehört der Request dem Plugin
}

Parameter können über Yii::app()->request->getParam('name') abgerufen werden. Das reicht für einfache Fälle – ein Parameter, eine Seite. Sobald das Plugin mehrere Seiten ausliefern soll, braucht man Routing.


Das Routing-Problem

Die naive Lösung ist ein action-Parameter und ein switch:

$action = Yii::app()->request->getParam('action', 'index');

switch ($action) {
    case 'dashboard': ...
    case 'export': ...
    default: ...
}

Das funktioniert – aber es ist dieselbe implizite Zustandsmaschine, die wir aus SurveyRuntimeHelper kennen. Routen sind nicht deklariert, Parameter nicht typisiert, Middleware nicht möglich. Sobald das Plugin wächst, wächst auch der switch.

Die sauberere Lösung ist ein echter Router.


Slim als Router im Plugin

Slim PHP ist ein Micro-Framework, das HTTP-Requests entgegennimmt, sie auf Handler mappt, und Middleware unterstützt. Es kennt keinen Anwendungskontext, keine Datenbankverbindung, kein Templating – das alles bleibt in der Hand des Entwicklers. Genau das macht es für diesen Zweck geeignet.

Der entscheidende Punkt ist der basePath. Slim muss wissen, unter welchem Pfad die Anwendung erreichbar ist, damit Routen relativ dazu definiert werden können:

$app->getRouteCollector()->setBasePath(
    '/index.php/plugins/direct/plugin/' . self::getName()
);

self::getName() liefert den Plugin-Namen dynamisch – der basePath ist nie hardcoded. Routen werden dann relativ dazu definiert:

$app->get('/', function ($request, $response) use ($twig) {
    $html = $twig->render('pages/index.twig', ['title' => 'Dashboard']);
    $response->getBody()->write($html);
    return $response;
});

$app->get('/dashboard', DashboardController::class);
$app->post('/export', ExportController::class);

/index.php/plugins/direct/plugin/MeinPlugin/dashboard landet in DashboardController. POST /index.php/plugins/direct/plugin/MeinPlugin/export landet in ExportController. Middleware für Authentifizierung, Logging oder Error-Handling lässt sich über $app->add() einbinden.


Der vollständige Aufbau

Das Plugin baut die Slim-App in einer privaten Methode und übergibt sie an newDirectRequest():

public function newDirectRequest(): void
{
    $event = $this->getEvent();
    if (strtolower($event->get('target')) !== strtolower(get_class($this))) {
        return;
    }

    $app = $this->buildApp();
    $app->run();

    Yii::app()->end();
}

private function buildApp(): \Slim\App
{
    $container = $this->createContainer();

    \Slim\Factory\AppFactory::setContainer($container);
    $app = \Slim\Factory\AppFactory::create();

    $app->getRouteCollector()->setBasePath(
        '/index.php/plugins/direct/plugin/' . self::getName()
    );

    $twig = $this->createTwig();

    $app->get('/', function ($request, $response) use ($twig) {
        $html = $twig->render('pages/index.twig', ['title' => 'Hello World']);
        $response->getBody()->write($html);
        return $response;
    });

    $app->get('/dashboard', DashboardController::class);

    return $app;
}

Yii::app()->end() nach $app->run() ist wichtig: es verhindert, dass LimeSurvey nach dem Plugin-Response noch eigene Ausgaben produziert.


Eigener DI-Container

Slim erwartet einen PSR-11-kompatiblen Container. LimeSurvey bringt php-di mit – aber dessen Container ist für den LimeSurvey-Core konfiguriert. Für das Plugin wird ein eigener Container gebaut:

private function createContainer(): \Psr\Container\ContainerInterface
{
    $builder = new \DI\ContainerBuilder();
    $builder->addDefinitions(require __DIR__ . '/config/container.php');
    return $builder->build();
}

config/container.php enthält die Plugin-spezifischen Definitionen – Datenbankverbindungen, Services, Repositories. Der Container kennt den LimeSurvey-Core nur insoweit, als das Plugin ihn explizit einbindet.


Eigenes Twig

LimeSurveys Twig-Instanz ist für Umfrage-Themes konfiguriert – falsches Template-Verzeichnis, falsche Konfiguration für eine eigene Webanwendung. Das Plugin konfiguriert Twig eigenständig:

private function createTwig(): \Twig\Environment
{
    $loader = new \Twig\Loader\FilesystemLoader(
        __DIR__ . '/resources/templates'
    );

    return new \Twig\Environment($loader, [
        'cache' => false,
        'debug' => true,
    ]);
}

Templates liegen unter upload/plugins/MeinPlugin/resources/templates/ – vollständig im Plugin-Verzeichnis, vollständig in Plugin-Verantwortung.


Das Composer-Setup

Slim und psr/container gehören nicht in LimeSurveys vendor/ – sie sind Plugin-Abhängigkeiten, nicht Core-Abhängigkeiten. Das Plugin hat eine eigene composer.json und ein eigenes vendor/-Verzeichnis:

upload/plugins/MeinPlugin/
    composer.json
    vendor/
    config/
        container.php
    resources/
        templates/
    src/

Der Autoloader wird in init() geladen, bevor er gebraucht wird:

public function init(): void
{
    require __DIR__ . '/vendor/autoload.php';
    // ...
}

Ein Stolperstein: Slim 4 setzt psr/container in Version 1.0 oder 2.0 voraus. LimeSurveys eigene Abhängigkeiten können hier zu Konflikten führen, wenn man nicht aufpasst. Die Lösung ist, psr/container explizit in der Plugin-composer.json zu fordern:

{
    "require": {
        "slim/slim": "^4.0",
        "psr/container": "^1.0"
    }
}

Da das Plugin seinen eigenen vendor/-Ordner hat, sind diese Abhängigkeiten vollständig isoliert vom LimeSurvey-Core.


Die Plugin-Struktur im Überblick

upload/plugins/MeinPlugin/
    config.xml
    MeinPlugin.php
    composer.json
    vendor/
    config/
        container.php
    resources/
        templates/
            pages/
                index.twig
                dashboard.twig
    src/
        Presentation/
            Http/
                Controller/
                    DashboardController.php

Das Plugin ist eine eigenständige Webanwendung – mit Router, DI-Container, Templates und eigenen Abhängigkeiten. LimeSurvey ist der Host, nicht der Rahmen.


Warum nicht Yii-Module?

Yii 1.1 kennt ein eigenes Modul-System, das für eigenständige Webanwendungen innerhalb einer Yii-Applikation konzipiert ist. Technisch wäre das eine valide Alternative – Module haben eigene Controller, Views und Konfiguration.

Der Haken liegt in der Einstiegshürde: Ein Yii-Modul muss in application/config/config.php registriert werden – ein Eingriff, der PHP-Kenntnisse und Zugriff auf die Serverkonfiguration voraussetzt. Die Verteilung des Moduls selbst erfordert FTP-Zugriff oder einen Git-Workflow auf dem Server. Das ist kein Problem für Entwickler, aber es schließt alle aus, die LimeSurvey ohne Serverzugriff betreiben.

Das Plugin-Konstrukt ist der niedrigschwelligere Weg: Upload über das LimeSurvey-Webinterface, Aktivierung im Plugin-Manager, kein Eingriff in die Serverkonfiguration.


Fazit

newDirectRequest gibt einem Plugin einen HTTP-Einstiegspunkt. Was man daraus macht, ist offen. Die Kombination aus Slim als Router, php-di als Container und Twig als Template-Engine ergibt eine vollständige, sauber strukturierte Webanwendung – vollständig im Plugin-Verzeichnis gekapselt, vollständig in Plugin-Verantwortung.

Das ist kein offiziell dokumentierter Ansatz. Es ist eine Konsequenz aus dem, was newDirectRequest ermöglicht und was die eingesetzten Werkzeuge leisten. Wer LimeSurvey als Plattform nutzen und gleichzeitig saubere Architektur einhalten will, kommt mit diesem Modell deutlich weiter als mit einem switch über einen action-Parameter.