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
NavigationRegistry
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
| Key | Label | Order |
|---|---|---|
top | Dashboard | 0 |
stammdaten | Stammdaten | 10 |
einsaetze | Einsätze | 20 |
auswertungen | Auswertungen | 30 |
system | System | 40 |
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 optionalecondition-Closure die Sichtbarkeit, die denCustomererhält. labelspeichert einen Übersetzungs-Key, keinen fertigen Text. Die Portal-Locale wird pro Anfrage (pro Kunde) inEnsureCustomergesetzt — 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.
| Fall | Wert |
|---|---|
LifecyclePoint::ShiftStart | shift.start |
LifecyclePoint::ShiftEnd | shift.end |
LifecyclePoint::JobStart | job.start |
LifecyclePoint::JobEnd | job.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
ShiftStartundJobStartentsteht die Schicht beziehungsweise der Einsatz innerhalb derselben Transaktion. Ein kritischer Fehler bedeutet dort, dass sie gar nicht erst angelegt wird. - Bei
ShiftEndundJobEndexistierte 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 angemeldet | Präfix nicht angemeldet | |
|---|---|---|
Route media.show | liefert die Datei an die Betrachter aus, die Ihre Prüf-Funktion durchlässt | 403 für alle, Administratoren eingeschlossen |
Rückfall-Route /storage/{pfad} | weist das Präfix rundheraus ab | liefert Ihre Dateien von vor 1.2.0 weiter an jeden aus, der den Pfad kennt |
PublicMediaMigrator | schiebt Ihre Dateien von vor 1.2.0 auf die private Ablage | rü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 rufenallows()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,mywürde also auchmystery/beanspruchen. $viewerist die vollständige Eingabe. Greifen Sie nicht aufauth(), die Sitzung oder den Request zu — die Prüf-Funktion läuft auch außerhalb eines HTTP-Kontexts.- Bei allem Unerwarteten
falsezurü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_existsab, 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.