Dokumentation durchblättern
Module entwickeln
Filter-Hooks
Die FilterRegistry erlaubt Modulen, Daten an festen Punkten im Core zu verändern — mit Prioritäten, Fehlerschutz und eigenen Hooks.
Die FilterRegistry ist ein Hook-System im Stil von WordPress. Module registrieren
Callbacks auf benannten Hooks; der Core wendet sie an klar definierten Stellen an und lässt
Daten durch sie hindurchfließen, während sie verändert werden. So greift ein Modul in den
Datenfluss ein, ohne eine einzige Core-Datei anzufassen.
Wie Filter arbeiten
Ein Filter besteht aus zwei Seiten: Ein Modul registriert einen Callback auf einem Hook, und der Core wendet diesen Hook auf einen Wert an. Der Callback bekommt den Wert herein, verändert ihn und gibt ihn zurück.
// Module registers a filter
$filters = app(FilterRegistry::class);
$filters->register('schneespur.navigation.items', function (array $items): array {
// Modify and return
$items['custom-group'][] = [...];
return $items;
}, 150); // priority 150
// Core applies the filter
$result = $filters->apply('schneespur.navigation.items', $grouped);
Warum so: Der Core kennt Ihr Modul nicht und ruft Ihren Callback nie direkt auf. Er
ruft nur apply() mit einem Wert auf. Welche Module sich auf diesem Hook eingetragen
haben, entscheidet zur Laufzeit die Registry. Dadurch bleiben Core und Modul vollständig
entkoppelt.
Priorität
Mehrere Module können auf demselben Hook sitzen. Die Reihenfolge steuert die Priorität:
- Niedrigere Zahlen laufen zuerst.
- Standard-Priorität ist
100. - Bei gleicher Priorität entscheidet die Reihenfolge der Registrierung.
- Wirft ein Callback eine Ausnahme, wird der vorherige Wert wiederhergestellt (kein Datenverlust).
Warum eine Priorität jenseits von 100 für Module sinnvoll ist: Die Core-Filter arbeiten auf Standard-Priorität. Tragen Sie Ihr Modul mit einer höheren Zahl ein, läuft es nach dem Core und sieht bereits dessen Ergebnis — Sie verändern also den fertig aufgebauten Wert, statt ihm zuvorzukommen.
Fehlerschutz
Ein einzelner fehlerhafter Filter darf nicht die ganze Kette reißen. Wirft ein Callback eine Ausnahme, fängt die Registry sie ab, protokolliert eine Warnung, stellt den vorherigen Wert wieder her und arbeitet die übrigen Filter weiter ab:
// If this callback throws, the previous value survives
$filters->register('schneespur.dashboard.kpis', function (array $widgets): array {
throw new \Exception('Bug in my filter');
// → warning logged, previous value restored, next filter continues
}, 200);
Warum das wichtig ist: Ein Modul, das die Navigation oder das Dashboard verändert, soll im Fehlerfall nur seinen eigenen Beitrag verlieren — nicht die Oberfläche für alle anderen Module lahmlegen. Verlassen sollten Sie sich darauf trotzdem nicht: Ein Filter, der wirft, verliert seine Änderung still. Siehe dazu die Best Practices am Ende.
Verfügbare Filter-Hooks
Der Core stellt eine feste Menge benannter Hooks bereit. Jeder hat einen genauen Anwendungsort, einen Werttyp und manchmal einen zusätzlichen Kontext.
schneespur.navigation.items
Angewendet in: NavigationRegistry::getItems()
Wert: array<string, array> — gruppierte Navigationspunkte
Kontext: keiner
Erlaubt Modulen, Navigationspunkte hinzuzufügen, zu entfernen oder umzusortieren, nachdem der Core die gruppierte Liste gebaut hat.
$filters->register('schneespur.navigation.items', function (array $grouped): array {
// Add an item to the 'modules' group
$grouped['modules'][] = [
'group' => 'modules',
'slug' => 'my-custom-nav',
'label' => 'My Module',
'route' => 'admin.my-module.index',
'icon' => 'heroicon-o-star',
'order' => 100,
'permission' => null,
'route_check' => null,
'active_pattern' => 'admin.my-module.*',
'badge' => null,
];
return $grouped;
}, 150);
Für den Normalfall — einen Navigationspunkt anmelden — gibt es mit der
NavigationRegistry einen direkteren Weg (siehe Navigation und Dashboard).
Dieser Filter ist für die Fälle, in denen Sie die bereits fertige, gruppierte Liste als
Ganzes umordnen oder Einträge anderer Module beeinflussen wollen.
schneespur.dashboard.kpis
Angewendet in: Admin\DashboardController
Wert: array — Widget-Konfigurationen
Kontext: keiner
Erlaubt Modulen, Dashboard-Widgets hinzuzufügen oder zu verändern, nachdem die Core-Widgets
gebaut wurden. Anders als die DashboardWidgetRegistry arbeitet dieser Filter auf dem schon
fertig vorbereiteten Widget-Array.
$filters->register('schneespur.dashboard.kpis', function (array $widgets): array {
$widgets[] = [
'key' => 'my-custom-kpi',
'label' => 'Custom KPI',
'view' => 'my-module::widgets.kpi',
'order' => 250,
'size' => 'half',
];
return $widgets;
}, 150);
schneespur.job.notification.recipients
Angewendet in: SendJobCompletedNotification-Listener
Wert: array — Empfängerliste
Kontext: $job (das abgeschlossene Job-Model)
Erlaubt Modulen, Empfänger zu ergänzen oder zu filtern, wenn ein Auftrag abgeschlossen
wird. Dieser Hook bekommt mit $job einen zweiten Parameter — das abgeschlossene Job-Model
—, sodass Sie die Empfänger anhand der konkreten Daten des Auftrags bestimmen können.
$filters->register('schneespur.job.notification.recipients', function (array $recipients, $job): array {
// Add the dispatcher to recipients
if ($dispatcher = $job->workShift?->user) {
$recipients[] = ['email' => $dispatcher->email, 'name' => $dispatcher->name];
}
return $recipients;
}, 100);
schneespur.job.notification.channels
Angewendet in: NotificationChannelRegistry::dispatch()
Wert: array<string, NotificationChannelInterface> — aktive Kanäle
Kontext: $job (das benachrichtigte Job-Model)
Erlaubt Modulen zu steuern, über welche Benachrichtigungs-Kanäle ein bestimmter Auftrag versendet wird. So lassen sich Kanäle abhängig vom Auftragstyp gezielt abschalten:
$filters->register('schneespur.job.notification.channels', function (array $channels, $job): array {
// Disable Telegram for Kontrolle-type jobs
if ($job->type === JobType::Kontrolle) {
unset($channels['telegram']);
}
return $channels;
}, 100);
schneespur.pdf.job.after_details, schneespur.pdf.job.after_weather, schneespur.pdf.job.end
Angewendet in: resources/views/pdf/job-report.blade.php und resources/views/pdf/partials/job-section.blade.php (über die Blade-Direktive @pdfExtensionSlot)
Wert: string[] — HTML-Fragmente, die an dieser Stelle in das PDF eingefügt werden
Kontext: $job (das gerenderte Job-Model)
Diese drei Hooks lassen ein Modul eigene HTML-Abschnitte in das PDF eines Einsatzes einfügen.
Die job.*-Familie wird einmal pro Einsatz gerendert: einmal im eigenständigen
Einsatznachweis und einmal pro Einsatz-Seite in den Sammelberichten für Kunde und Objekt —
beide teilen sich das Partial partials/job-section.blade.php. Sie registrieren den Hook
also einmal und erscheinen überall, wo ein Einsatz gedruckt wird.
| Hook | Position im Einsatz-Block |
|---|---|
schneespur.pdf.job.after_details | nach dem Abschnitt mit den Einsatzdaten |
schneespur.pdf.job.after_weather | nach dem Wetter-Abschnitt |
schneespur.pdf.job.end | am Ende des Einsatz-Blocks (nach den Fotos) |
Anders als die bisherigen Filter, die ein fertiges Daten-Array durchreichen, sammeln diese Hooks ein Array von HTML-Zeichenketten ein: Ihr Callback rendert einen eigenen, DomPDF-tauglichen Blade-View zu einer Zeichenkette und hängt sie an. So bringt ein Modul sichtbare Inhalte in einen Core-Bericht, ohne einen eigenen Controller oder ein eigenes Template zu betreiben.
use App\Models\Job;
$filters->register('schneespur.pdf.job.end', function (array $sections, Job $job): array {
// Render your own DomPDF-friendly Blade view to a string and append it.
$html = view('my-module::pdf.consumption', ['job' => $job])->render();
if ($html !== '') {
$sections[] = $html;
}
return $sections;
}, 100);
Hinzugekommen in 1.1.7.
schneespur.pdf.collective.cover_end
Angewendet in: resources/views/pdf/customer-report.blade.php (über @pdfExtensionSlot)
Wert: string[] — HTML-Fragmente, die auf dem Deckblatt eingefügt werden
Kontext: $customer (Customer), $jobs (Collection von Job), $from (Carbon), $to (Carbon)
Dieser Hook wird einmal auf dem Deckblatt der Sammelberichte für Kunde und Objekt
gerendert, im Anschluss an das Inhaltsverzeichnis. Er ist für eine Zusammenfassung über
alle Einsätze im Zeitraum gedacht — etwa eine Gesamt-Verbrauchszahl. Entsprechend
bekommt der Callback mit $jobs, $from und $to den vollständigen Bezugsrahmen mit, nicht
nur einen einzelnen Einsatz:
use App\Models\Customer;
use Illuminate\Support\Collection;
use Carbon\Carbon;
$filters->register('schneespur.pdf.collective.cover_end', function (array $sections, Customer $customer, Collection $jobs, Carbon $from, Carbon $to): array {
$sections[] = view('my-module::pdf.cover-summary', [
'customer' => $customer,
'jobs' => $jobs,
'from' => $from,
'to' => $to,
])->render();
return $sections;
}, 100);
Hinzugekommen in 1.1.7.
Für alle PDF-Hooks gilt:
- Geben Sie das (erweiterte)
arrayzurück — dieselbe Vertragsregel wie bei den übrigen Filter-Hooks. - Werfen Sie nicht absichtlich: Die
FilterRegistryfängt Ausnahmen je Callback ab, protokolliert sie und überspringt nur diesen einen Beitrag — das PDF bleibt intakt. Ein Modul kann den kundenseitigen Bericht so nicht zerstören. - Das HTML muss DomPDF-tauglich sein (einfaches, überwiegend inline gehaltenes CSS — siehe die Core-PDF-Views unter PDF und Berichte).
- Ist kein Hook registriert, wird nichts eingefügt; der Erweiterungspunkt ist auf einem Core
ohne Module inert. Greifen Sie ihn abgesichert ab:
if ($this->app->bound(FilterRegistry::class)) { ... }.
Eigene Hooks im Modul anlegen
Ein Filter-Hook ist nicht dem Core vorbehalten. Sie können in Ihrem Modul eigene Hooks definieren, auf die andere Module sich eintragen, um Ihre Daten zu erweitern:
// In your module's code
$filterRegistry = app(FilterRegistry::class);
$reportData = $filterRegistry->apply('my-module.report.data', $baseData, $customer);
Andere Module können sich dann auf my-module.report.data registrieren, um die Daten zu
verändern. Warum das nützlich ist: Damit machen Sie Ihr Modul selbst erweiterbar — die
gleiche Entkopplung, die der Core Ihnen bietet, geben Sie an nachgelagerte Module weiter,
ohne deren Code zu kennen.
Best Practices
- Geben Sie den veränderten Wert immer zurück — ein Filter muss den (gegebenenfalls geänderten) Wert zurückgeben.
- Nutzen Sie eine Priorität über
100für Modul-Filter, damit die Core-Filter zuerst laufen. - Benennen Sie eigene Hooks mit dem Modul-Slug als Präfix:
my-module.hook.name. - Behandeln Sie fehlende Daten umsichtig — der Wert kann bereits von einem anderen Filter verändert worden sein.
- Werfen Sie keine Ausnahmen — sie werden zwar abgefangen, aber Ihre Änderung geht dabei verloren.
Wie Module ihre Erweiterungspunkte grundsätzlich in boot() anmelden, zeigt das
ServiceProvider-Muster. Eng verwandt sind die
Events: Filter verändern einen Wert und geben ihn zurück, Events
melden ein Geschehen, ohne einen Wert zu erwarten.