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.


RegionRegistry

Klasse: App\Services\Format\RegionRegistry

Regionsvorlagen, die die neun Format-Einstellungen auf einen Schlag füllen. Der Core liefert de, at, ch, us, ca und gb; ein Sprachpaket-Modul meldet seine eigene Vorlage auf demselben Weg an, auf dem es auch eine Locale anmeldet — lang-cs kann Tschechien also mitbringen, ohne dass der Core davon weiß. Hinzugekommen in 1.2.0. Die Einzelheiten stehen unter Formate & Einheiten.

Methoden

add(RegionPreset $preset): void          // Vorlage registrieren
get(string $code): ?RegionPreset         // null bei unbekanntem Code
labels(): array                          // ['de' => 'Deutschland', ...] — für die Auswahl

RegionPreset (App\Services\Format\RegionPreset) ist ein unveränderliches Wertobjekt mit code, label, currencyCode, currencyPosition, numberFormat, dateFormat, timeFormat, unitDistance, unitTemperature, unitWeight sowie toSettings(): array<string,string>.

Beispiel

// In der ServiceProvider::boot() eines Sprachpaket-Moduls
app(RegionRegistry::class)->add(new RegionPreset(
    code: 'cz',
    label: 'Česko',
    currencyCode: 'CZK',
    currencyPosition: 'after',
    numberFormat: 'dot_comma',
    dateFormat: 'dmy_dot',
    timeFormat: '24h',
    unitDistance: 'metric',
    unitTemperature: 'celsius',
    unitWeight: 'metric',
));

Eine Vorlage ist eine Schablone, keine laufende Quelle. Sie anzuwenden schreibt die neun Einstellungen einmalig; der Betreiber kann danach jedes einzelne Feld ändern, und der Formatter liest immer die Einstellungen, nie die Vorlage.

Das ist auch der Grund, warum der Core ca und gb als eigene Einträge mitbringt, statt sie abzuleiten: Kanada rechnet metrisch, zahlt aber in Dollar und liest die 12-Stunden-Uhr, und das Vereinigte Königreich fährt in Meilen, wiegt aber in Kilogramm. Keine dieser Achsen sagt die anderen vorher.

Die Bezeichnungen bleiben unübersetzt. „Deutschland“ ist ein Eigenname und liest sich in einer englischen Oberfläche genauso — so, wie eine Sprachauswahl aus „Deutsch“ auch kein „German“ macht.


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()
    'critical' => false,    // wenn true, wird ein Fehler in persist() weitergeworfen — siehe unten
]

critical (hinzugekommen in 1.2.0). Die Voreinstellung false ist das Verhalten von vor 1.2.0: Ein persist-Handler, der eine Ausnahme wirft, wird protokolliert, an die Diagnose gemeldet und dann verschluckt, damit der übrige Ablauf durchläuft.

Setzen Sie critical: true, wenn das Feld so tragend ist, dass ein stilles Scheitern schlimmer wäre, als den Fahrer aufzuhalten. Die Ausnahme wird dann weitergeworfen und reißt die umgebende Transaktion des JobLifecycleService mit — die Entität bleibt also nicht mit einem Wert zurück, den ihr Handler ihr schuldig geblieben ist.

Was das konkret heißt, hängt vom Zeitpunkt ab:

  • Bei ShiftStart und JobStart entsteht die Schicht beziehungsweise der Einsatz innerhalb derselben Transaktion. Ein kritischer Fehler bedeutet dort, dass sie gar nicht erst angelegt wird.
  • Bei ShiftEnd und JobEnd existierte die Entität schon vorher. Ein kritischer Fehler rollt deshalb nur den Abschluss zurück: Die Schicht oder der Einsatz bleibt offen, verschwindet aber nicht.

Der Fahrer sieht in beiden Fällen eine gewöhnliche Fehlermeldung; die Ausnahme wird so verpackt, dass sie nicht als Serverfehler durchschlägt.

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).


HelpTopicRegistry

Klasse: App\Services\Extension\HelpTopicRegistry

Erlaubt einem Modul, eigene Themen zur zentralen Hilfe im Admin-Bereich (/admin/help) beizusteuern. Der Core bringt einen festen Satz mit (Installation, Erste Schritte, Kunden, Fahrer, Einsätze, Exporte, DSGVO, Einstellungen, Updates, Module und weitere); ein Modul ergänzt zusätzliche Themen, die in derselben Liste und mit denselben Detailseiten erscheinen. Hinzugekommen in 1.1.8.

Methoden

registerTopic(
    string $slug,
    array|string $title,              // ['de'=>…,'en'=>…] ODER ein Übersetzungsschlüssel
    string $view,                     // Blade-Pfad des Moduls, z. B. 'lager::help.index'
    array|string|null $description = null,  // gleiche Form wie $title; optional
    string $icon = /* Heroicon-Pfad: question-mark-circle */,
    ?string $permission = null,       // optionales Gate; blendet das Thema sonst aus
): void
getTopics(?Authenticatable $user = null): array   // aufgelöst und rechtegefiltert

Titel und Beschreibung

$title und $description nehmen entweder eine Locale-Zuordnung oder einen Übersetzungsschlüssel entgegen; aufgelöst wird erst beim Lesen:

  • ['de' => 'Lager', 'en' => 'Warehouse'] — es gewinnt die aktive App-Locale, ersatzweise der erste Eintrag.
  • 'lager::help.title' — läuft durch __().

Das folgt derselben Konvention wie die Navigations-Registries: Beschriftungen niemals schon bei der Registrierung auflösen, sonst frieren Sie die Sprache des Boot-Zeitpunkts ein.

Symbol

$icon ist das rohe d-Attribut eines SVG-<path> im Heroicons-Outline-Stil, genau wie bei der NavigationRegistry. Lassen Sie es weg, erscheint das Fragezeichen-Symbol.

Zusammenführung

HelpController::mergedTopics() baut zuerst die Core-Themen und legt die registrierten per array_merge darüber. Ein Modul kann damit ein Core-Thema ersetzen, indem es dessen Slug wiederverwendet. Der Zugang zur Hilfe selbst hängt am Recht help.view.

Beispiel

app(HelpTopicRegistry::class)->registerTopic(
    'lager',
    ['de' => 'Lagerverwaltung', 'en' => 'Warehouse'],
    'lager::help.index',
    ['de' => 'Bestände, Buchungen und Inventur.', 'en' => 'Stock, movements and inventory.'],
    permission: 'lager.view',
);

Drei Stolperstellen: $view ist ein Modul-Blade-Pfad (lager::help.index), kein Core-Pfad — der Core löst seine eigenen Themen-Views über die Slug-Konvention auf, Module zeigen auf ihre eigene View. Ein wiederverwendeter Core-Slug ersetzt das Core-Thema stillschweigend, benennen Sie Ihre Slugs also nach dem Modul (lager, nicht overview), solange die Ersetzung nicht beabsichtigt ist. Und das permission-Gate greift in getTopics(): Wer es nicht besteht, sieht das Thema gar nicht erst in der Liste.


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.


MailableRegistry

Klasse: App\Services\Notification\MailableRegistry

Ordnet einem Benachrichtigungstyp — der Zeichenkette, die an NotificationChannelRegistry::dispatch() geht — das Mailable zu, das dafür verschickt werden soll. Der EmailNotificationChannel fragt diese Registry, statt immer die Auftragsabschluss-Mail zu senden. Hinzugekommen in 1.2.0.

Vor 1.2.0 war das ein handfestes Problem: Ein Modul, das irgendeinen Benachrichtigungstyp anmeldete, bekam für alle seine Typen die Auftragsabschluss-Mail verschickt — die falsche Vorlage an die falschen Empfänger.

Methoden

/** @param callable(Job, array<string,mixed>): ?Mailable $factory */
registerType(string $type, callable $factory): void

make(string $type, Job $job, array $context): ?Mailable  // null bei unbekanntem Typ
types(): array   // string[] der registrierten Typen — für Diagnose und Dokumentation
has(string $type): bool

Warum eine Factory und keine Klassen-Zeichenkette

Ein bloßes class-string<Mailable> ließe sich nicht instanziieren, ohne dass die Registry die Konstruktor-Form jedes Mailables kennt. Die core-eigene JobCompletedMail nimmt fünf Argumente entgegen, die aus dem $context stammen. Eine Klassen-Zeichenkette verliert diese Zuordnung, und die Registry müsste die Kontext-Form jedes jemals registrierten Mailables kennen, um erweiterbar zu bleiben. Eine Factory-Closure hält die Zuordnung dort, wo der Typ angemeldet wird.

Beispiel

// In Ihrer ServiceProvider::boot()
app(MailableRegistry::class)->registerType(
    'driver_en_route',
    fn (Job $job, array $context) => new DriverEnRouteMail($job, $context['eta'] ?? null),
);

EmailNotificationChannel::send() löst den Typ über make() auf. Ein nicht registrierter Typ verschickt nichts — nie die falsche Mail —, und dispatch() meldet den Kanal für diese Benachrichtigung als skipped. Ein Registry-Eintrag, der nicht aufrufbar ist, scheitert bereits bei der Registrierung laut, statt erst beim Versand.

Siehe auch Benachrichtigungen und Interfaces.


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.


MediaAccessRegistry

Klasse: App\Services\Storage\MediaAccessRegistry

Entscheidet je Pfad-Präfix, wer eine gespeicherte Datei lesen darf. Arbeitet default-deny. Hinzugekommen in 1.2.0.

Pflicht für jedes Modul, das Dateien speichert. Das ist keine Härtungs-Zugabe, die sich verschieben lässt: Dieselbe Präfix-Liste steuert drei verschiedene Dinge, und eine vergessene Anmeldung scheitert still — und zwar in die Richtung, die harmlos aussieht.

Präfix angemeldetPräfix nicht angemeldet
Route media.showliefert die Datei an die Betrachter aus, die Ihre Prüf-Funktion durchlässt403 für alle, Administratoren eingeschlossen
Rückfall-Route /storage/{pfad}weist das Präfix rundheraus abliefert Ihre Dateien von vor 1.2.0 weiter an jeden aus, der den Pfad kennt
PublicMediaMigratorschiebt Ihre Dateien von vor 1.2.0 auf die private Ablagerührt sie nie an

Methoden

register(string $prefix, callable $authorizer): void
// $authorizer: fn (string $relativePath, ?Authenticatable $viewer): bool

allows(string $relativePath, ?Authenticatable $viewer): bool
prefixes(): array  // array<int, string>

Registrierung

app(MediaAccessRegistry::class)->register('my-module/', function (
    string $path,
    ?Authenticatable $viewer,
): bool {
    if ($viewer === null) {
        return false;                       // guest
    }

    $file = MyFile::where('storage_path', $path)->first();

    if ($file === null) {
        return false;                       // our prefix, not our file
    }

    // One branch per guard — never a ?-> chain. User ids and customer ids are
    // separate spaces that collide: customer #7 must not be measured against a
    // User-side id 7.
    if ($viewer instanceof \App\Models\User) {
        return $viewer->isAdmin();
    }

    if ($viewer instanceof \App\Models\Customer) {
        return $viewer->id === $file->customer_id;
    }

    return false;                           // unknown guard
});

Regeln, die keine Stilfragen sind:

  • Beim Boot anmelden, im Service Provider. Der Migrator und die Rückfall-Route lesen nur prefixes() und rufen allows() nie auf; eine Anmeldung, die erst in einer Download-Route passiert, lässt beide ungeschützt.
  • Den abschließenden Schrägstrich behalten. Verglichen wird mit str_starts_with, my würde also auch mystery/ beanspruchen.
  • $viewer ist die vollständige Eingabe. Greifen Sie nicht auf auth(), die Sitzung oder den Request zu — die Prüf-Funktion läuft auch außerhalb eines HTTP-Kontexts.
  • Bei allem Unerwarteten false zurückgeben. Eine werfende Prüf-Funktion wird abgefangen, protokolliert und als Ablehnung behandelt; verlassen Sie sich nicht darauf als Ablaufsteuerung.
  • Sichern Sie den Aufruf mit class_exists ab, wenn Ihr Modul auch in einer eigenständigen Testumgebung ohne Core booten können muss.

Der ausführliche Zusammenhang mit der Dateiablage steht unter Speicher & Backup.


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.