Zum Hauptinhalt springen
Schneespur
Dokumentation durchblättern

Module entwickeln

Modul-Lebenszyklus

Wie Module gefunden, geladen, gebootet, aktiviert, deaktiviert, aktualisiert und entfernt werden — und worauf Sie bei Routen- und Config-Caching achten müssen.

Ein Modul durchläuft mehrere Stationen: von der Entdeckung auf der Festplatte über das Booten bis zum geordneten Entfernen. Wer diese Stationen kennt, versteht, warum bestimmte Dinge nur in boot() erlaubt sind und warum ein gecachter Routen-Stand Module unsichtbar machen kann.

Entdeckung (Discovery)

Der ModuleManager findet Module, indem er nach module.json-Dateien sucht:

modules/*/module.json

Jedes gültige Manifest wird intern mit seinem slug (dem Verzeichnisnamen) und seinem path abgelegt.

// ModuleManager::discover()
$dirs = glob($this->modulesPath . '/*/module.json');
// Je Treffer: JSON parsen, slug aus dem Verzeichnisnamen ableiten, Manifest ablegen

Der Modul-Pfad ist standardmäßig base_path('modules') und lässt sich in config/schneespur_modules.php ändern.

Boot-Sequenz

Für jedes entdeckte, aktivierte Modul führt ModuleManager::boot() aus:

  1. PSR-4-Autoloader registrieren — bildet den namespace des Moduls auf sein src/-Verzeichnis ab
  2. Übersetzungs-Namespace ergänzen — macht das lang/-Verzeichnis als {slug}::key verfügbar
  3. ServiceProvider instanziieren — die Klasse aus dem Manifest-Feld service_provider
  4. register() aufrufen — Services in den Container binden
  5. boot() aufrufen — in die Erweiterungs-Registries eintragen, Routen/Views/Events laden
  6. Modul-Assets registrierendist/manifest.json lesen, öffentlichen Symlink anlegen

Scheitert ein Schritt, wird das Modul automatisch deaktiviert und ein Diagnose-Ereignis gemeldet. Das ist Absicht: Ein kaputtes Modul soll die ganze Anwendung nicht mitreißen, sondern sich sauber selbst aus dem Weg räumen.

try {
    $provider->register();
    $provider->boot();
    $this->registerModuleAssets($slug, $manifest['path']);
} catch (\Throwable $e) {
    $this->autoDisable($slug, $e->getMessage());
    $this->reportDiagnostic('module_boot_failed', $slug, $e);
}

Aktivieren / Deaktivieren

Ein Modul aktivieren

$manager = app(ModuleManager::class);
$result = $manager->enable('my-module');

if ($result === true) {
    // Erfolg — das Modul ist jetzt aktiv
} else {
    // $result ist ein Array von Fehler-Strings (Abhängigkeits-Fehler, not_found)
}

Vor dem Aktivieren prüft der DependencyValidator:

  • alle requires-Abhängigkeiten sind aktiv und versions-kompatibel
  • kein conflicts-Eintrag ist aktiv
  • zirkuläre Abhängigkeiten werden per Tiefensuche (DFS) erkannt

Ein Modul deaktivieren

$result = $manager->disable('my-module');

if ($result === true) {
    // Erfolg — das Modul ist jetzt deaktiviert
} else {
    // $result ist ein Array von Modul-Slugs, die von diesem Modul abhängen
}

Vor dem Deaktivieren werden die umgekehrten Abhängigkeiten geprüft: Ein Modul, das ein anderes aktives Modul benötigt, lässt sich nicht deaktivieren — sonst stünde das abhängige Modul plötzlich ohne Fundament da.

Installation (aus dem Katalog)

Der Artisan-Command schneespur:modules-sync übernimmt die Remote-Installation:

  1. lädt den Modul-Katalog vom konfigurierten Server
  2. prüft die Katalog-Signaturen mit libsodium
  3. lädt neue/aktualisierte Modul-ZIPs herunter
  4. entpackt nach modules/{slug}/ über den SchneespurModuleInstaller
  5. führt Migrationen aus: php artisan migrate --path=modules/{slug}/database/migrations
  6. erkennt verwaiste Module (lokal vorhanden, aber aus dem Katalog entfernt)

Entfernen

Der Command schneespur:modules-remove {slug}:

  1. deaktiviert das Modul
  2. rollt alle Modul-Migrationen zurück
  3. räumt die Modul-Einstellungen auf (Setting::where('key', 'like', '{slug}.%'))
  4. löscht die Modul-Dateien von der Platte
  5. entfernt den öffentlichen Symlink

Update-Ablauf

Updates erledigt SchneespurModuleInstaller::update():

  1. legt eine .bak-Sicherung des aktuellen Modul-Verzeichnisses an
  2. entpackt die neue ZIP-Version
  3. rollt bei einem Fehler aus der .bak zurück
  4. führt nach erfolgreichem Entpacken die Migrationen aus

Die .bak-Sicherung ist der Grund, warum ein fehlgeschlagenes Update nicht in einem halb-entpackten Zustand endet: Entweder die neue Version steht vollständig, oder die alte kommt zurück.

Zustands-Verwaltung

Welche Module installiert und welche aktiviert sind, wird an zwei Stellen geführt:

  • Tabelle modules — der dauerhafte Datensatz mit Slug, Version, Aktiv-Flag, Manifest und Signatur-Status
  • storage/app/schneespur_modules_state.json — der Katalog-Sync-Status (ETag, Cache, Trust-Keys)

Abhängigkeits-Prüfung

Der DependencyValidator versteht Versions-Constraints in module.json:

ConstraintBeispielBedeutung
*"*"jede Version
>=X">=2.0.0"mindestens Version X
^X"^1.2.0"kompatibel (gleicher Major, >= Minor.Patch)
~X"~1.2.0"gleicher Major.Minor, >= Patch

Beispiel:

{
    "requires": {
        "notifications-core": "^1.0.0",
        "another-module": ">=2.1.0"
    },
    "conflicts": ["legacy-module"]
}

Routen, Config und Caching

Das ist die Stolperstelle, an der die meisten Modul-Probleme im Betrieb entstehen.

Module registrieren ihre Routen (Route::group()) und mischen ihre Config (mergeConfigFrom) dynamisch beim Boot ein — abhängig davon, welche Module in der Datenbank aktiviert sind. Das verträgt sich schlecht mit den statischen Caches von Laravel:

  • php artisan route:cache serialisiert nur die Routen, die im Moment des Cache-Baus registriert waren. Ein danach aktiviertes Modul ist nicht im Cache, und seine Laufzeit-Registrierung per Route::group() wird von der gecachten Routen-Sammlung überdeckt → route('admin.<slug>.*') wirft eine RouteNotFoundException (500 auf der Admin-Navigation oder bei Dashboard-Widgets).
  • php artisan config:cache friert die Config genauso ein → config('<slug>.*') liefert null, das Modul verhält sich zur Laufzeit falsch.

Es gibt zwei Wege, damit zu leben:

  1. Caches bei Modul-Änderungen leeren. Nach dem Aktivieren/Deaktivieren von Modulen config:cache und route:cache neu bauen (oder bei jedem Umschalten optimize:clear ausführen). Nötig, wenn Sie aus Performance-Gründen cachen.
  2. Routen/Config gar nicht cachen auf Deployments, die Module zur Laufzeit installieren. So macht es das offizielle Docker-/Coolify-Image: Es cached nur Views und Events.

Modul-Autoren sollten zusätzlich der Navigations-Registrierung den eigenen Routennamen als routeCheck: mitgeben (siehe das Kapitel zur Navigation). Fehlt die Route, wird der Menüpunkt dann ausgeblendet, statt einen 500-Fehler auszulösen — ein deaktiviertes oder noch nicht gebootetes Modul fällt so leise aus der Oberfläche heraus.