Zum Hauptinhalt springen
Schneespur
Dokumentation durchblättern

Module entwickeln

Benachrichtigungen

Wie Schneespur abgeschlossene Einsätze über kanalbasierte Benachrichtigungen meldet und wie ein Modul einen eigenen Kanal wie Telegram oder SMS ergänzt.

Wenn ein Einsatz abgeschlossen ist, soll oft jemand davon erfahren — die Hausverwaltung, der Kunde, die Disposition. Schneespur löst das über ein kanalbasiertes Benachrichtigungssystem: Der Kern kennt nur das Konzept „Kanal”, die konkreten Wege (E-Mail, Telegram, SMS …) liefern Module nach. So lässt sich ein neuer Benachrichtigungsweg hinzufügen, ohne den Kern anzufassen.

Wie eine Benachrichtigung entsteht

Auslöser ist das JobCompleted-Event. Der Listener SendJobCompletedNotification nimmt es auf und verteilt die Meldung über alle aktiven Kanäle der NotificationChannelRegistry:

JobCompleted event
  → SendJobCompletedNotification listener
    → FilterRegistry applies 'schneespur.job.notification.recipients'
    → NotificationChannelRegistry::dispatch()
      → FilterRegistry applies 'schneespur.job.notification.channels'
      → Each enabled channel's send() method is called
      → Results logged to notification_logs table

Wichtig sind die zwei Filter-Punkte in diesem Ablauf: Bevor versendet wird, dürfen Module die Empfängerliste (schneespur.job.notification.recipients) und die Auswahl der aktiven Kanäle (schneespur.job.notification.channels) anpassen. Dadurch greift ein Modul in den Versand ein, ohne den Listener selbst zu ersetzen. Mehr zu diesem Mechanismus unter Filter & Hooks und Events.

Der Kern-Kanal: E-Mail

E-Mail ist bereits eingebaut. Der Kern registriert EmailNotificationChannel, der über Laravels Mail-System versendet — mit dem Mailable JobCompletedMail. Er beachtet die Benachrichtigungs-Einstellungen pro Kunde (auto_notify_email, notification_email). Ein eigener Kanal ist also nur dann nötig, wenn ein anderer Weg als E-Mail dazukommen soll.

Einen eigenen Benachrichtigungs-Kanal bauen

Am Beispiel eines Telegram-Kanals: Das Muster ist immer dasselbe — Interface implementieren, in der Registry anmelden, Einstellungen bereitstellen.

1. Das Interface implementieren

Ein Kanal implementiert NotificationChannelInterface. Die zentrale Methode ist send(): Sie bekommt den abgeschlossenen Job, den Benachrichtigungstyp und einen Kontext und ist dafür verantwortlich, die Meldung tatsächlich zuzustellen. Die Methoden name(), slug() und isEnabled() beschreiben den Kanal und sagen, ob er einsatzbereit konfiguriert ist.

namespace Schneespur\Module\Telegram\Notification;

use App\Models\Job;
use App\Models\Setting;
use App\Services\Notification\NotificationChannelInterface;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;

class TelegramChannel implements NotificationChannelInterface
{
    public function send(Job $job, string $type, array $context): void
    {
        $botToken = Setting::get('telegram.bot_token');
        $chatId = Setting::get('telegram.chat_id');

        $customer = $job->customer;
        $object = $job->customerObject;
        $driver = $job->user;

        $text = sprintf(
            "✅ *Einsatz abgeschlossen*\n" .
            "Typ: %s\n" .
            "Kunde: %s\n" .
            "Objekt: %s\n" .
            "Fahrer: %s\n" .
            "Dauer: %s",
            $job->type->label(),
            $customer?->name ?? '—',
            $object?->name ?? '—',
            $driver?->displayName() ?? '—',
            $job->durationFormatted()
        );

        $response = Http::post("https://api.telegram.org/bot{$botToken}/sendMessage", [
            'chat_id' => $chatId,
            'text' => $text,
            'parse_mode' => 'Markdown',
        ]);

        if (!$response->successful()) {
            throw new \RuntimeException('Telegram API error: ' . $response->body());
        }
    }

    public function name(): string
    {
        return 'Telegram';
    }

    public function slug(): string
    {
        return 'telegram';
    }

    public function isEnabled(): bool
    {
        return !empty(Setting::get('telegram.bot_token'))
            && !empty(Setting::get('telegram.chat_id'));
    }
}

isEnabled() ist mehr als Formsache: Über diese Methode entscheidet der Kanal selbst, ob er am Versand teilnimmt. Solange Bot-Token und Chat-ID fehlen, bleibt der Kanal still — statt mit einer halb konfigurierten Verbindung zu scheitern. Geworfene Ausnahmen in send() werden vom System aufgefangen und protokolliert (siehe unten), bringen den Gesamtversand also nicht zu Fall.

2. Im ServiceProvider registrieren

Angemeldet wird der Kanal in der boot()-Phase des Modul-ServiceProviders, über die NotificationChannelRegistry. Der erste Parameter ist der Slug, der zweite die Klasse:

protected function registerNotificationChannels(): void
{
    $registry = app(NotificationChannelRegistry::class);
    $registry->register('telegram', TelegramChannel::class);
}

Wo dieser Aufruf im Lebenszyklus des Moduls steht, beschreibt Das ServiceProvider-Muster.

3. Standard-Einstellungen registrieren

Damit Bot-Token und Chat-ID konfigurierbar sind, registriert das Modul sie als Einstellungen. Sie landen mit dem Modul-Slug als Präfix in der Tabelle settings:

app(ModuleManager::class)->registerSettings('telegram', [
    'bot_token' => '',
    'chat_id' => '',
]);

4. Eine Einstellungs-Seite bauen

Zuletzt braucht es einen Admin-Controller und eine Blade-View, in der Token und Chat-ID eingetragen werden. Bewährt hat sich das test-before-save-Muster: den eingegebenen Wert erst gegen die echte API prüfen und nur speichern, wenn der Test trägt — so fällt eine falsche Zugangsangabe sofort auf und nicht erst beim nächsten Einsatz.

Der Benachrichtigungs-Kontext

Das $context-Array, das an send() übergeben wird, enthält in der Regel die Empfänger und je nach Benachrichtigungstyp weitere Angaben:

[
    'recipients' => [
        ['email' => 'customer@example.com', 'name' => 'Customer Name'],
    ],
    // Additional context depending on notification type
]

Filter: Empfänger anpassen

Über den Filter schneespur.job.notification.recipients ändert ein Modul die Empfängerliste, bevor versendet wird — etwa um eine feste Kopie an die Disposition zu ergänzen. Der dritte Parameter (100) ist die Priorität, die die Reihenfolge mehrerer Filter bestimmt:

$filters = app(FilterRegistry::class);
$filters->register('schneespur.job.notification.recipients', function (array $recipients, $job): array {
    // Add a CC recipient for all jobs
    $recipients[] = [
        'email' => Setting::get('telegram.cc_email'),
        'name' => 'Dispatcher',
    ];
    return $recipients;
}, 100);

Filter: Steuern, welche Kanäle feuern

Spiegelbildlich entscheidet der Filter schneespur.job.notification.channels, welche Kanäle für einen konkreten Einsatz überhaupt feuern. So lässt sich ein Kanal gezielt für bestimmte Einsatztypen aus- oder einschalten:

$filters->register('schneespur.job.notification.channels', function (array $channels, $job): array {
    // Only use Telegram for urgent job types
    if ($job->type === JobType::Kontrolle) {
        unset($channels['telegram']);
    }
    return $channels;
}, 100);

Versand protokollieren

Jeder Versand wird festgehalten — das ist der Grund, warum eine fehlgeschlagene Zustellung nicht spurlos verschwindet. Das Modell NotificationLog speichert pro Meldung, über welchen Kanal, an wen und mit welchem Ergebnis versendet wurde:

// Fields:
notifiable_type  // e.g. 'App\Models\Job' or 'App\Models\Customer'
notifiable_id    // the model ID
channel          // 'email', 'telegram', etc.
type             // notification type
recipient        // where it was sent
status           // 'sent', 'failed'
error_message    // failure reason (if any)
metadata         // JSON additional data

Ein Kanal sollte für seine Zustellungen selbst einen Log-Eintrag anlegen — das ergibt einen nachvollziehbaren Verlauf, wer wann benachrichtigt wurde:

use App\Models\NotificationLog;

NotificationLog::create([
    'notifiable_type' => Job::class,
    'notifiable_id' => $job->id,
    'channel' => 'telegram',
    'type' => 'job_completed',
    'recipient' => $chatId,
    'status' => 'sent',
    'metadata' => ['message_id' => $response->json('result.message_id')],
]);

Weitere denkbare Kanäle

Das gezeigte Muster trägt für beliebige Transportwege. Folgende Kanäle lassen sich nach demselben Schema umsetzen — sie unterscheiden sich nur in Transport und benötigter Konfiguration:

ChannelTransportKey Config
TelegramHTTP POST to Bot APIBot token, chat ID
SMS (Twilio)Twilio REST APIAccount SID, auth token, from number
SMS (Vonage)Vonage REST APIAPI key, secret, from number
SlackIncoming WebhookWebhook URL
Push (Firebase)FCM HTTP v1 APIService account key
Microsoft TeamsIncoming WebhookWebhook URL
WhatsAppTwilio/Meta APIAccount credentials

Jeder dieser Kanäle folgt denselben vier Schritten: NotificationChannelInterface implementieren, in der NotificationChannelRegistry registrieren, Standard-Einstellungen anmelden und eine Einstellungs-Seite bereitstellen.