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

Was „aktiviert“ bedeutet (geändert in 1.2.0)

ModuleManager::isEnabled() arbeitet default-deny und liest die Datenbanktabelle modules:

// ModuleManager::isEnabled()
if (! isset($this->modules[$slug]))            return false; // not discovered
if (in_array($slug, $this->disabledModules))   return false; // crash-disabled this request
return $this->persistedEnabled()[$slug] ?? false;            // <- no row, no boot

Drei Folgen, die Sie besser kennen, bevor sie einen Nachmittag kosten:

  1. Kein Datenbank-Eintrag heißt: Das Modul bootet nicht — nicht etwa „bootet mit Voreinstellungen“. Vor 1.2.0 lebte der Abschalt-Schalter nur im Arbeitsspeicher, weshalb ein im Admin-Bereich abgeschaltetes Modul beim nächsten Neustart wiederkam. Jetzt ist der gespeicherte Zustand die Wahrheit, und ein fehlender Zustand ist ein Nein.
  2. Ein von Hand kopiertes Verzeichnis modules/<slug> tut nichts. Es wird entdeckt, es bootet nicht, und es taucht auch nicht in der Modulliste im Admin-Bereich auf — diese Liste entsteht aus Katalog und Datenbankzeilen, nie aus dem Verzeichnis. Es gibt also nicht einmal einen Schalter zum Einschalten.
  3. Zeilen entstehen auf genau zwei Wegen: durch die Installation über den Admin-Bereich und durch den Installationszweig von ModulesSync. Beachten Sie dabei, dass schneespur:modules-sync aus dem Katalog lädt — es registriert nicht, was bereits auf der Platte liegt. Ein Modul-Update rührt enabled nie an, eine bestehende Installation behält ihren Zustand also über ein Upgrade hinweg.

Für die lokale Entwicklung bedeutet das eine Zeile von Hand je Modul, an dem Sie arbeiten:

\App\Models\Module::create([
    'slug' => 'my-module',       // must match the directory name
    'version' => '1.0.0',        // must match module.json
    'enabled' => true,
    'manifest_json' => [],
    'installed_at' => now(),
]);

Ein Befehl schneespur:modules-register dafür ist geplant, aber noch nicht gebaut. Bis dahin ist php artisan tinker mit dem Ausschnitt oben der Weg hinein. Der Zustand wird nach dem ersten Lesen für die Anfrage zwischengespeichert; Module::saved und Module::deleted verwerfen ihn wieder, eine über Eloquent angelegte Zeile greift also ab der nächsten Anfrage.

Die Sequenz selbst

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 registrieren — dist/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.