Zum Hauptinhalt springen
Schneespur
Dokumentation durchblättern

Module entwickeln

Registries-Referenz

Die vollständige Referenz aller Erweiterungs-Registries mit Methoden und Beispielen — von Navigation und Dashboard-Widgets über Benachrichtigungs-Kanäle bis zu Wetter, Speicher, PDF und geplanten Aufgaben.

Die Registries sind das Herz des Modulsystems: Über sie meldet ein Modul seine Fähigkeiten an, ohne den Core anzufassen. Diese Seite ist die Nachschlage-Referenz — Sie müssen sie nicht am Stück lesen, sondern springen zu der Registry, die Sie gerade brauchen.

Alle Erweiterungs-Registries erweitern App\Services\Extension\ExtensionRegistry (oder folgen deren Muster). Die Basisklasse stellt bereit:

abstract class ExtensionRegistry
{
    protected array $items = [];

    public function register(string $slug, mixed $entry): void;  // Eintrag hinzufügen (warnt bei Überschreiben)
    public function resolve(string $slug): mixed;                 // Eintrag oder null
    public function all(): array;                                 // alle Einträge
    public function has(string $slug): bool;                      // Existenz prüfen
    public function remove(string $slug): void;                   // entfernen
}

Alle Registries sind im AppServiceProvider als Singletons registriert. Lösen Sie sie aus dem Container auf:

$registry = app(NavigationRegistry::class);
// oder per Constructor-Injection in Ihrem ServiceProvider

Klasse: App\Services\Extension\NavigationRegistry

Verwaltet die Navigation der Admin-Seitenleiste.

Methoden

// Eine Navigationsgruppe (Abschnitts-Überschrift) hinzufügen
addGroup(string $key, string $label, int $order = 100): void

// Alle Gruppen nach order sortiert holen
getGroups(): array

// Einen Navigationspunkt hinzufügen
addItem(
    string $group,        // Gruppen-Key (z. B. 'system', 'stammdaten')
    string $slug,         // eindeutige Item-Kennung
    string $label,        // Anzeigetext
    string $route,        // Laravel-Routenname
    string $icon,         // ROHE SVG-Pfad-Geometrie (Inhalt von d=), bei mehreren
                          // Pfaden '||'-getrennt — KEIN 'heroicon-o-*'-Name.
    int $order = 100,     // Sortierung innerhalb der Gruppe
    ?string $permission = null,    // benötigte Berechtigung
    ?string $routeCheck = null,    // Routenname; Item wird ausgeblendet, falls !Route::has() — unbedingt setzen!
    ?string $activePattern = null, // Routen-Muster für den Aktiv-Zustand
    ?string $badge = null,         // Badge-HTML/-Text
): void

// Gruppierte Items, gefiltert nach den Berechtigungen des Nutzers
getItems(?User $user = null): array

Core-Navigationsgruppen

KeyLabelOrder
topDashboard0
stammdatenStammdaten10
einsaetzeEinsätze20
auswertungenAuswertungen30
systemSystem40

Beispiel

$nav = app(NavigationRegistry::class);

$nav->addItem(
    group: 'system',
    slug: 'my-module-settings',
    label: __('my-module::messages.settings'),
    route: 'admin.my-module.settings',
    icon: 'heroicon-o-cog',
    order: 200,
    permission: 'settings.view',
);

Setzen Sie routeCheck: auf den eigenen Routennamen. Ist die Route — etwa weil das Modul gerade deaktiviert ist — nicht vorhanden, verschwindet der Menüpunkt sauber, statt einen 500-Fehler auszulösen. Warum das gerade beim Routen-Caching wichtig ist, steht unter Modul-Lebenszyklus.


PortalNavigationRegistry

Klasse: App\Services\Extension\PortalNavigationRegistry

Verwaltet die Navigation des Kundenportals (Desktop- und Mobil-Menü) — das Gegenstück zur NavigationRegistry für die Kundenseite. Hinzugekommen in 1.1.3.

Unterschiede zur Admin-NavigationRegistry:

  • läuft gegen den customer-Guard. Kunden haben keine Gates/Berechtigungen, daher steuert eine optionale condition-Closure die Sichtbarkeit, die den Customer erhält.
  • label speichert einen Übersetzungs-Key, keinen fertigen Text. Die Portal-Locale wird pro Anfrage (pro Kunde) in EnsureCustomer gesetzt — nach dem Boot. Das Layout löst den Key daher erst beim Rendern mit __() auf. (Die Admin-Registry speichert bereits übersetzte Labels.)
  • kein Icon — Portal-Tabs sind reiner Text.

Methoden

addItem(
    string $slug,                  // eindeutige Item-Kennung
    string $label,                 // ÜBERSETZUNGS-KEY (wird beim Rendern mit __() aufgelöst)
    string $route,                 // Laravel-Routenname, z. B. 'portal.documents.index'
    int $order = 100,              // Sortierung
    ?string $activePattern = null, // Routen-Muster für Aktiv-Zustand (Default: $route)
    ?\Closure $condition = null,   // optionale fn(Customer $c): bool — false blendet aus
): void

getItems(?Customer $customer = null): array

Beispiel

$nav = app(PortalNavigationRegistry::class);

$nav->addItem(
    slug: 'contracts',
    label: 'my-module::portal.nav_contracts', // Übersetzungs-Key
    route: 'portal.contracts.index',
    order: 25,
    activePattern: 'portal.contracts.*',
    condition: fn (\App\Models\Customer $c) => true, // optionale Sichtbarkeit pro Kunde
);

Die Items erscheinen automatisch in der Desktop-Navigation und im Mobil-Menü.


LocaleRegistry

Klasse: App\Services\Extension\LocaleRegistry

Registry der verfügbaren UI-Sprachen. Der Core registriert de und en; Sprachpaket-Module ergänzen weitere Locales (z. B. cs). Hinzugekommen in 1.1.3.

Methoden

add(string $code, string $label): void   // Locale registrieren, z. B. add('cs', 'Čeština')
codes(): array                           // ['de', 'en', ...] — für Validierung (Rule::in)
labels(): array                          // ['de' => 'Deutsch', ...] — für Sprach-Auswahl
has(string $code): bool                  // zur Absicherung von App::setLocale()

Beispiel

// In der ServiceProvider::boot() eines Sprachpaket-Moduls
app(LocaleRegistry::class)->add('cs', 'Čeština');

Boot-Zeitpunkt: Modul-Locales werden während ModuleManager::boot() registriert; die anwendungsweite default_locale wird erst danach angewendet. So kann eine vollständig tschechische Installation (default_locale = cs) über eine von einem Modul bereitgestellte Locale aufgelöst werden.


PublicHomepageRegistry

Klasse: App\Services\Extension\PublicHomepageRegistry

Lässt ein Modul die öffentliche Wurzel-URL / übernehmen und dort eine öffentliche Startseite ausliefern, statt auf den Login umzuleiten. Zugleich ist diese Registry die einzige Quelle der Wahrheit dafür, welche Seiten Suchmaschinen sehen dürfen. Sie ist die Grundlage für ein geplantes Frontpage-Modul — die Registry selbst ist vorhanden, das Modul, das sie befüllt, ist noch nicht verfügbar. Hinzugekommen in 1.1.6.

Methoden

register(callable $handler): void   // "/" bedienen; handler liefert Response|View|string. Markiert "/" automatisch als crawlbar.
has(): bool
render(): mixed

allowCrawling(string ...$paths): void  // weitere öffentliche Seiten anmelden, z. B. '/leistungen'
crawlablePaths(): array                 // list<string>
isCrawlable(string $path): bool         // Wurzel matcht exakt; Abschnitte matchen Unterpfade, nicht bloße Präfixe

setSitemapUrl(string $url): void        // wird in der robots.txt angekündigt (das Modul liefert die Sitemap selbst)
sitemapUrl(): ?string

Beispiel

// In der ServiceProvider::boot() des Frontpage-Moduls
$home = app(PublicHomepageRegistry::class);
$home->register(fn () => view('frontpage::homepage'));     // bedient "/"
$home->allowCrawling('/leistungen', '/impressum');         // weitere öffentliche Seiten
$home->setSitemapUrl(url('/sitemap.xml'));

Der Grundgedanke ist Default-Deny: Alles, was nicht ausdrücklich angemeldet wurde, bleibt privat. Eine Installation legt also nichts versehentlich offen, nur weil ein Modul aktiv ist — erst register() macht / öffentlich, erst allowCrawling() gibt weitere Pfade frei. Wichtig ist außerdem das Auflösen zur Anfragezeit: Die /-Route fragt die Registry bei jedem Aufruf ab, statt das Ergebnis beim Booten festzuschreiben. So übersteht die Konfiguration auch ein route:cache, das andernfalls eine zur Boot-Zeit getroffene Entscheidung einfrieren würde.

Hier steht bewusst nur die Registry-API. Das vollständige Indexierungs-Modell — die robots.txt-Route, der X-Robots-Tag-Header und das <meta name="robots"> — sowie das Narrativ und die Anleitung zur öffentlichen Startseite liegen im Kapitel Öffentliche Startseite & SEO. Wie die /-Route in das übrige Routing eingebettet ist, beschreibt Routen & APIs.


JobTypeRegistry

Klasse: App\Services\Extension\JobTypeRegistry

Macht die Menge der Einsatz- bzw. Tätigkeitstypen erweiterbar. Der Core registriert beim Booten seine eingebauten JobType-Enum-Fälle; ein Modul ergänzt eigene Typen, ohne den Core anzufassen. Weil ein Einsatz seinen type als String speichert, braucht ein neuer Typ keine Migration. Die Monatsstatistik wertet die Einsätze pro Typ aus, sodass ein modul-eigener Typ ohne weiteres Zutun in den Auswertungen erscheint. Grundlage ist sie unter anderem für ein geplantes Grünpflege-Modul — die Registry ist vorhanden, das Modul noch nicht verfügbar. Hinzugekommen in 1.1.6.

Methoden

registerType(string $value, string $labelKey, int $order = 100, ?string $module = null): void
hasType(string $value): bool
values(): array          // string[] der Typ-Werte, sortiert
types(): array           // App\ValueObjects\JobTypeValue[]
label(string $value): string  // übersetztes Label via __() des registrierten label_key

Beispiel

app(JobTypeRegistry::class)->registerType(
    value: 'gruenpflege',
    labelKey: 'gruenpflege::job.type_gruenpflege',
    order: 100,
    module: 'gruenpflege',
);

Der labelKey ist absichtlich ein Übersetzungs-Key und kein fertiger Text: label() löst ihn erst beim Anzeigen mit __() auf, sodass derselbe Typ in jeder UI-Sprache passend erscheint. Geben Sie module: mit, damit sich der Typ einem Modul zuordnen und beim Entfernen wieder sauber abräumen lässt.


LifecycleFieldRegistry

Klasse: App\Services\Extension\LifecycleFieldRegistry

Lässt Module zusätzliche Felder in die Fahrer-Lebenszyklus-Abläufe einhängen (Schichtbeginn/-ende, Einsatzbeginn/-ende) — also modul-eigene Daten erfassen, validieren und speichern, ohne die Core-Formulare zu patchen. Grundlage ist sie unter anderem für geplante Lager- und Grünpflege-Module — die Erfassungspunkte existieren, die Module sind noch nicht verfügbar. Die vier Momente sind das Enum App\Enums\LifecyclePoint. Hinzugekommen in 1.1.6.

FallWert
LifecyclePoint::ShiftStartshift.start
LifecyclePoint::ShiftEndshift.end
LifecyclePoint::JobStartjob.start
LifecyclePoint::JobEndjob.end

Methoden

registerField(LifecyclePoint $point, string $slug, array $contribution): void
contributions(LifecyclePoint $point, ?Authenticatable $user = null): array  // sortiert, nach Berechtigung gefiltert
rules(LifecyclePoint $point, ?Authenticatable $user = null): array          // zusammengeführte Validierungsregeln
fieldKeys(LifecyclePoint $point): array                                     // aus dem Request zu lesende Keys
render(LifecyclePoint $point, ?Authenticatable $user = null): string        // verkettetes View-HTML
persist(LifecyclePoint $point, Model $entity, array $validated, User $user): void

Contribution-Array

[
    'view' => 'lager::fields.salt_used',   // ins Formular gerenderte Blade-View (erhält $user)
    'rules' => ['lager_salt_used' => 'nullable|numeric|min:0'],  // Keys mit Namespace versehen!
    'persist' => fn (Model $entity, array $validated, User $user) => /* ... */,  // Closure | class-string | LifecycleFieldHandler
    'order' => 100,
    'permission' => null,   // optionales Gate; gilt für render(), rules() UND persist()
]

Rendern — die @lifecycleFields-Direktive

Die Core-Formulare rendern den Einschubpunkt über eine Blade-Direktive:

@lifecycleFields(\App\Enums\LifecyclePoint::ShiftStart)

Beispiel

app(LifecycleFieldRegistry::class)->registerField(
    LifecyclePoint::JobEnd,
    'lager_salt_used',
    [
        'view' => 'lager::fields.salt_used',
        'rules' => ['lager_salt_used' => 'nullable|numeric|min:0'],
        'persist' => \Schneespur\Module\Lager\Lifecycle\RecordSaltUsage::class,
        'permission' => 'lager.record',
    ],
);

Drei Punkte sind in der Praxis entscheidend — und erklären, warum die API so geschnitten ist. Erstens: Geben Sie Ihren Feld-Keys einen Namespace (lager_salt_used, nicht notes). Mehrere Module tragen in denselben Lebenszyklus-Moment ein; ihre Beiträge werden zusammengeführt, und bei gleichem Key gewinnt der letzte. Ein eigener Präfix verhindert, dass sich zwei Module gegenseitig überschreiben. Zweitens: Ein permission-Key sichert nicht nur die Anzeige, sondern auch persist() ab — ein Feld, das ein Nutzer nicht sehen darf, kann er auch nicht schreiben. Drittens: Manuelle Einsätze umgehen die Lebenszyklus-Hooks — die Felder werden ausschließlich in den Fahrer-Abläufen (Schicht/Einsatz) erfasst.

Die @lifecycleFields-Direktive ist bei den Frontend-Assets beschrieben; das Interface, das ein persist-Handler implementiert, finden Sie unter Interfaces (LifecycleFieldHandler).


DashboardWidgetRegistry

Klasse: App\Services\Extension\DashboardWidgetRegistry

Verwaltet die Karten auf dem Admin-Dashboard.

Methoden

registerWidget(string $slug, array $config): void

// Sichtbare Widgets für einen Nutzer (nach Berechtigung + condition gefiltert, nach order sortiert)
getWidgets(?User $user = null): array

Config-Array

[
    'slug' => 'my-widget',           // wird aus dem ersten Parameter gesetzt
    'label' => 'Widget Title',       // Anzeige-Label
    'view' => 'my-module::widgets.card', // zu rendernde Blade-View
    'dataCallback' => fn () => [...],    // optional: Callable, das View-Daten liefert
    'order' => 100,                  // Sortierung (kleiner = weiter oben)
    'permission' => 'dashboard.view', // optional: benötigte Berechtigung
    'condition' => fn () => true,    // optional: Callable, false blendet aus
    'size' => 'full',               // 'full' oder 'half'
]

Beispiel

$widgets = app(DashboardWidgetRegistry::class);

$widgets->registerWidget('telegram-status', [
    'label' => 'Telegram Status',
    'view' => 'telegram::widgets.status',
    'dataCallback' => fn () => ['connected' => TelegramService::isConnected()],
    'order' => 150,
    'size' => 'half',
    'permission' => 'settings.view',
]);

Das dataCallback wird erst beim Rendern ausgeführt — teure Abfragen laufen also nur, wenn das Widget für den jeweiligen Nutzer überhaupt sichtbar ist.


FilterRegistry

Klasse: App\Services\Extension\FilterRegistry

Hook-/Filter-System im WordPress-Stil. Registrieren Sie Callbacks auf benannte Hooks; der Core wendet sie an definierten Stellen an.

Methoden

register(string $hook, callable $callback, int $priority = 100): void

apply(string $hook, mixed $value, mixed ...$context): mixed

Priorität

Kleinere Prioritätszahlen laufen zuerst. Bei gleicher Priorität entscheidet die Einfüge-Reihenfolge. Wirft ein Callback eine Ausnahme, wird der vorige Wert wiederhergestellt und eine Warnung protokolliert — ein fehlerhafter Filter kann die Pipeline so nicht zum Absturz bringen.

Die verfügbaren Hooks mit ihren Signaturen sind im Kapitel Filter-Hooks aufgeführt.


SlotRegistry

Klasse: App\Services\Extension\SlotRegistry

Template-Einschub-System — fügt Inhalt an vordefinierten Punkten in den Blade-Layouts ein.

Methoden

// Eine View an einen Slot anhängen (mehrere Anhänge werden der Reihe nach gerendert)
append(
    string $slotName,
    string $viewPath,
    array $data = [],
    int $order = 100,
    ?string $permission = null,
): void

// Einen Slot vollständig ersetzen (bei mehreren gewinnt der letzte)
replace(
    string $slotName,
    string $viewPath,
    array $data = [],
    ?string $permission = null,
): void

// Den Inhalt eines Slots rendern (von der @extensionSlot-Blade-Direktive aufgerufen)
render(string $slotName, ?Authenticatable $user = null): string

// Alle registrierten Slot-Namen auflisten
getSlotNames(): array

Welche Slot-Namen es je Layout gibt, listet getSlotNames() zur Laufzeit auf; die verfügbaren Einschubpunkte beschreibt das Kapitel Template-Slots.


PermissionRegistry

Klasse: App\Services\Extension\PermissionRegistry

Registriert Berechtigungen, die Rollen zugewiesen werden können.

Methoden

registerPermission(
    string $slug,         // z. B. 'my-module.manage'
    string $label,        // lesbares Label
    string $group,        // Berechtigungs-Gruppe
    ?string $module = null, // Modul-Slug (für Aufräumen beim Entfernen)
): void

getPermissions(): array          // alle Berechtigungen
getByGroup(string $group): array // nach Gruppe filtern
getByModule(string $module): array  // nach Modul filtern
removeByModule(string $module): void // alle eines Moduls entfernen

Beispiel

$permissions = app(PermissionRegistry::class);

$permissions->registerPermission(
    slug: 'telegram.manage',
    label: 'Manage Telegram settings',
    group: 'telegram',
    module: 'telegram',
);

Geben Sie module: immer mit — dadurch lassen sich beim Entfernen des Moduls alle seine Berechtigungen in einem Zug aufräumen.


RoleTemplateRegistry

Klasse: App\Services\Extension\RoleTemplateRegistry

Vordefinierte Rollen-Konfigurationen, die Admins beim Anlegen von Rollen anwenden können.

Methoden

registerTemplate(
    string $slug,
    string $name,
    string $description,
    array $permissions,     // Array von Berechtigungs-Slugs
    ?string $module = null,
): void

getTemplates(): array
getByModule(string $module): array
removeByModule(string $module): void

Beispiel

$templates = app(RoleTemplateRegistry::class);

$templates->registerTemplate(
    slug: 'accountant',
    name: 'Buchhaltung',
    description: 'Zugriff auf Dokumente und Rechnungen',
    permissions: ['documents.view', 'invoices.view', 'invoices.export'],
    module: 'documents',
);

DispatchStrategyRegistry

Klasse: App\Services\Extension\DispatchStrategyRegistry

Verwaltet Algorithmen zur Einsatz-Zuteilung.

Methoden

register(string $slug, string $class): void      // class-string<DispatchStrategyInterface>
resolve(?string $slug = null): DispatchStrategyInterface  // aus dem Container; Fallback 'manual'
availableStrategies(): array   // array<string, array{name: string}>
activeSlug(): string           // aktuell konfigurierte Strategie

Die aktive Strategie liegt in der Einstellung dispatch_strategy. Default ist 'manual'.


NotificationChannelRegistry

Klasse: App\Services\Notification\NotificationChannelRegistry

Verwaltet Benachrichtigungs-Kanäle (E-Mail, Telegram, SMS usw.).

Methoden

register(string $slug, string $channelClass): void  // class-string<NotificationChannelInterface>

// Benachrichtigung an alle aktivierten Kanäle senden (mit Filter-Hook)
dispatch(Job $job, string $type, array $context): array

// Instanziierte, aktivierte Kanäle holen
enabledChannels(): array  // array<string, NotificationChannelInterface>

dispatch() wendet den Filter schneespur.job.notification.channels an — Module können darüber beeinflussen, welche Kanäle eine bestimmte Benachrichtigung erhalten.


TwoFactorMethodRegistry

Klasse: App\Services\Extension\TwoFactorMethodRegistry

Verwaltet Verfahren zur Zwei-Faktor-Authentifizierung.

Methoden

registerMethod(string $slug, string $methodClass): void  // class-string<TwoFactorMethodInterface>
getMethods(): array                    // alle registrierten Klassen
getAvailableMethods(Container $container): array  // instanziierte Methoden
getByModule(string $module): array     // nach Modul-Namespace filtern
removeByModule(string $module): void

BackupTargetRegistry

Klasse: App\Services\Backup\BackupTargetRegistry

Verwaltet Backup-Speicherziele.

Methoden

register(string $slug, string $class): void  // class-string<BackupTargetInterface>
resolve(?string $slug = null): BackupTargetInterface  // Fallback 'local'
availableTargets(): array  // array<string, array{label, configured}>
activeSlug(): string

StorageBackendRegistry

Klasse: App\Services\Storage\StorageBackendRegistry

Verwaltet Datei-Speicher-Backends mit automatischem Rückfall auf den lokalen Speicher.

Methoden

register(string $slug, string $class): void  // class-string<StorageBackendInterface>
resolve(?string $slug = null): StorageBackendInterface
availableBackends(): array  // array<string, array{label, configured}>
activeSlug(): string

// Lesen mit automatischem Rückfall auf lokal, falls das aktive Backend nichts findet
retrieveWithFallback(string $relativePath): ?string

// URL mit automatischem Rückfall auf lokal
urlWithFallback(string $relativePath): string

Der eingebaute Fallback ist der Grund, warum eine vorübergehend nicht erreichbare Remote-Ablage nicht sofort zu fehlenden Dateien führt — gelesen wird notfalls aus dem lokalen Speicher.


WeatherProviderRegistry

Klasse: App\Services\Weather\WeatherProviderRegistry

Verwaltet Wetter-Datenquellen.

Methoden

register(string $slug, string $class): void  // class-string<WeatherProviderInterface>
resolve(?string $slug = null): WeatherProviderInterface
availableProviders(): array  // array<string, array{name, requires_api_key}>
activeSlug(): string

PdfRendererRegistry

Klasse: App\Services\Pdf\PdfRendererRegistry

Verwaltet PDF-Engines (Standard: DomPDF).

Methoden

register(string $slug, string $class): void  // class-string<PdfRendererInterface>
resolve(?string $slug = null): PdfRendererInterface

ReportFormatRegistry

Klasse: App\Services\Report\ReportFormatRegistry

Verwaltet Export-Formate (PDF, CSV und eigene Formate).

Methoden

register(string $slug, string $class): void  // class-string<ReportFormatInterface>
resolve(string $slug): ?ReportFormatInterface
availableFormats(): array

ScheduledTaskRegistry

Klasse: App\Services\Scheduler\ScheduledTaskRegistry

Verwaltet Cron-Aufgaben.

Methoden

register(string $slug, string $class): void  // class-string<ScheduledTaskInterface>
resolve(string $slug): ?ScheduledTaskInterface
enabledTasks(): array            // instanziierte, aktivierte Aufgaben
recordRun(string $slug, string $status, ?string $error, int $durationMs): void
lastRun(string $slug): ?object   // letzter Ausführungs-Datensatz
allWithStatus(): array           // alle Aufgaben mit ihrem letzten Lauf

DiagnosticReporterRegistry

Klasse: App\Services\Diagnostic\DiagnosticReporterRegistry

Verwaltet Backends fürs Fehler-/Crash-Reporting.

Methoden

register(string $slug, string $class): void  // class-string<DiagnosticReporterInterface>

Der DiagnosticManager verteilt an alle aktivierten Reporter. Die Payloads werden vor dem Versand vom DiagnosticPayloadSanitizer bereinigt — sensible Felder verlassen die Installation also nicht ungefiltert.


ModuleAssetRegistry

Klasse: App\Services\Extension\ModuleAssetRegistry

Verwaltet CSS-/JS-Assets aus Modulen.

Methoden

registerAssets(string $slug, string $modulePath): void  // liest dist/manifest.json
getCss(): string[]  // URLs aller registrierten CSS-Dateien
getJs(): string[]   // URLs aller registrierten JS-Dateien
all(): array        // alle Assets mit Typ, URL, Slug

ModuleApiRegistrar

Klasse: App\Services\Extension\ModuleApiRegistrar

Erstellt präfixierte, authentifizierte API-Routengruppen für Module.

Methoden

routes(string $slug, int $version, Closure $callback): void

Legt Routen unter /api/mod/{slug}/v{version}/ an, mit der Middleware module.api:{slug} und dem Routennamen-Präfix module.{slug}.api.v{version}..


Ein konkretes Modul, das Navigation, Dashboard-Widget, Einstellungen und Routen zusammen nutzt, bauen Sie im Schnelleinstieg.