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:
- 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.
- 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. - Zeilen entstehen auf genau zwei Wegen: durch die Installation über den Admin-Bereich
und durch den Installationszweig von
ModulesSync. Beachten Sie dabei, dassschneespur:modules-syncaus dem Katalog lädt — es registriert nicht, was bereits auf der Platte liegt. Ein Modul-Update rührtenablednie 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:
- PSR-4-Autoloader registrieren — bildet den
namespacedes Moduls auf seinsrc/-Verzeichnis ab - Übersetzungs-Namespace ergänzen — macht das
lang/-Verzeichnis als{slug}::keyverfügbar - ServiceProvider instanziieren — die Klasse aus dem Manifest-Feld
service_provider register()aufrufen — Services in den Container bindenboot()aufrufen — in die Erweiterungs-Registries eintragen, Routen/Views/Events laden- Modul-Assets registrieren —
dist/manifest.jsonlesen, ö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:
- lädt den Modul-Katalog vom konfigurierten Server
- prüft die Katalog-Signaturen mit libsodium
- lädt neue/aktualisierte Modul-ZIPs herunter
- entpackt nach
modules/{slug}/über denSchneespurModuleInstaller - führt Migrationen aus:
php artisan migrate --path=modules/{slug}/database/migrations - erkennt verwaiste Module (lokal vorhanden, aber aus dem Katalog entfernt)
Entfernen
Der Command schneespur:modules-remove {slug}:
- deaktiviert das Modul
- rollt alle Modul-Migrationen zurück
- räumt die Modul-Einstellungen auf (
Setting::where('key', 'like', '{slug}.%')) - löscht die Modul-Dateien von der Platte
- entfernt den öffentlichen Symlink
Update-Ablauf
Updates erledigt SchneespurModuleInstaller::update():
- legt eine
.bak-Sicherung des aktuellen Modul-Verzeichnisses an - entpackt die neue ZIP-Version
- rollt bei einem Fehler aus der
.bakzurück - 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:
| Constraint | Beispiel | Bedeutung |
|---|---|---|
* | "*" | 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:cacheserialisiert nur die Routen, die im Moment des Cache-Baus registriert waren. Ein danach aktiviertes Modul ist nicht im Cache, und seine Laufzeit-Registrierung perRoute::group()wird von der gecachten Routen-Sammlung überdeckt →route('admin.<slug>.*')wirft eineRouteNotFoundException(500 auf der Admin-Navigation oder bei Dashboard-Widgets).php artisan config:cachefriert die Config genauso ein →config('<slug>.*')liefertnull, das Modul verhält sich zur Laufzeit falsch.
Es gibt zwei Wege, damit zu leben:
- Caches bei Modul-Änderungen leeren. Nach dem Aktivieren/Deaktivieren von Modulen
config:cacheundroute:cacheneu bauen (oder bei jedem Umschaltenoptimize:clearausführen). Nötig, wenn Sie aus Performance-Gründen cachen. - 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.