Zum Hauptinhalt springen

Listeners & Observers

Event-Handling in Postbox folgt einem schlanken Pattern: Statt klassischer Laravel Listener-Klassen werden Events primär über Livewire #[On()] Handler, Alpine.js Echo-Subscriptions und Eloquent Observers verarbeitet.

Event-Verarbeitungs-Architektur

Event dispatched
├── Laravel Reverb (WebSocket Broadcast)
│ └── Browser (Echo.js)
│ └── Alpine.js Handler
│ └── $wire.dispatch('reverb-*')
│ └── Livewire NotificationCenter
│ ├── Toast anzeigen
│ └── $this->dispatch('notification-received')
│ └── NotificationBell: Unread Count refresh

└── (Kein klassischer Listener → direkte Verarbeitung im Event selbst)

Event -> Handler Mapping

EventHandler-TypHandlerAktion
WatcherImportProgressLivewire #[On]NotificationCenterToast mit Import-Fortschritt
YouTubeVideoSyncCompletedLivewire #[On]NotificationCenterToast mit Sync-Ergebnis
DailyScrapeCompletedLivewire #[On]NotificationCenterToast mit Scrape-Summary
RelatedProfilesCalculatedLivewire #[On]NotificationCenterToast + UI-Refresh
ProfileReactivatedLivewire #[On]NotificationCenterToast + Persistente Notification
ProfileSanitizedLivewire #[On]NotificationCenterToast + Persistente Notification
ProfileUnsanitizedLivewire #[On]NotificationCenterToast + Persistente Notification
UserRegisteredLivewire #[On]NotificationCenterToast (nur Admins) + Persistente Notification
TestBroadcastAlpine.jsEcho ListenerToast direkt via Flux.toast()
ServerMetricsUpdatedLivewire #[On]Admin\ServerDashboardLive-Metriken aktualisieren
ServerAlertTriggeredLivewire #[On]Admin\ServerDashboardAlert-Toast + History-Update

Livewire NotificationCenter

Der zentrale Handler für alle Reverb-Events im Frontend. Empfängt Events über Alpine.js Echo-Subscriptions und verarbeitet sie server-seitig.

Location: app/Livewire/NotificationCenter.php

Echo-Subscriptions (Frontend)

// resources/views/livewire/notification-center.blade.php
subscribeToAllChannels() {
// User-Channel
Echo.private(`user.${userId}`)
.listen('.youtube.sync.completed', (e) => $wire.dispatch('reverb-youtube-sync', e))
.listen('.daily.scrape.completed', (e) => $wire.dispatch('reverb-daily-scrape', e))
.listen('.related.profiles.calculated', (e) => $wire.dispatch('reverb-related', e))
.listen('.profile.reactivated', (e) => $wire.dispatch('reverb-reactivated', e))
.listen('.profile.sanitized', (e) => $wire.dispatch('reverb-sanitized', e))
.listen('.profile.unsanitized', (e) => $wire.dispatch('reverb-unsanitized', e));

// Workspace-Channels (pro Workspace)
workspaceIds.forEach(id => {
Echo.private(`workspace.${id}`)
.listen('.import.progress', (e) => $wire.dispatch('reverb-import', e));
});

// Admin-Channel (nur für Admins)
Echo.private('admin')
.listen('.user.registered', (e) => $wire.dispatch('reverb-user-registered', e));
}

Server-seitige Handler

// Beispiel: YouTube Sync Handler
#[On('reverb-youtube-sync')]
public function handleYouTubeSyncCompleted(array $data): void
{
// Toast dispatchen
$this->dispatch('show-toast', [
'message' => $data['message'],
'type' => $data['success'] ? 'success' : 'danger',
]);
}

Rate Limiting

Der NotificationCenter nutzt safeServerDispatch() mit max 40 Calls/Burst, um bei hochfrequenten Events (z.B. Import mit vielen URLs) den Server nicht zu überlasten.

Observers

Eloquent Model Observers für automatische Side-Effects bei Datenänderungen.

Location: app/Observers/

UserObserver

Reagiert auf die Erstellung neuer User.

Location: app/Observers/UserObserver.php

ModelEventAktion
UsercreatedDefault-Workspace provisionieren + Admin-Email senden
class UserObserver
{
public function created(User $user): void
{
// Default-Workspace mit Starter-Watchern erstellen
app(DefaultWorkspaceProvisioner::class)->ensureGeneralWorkspace($user);

// Admin per E-Mail über neue Registrierung informieren
$this->notifyAdminAboutNewUser($user);
}
}

Registrierung: AppServiceProvider::boot() via User::observe(UserObserver::class)

WatcherSourceObserver

Spiegelt neue WatcherSources automatisch in den Admin-Workspace.

Location: app/Observers/WatcherSourceObserver.php

ModelEventAktion
WatcherSourcecreatedProfil in Admin-Workspace spiegeln
class WatcherSourceObserver
{
public function created(WatcherSource $source): void
{
// Guard: nicht für den Admin-Workspace selbst
if ($manager->isAdminWorkspaceId($watcher->workspace_id)) {
return;
}

// Profil in Admin-Workspace spiegeln für globale Sichtbarkeit
$manager->ensureProfileWatcher($profile);
}
}

Registrierung: AppServiceProvider::boot() via WatcherSource::observe(WatcherSourceObserver::class)

SuppressesBroadcastFailures Pattern

Broadcasts sind "nice to have" -- wenn Reverb temporär nicht erreichbar ist, soll die App trotzdem funktionieren. Alle ShouldBroadcast Events (außer TestBroadcast) verwenden das SuppressesBroadcastFailures Trait.

Location: app/Events/Concerns/SuppressesBroadcastFailures.php

Trait-Implementierung

trait SuppressesBroadcastFailures
{
public function middleware(): array
{
return [
new \App\Queue\Middleware\SuppressBroadcastFailures,
];
}
}

Queue Middleware

Location: app/Queue/Middleware/SuppressBroadcastFailures.php

class SuppressBroadcastFailures
{
public function handle(object $job, Closure $next): void
{
try {
$next($job);
} catch (BroadcastException $e) {
Log::warning('Broadcast failed (Reverb unavailable)', [
'job' => get_class($job),
'message' => $e->getMessage(),
]);
}
}
}

Verhalten bei Reverb-Ausfall

AspektOhne TraitMit Trait
Queue-JobFailed (BroadcastException)Erfolgreich (Warning geloggt)
Eigentliche AktionGgf. Retry/RollbackLäuft normal weiter
Echtzeit-NotificationFehltFehlt
Flare-AlertJa (Exception)Nein (nur Warning)

Betroffene Events

Events mit SuppressesBroadcastFailures:

  • WatcherImportProgress
  • YouTubeVideoSyncCompleted
  • DailyScrapeCompleted
  • RelatedProfilesCalculated
  • ProfileReactivated
  • ProfileSanitized
  • ProfileUnsanitized
  • UserRegistered
  • ServerMetricsUpdated
  • ServerAlertTriggered

Events ohne Trait:

  • TestBroadcast -- verwendet ShouldBroadcastNow (synchron, nicht gequeued); Fehler sollen beim Testen sichtbar sein

Backup: Global Exception Suppression

Als zusätzliche Absicherung ist BroadcastException in bootstrap/app.php als dontReport konfiguriert:

// bootstrap/app.php
->withExceptions(function (Exceptions $exceptions) {
$exceptions->dontReport([
\Illuminate\Broadcasting\BroadcastException::class,
]);
})

Policies

PolicyModelMethodenBeschreibung
CollectorJobPolicyCollectorJobmanageNur wenn Job zum Client geleast ist

Location: app/Policies/CollectorJobPolicy.php

Registrierung: AuthServiceProvider via CollectorJob::class => CollectorJobPolicy::class