Zum Hauptinhalt springen
Schneespur
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);

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

  1. Geben Sie den veränderten Wert immer zurück — ein Filter muss den (gegebenenfalls geänderten) Wert zurückgeben.
  2. Nutzen Sie eine Priorität über 100 für Modul-Filter, damit die Core-Filter zuerst laufen.
  3. Benennen Sie eigene Hooks mit dem Modul-Slug als Präfix: my-module.hook.name.
  4. Behandeln Sie fehlende Daten umsichtig — der Wert kann bereits von einem anderen Filter verändert worden sein.
  5. 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.