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
| Event | Handler-Typ | Handler | Aktion |
|---|---|---|---|
WatcherImportProgress | Livewire #[On] | NotificationCenter | Toast mit Import-Fortschritt |
YouTubeVideoSyncCompleted | Livewire #[On] | NotificationCenter | Toast mit Sync-Ergebnis |
DailyScrapeCompleted | Livewire #[On] | NotificationCenter | Toast mit Scrape-Summary |
RelatedProfilesCalculated | Livewire #[On] | NotificationCenter | Toast + UI-Refresh |
ProfileReactivated | Livewire #[On] | NotificationCenter | Toast + Persistente Notification |
ProfileSanitized | Livewire #[On] | NotificationCenter | Toast + Persistente Notification |
ProfileUnsanitized | Livewire #[On] | NotificationCenter | Toast + Persistente Notification |
UserRegistered | Livewire #[On] | NotificationCenter | Toast (nur Admins) + Persistente Notification |
TestBroadcast | Alpine.js | Echo Listener | Toast direkt via Flux.toast() |
ServerMetricsUpdated | Livewire #[On] | Admin\ServerDashboard | Live-Metriken aktualisieren |
ServerAlertTriggered | Livewire #[On] | Admin\ServerDashboard | Alert-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
| Model | Event | Aktion |
|---|---|---|
User | created | Default-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
| Model | Event | Aktion |
|---|---|---|
WatcherSource | created | Profil 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
| Aspekt | Ohne Trait | Mit Trait |
|---|---|---|
| Queue-Job | Failed (BroadcastException) | Erfolgreich (Warning geloggt) |
| Eigentliche Aktion | Ggf. Retry/Rollback | Läuft normal weiter |
| Echtzeit-Notification | Fehlt | Fehlt |
| Flare-Alert | Ja (Exception) | Nein (nur Warning) |
Betroffene Events
Events mit SuppressesBroadcastFailures:
WatcherImportProgressYouTubeVideoSyncCompletedDailyScrapeCompletedRelatedProfilesCalculatedProfileReactivatedProfileSanitizedProfileUnsanitizedUserRegisteredServerMetricsUpdatedServerAlertTriggered
Events ohne Trait:
TestBroadcast-- verwendetShouldBroadcastNow(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
| Policy | Model | Methoden | Beschreibung |
|---|---|---|---|
CollectorJobPolicy | CollectorJob | manage | Nur wenn Job zum Client geleast ist |
Location: app/Policies/CollectorJobPolicy.php
Registrierung: AuthServiceProvider via CollectorJob::class => CollectorJobPolicy::class