# SSE Integration Guide **Praktischer Guide** für die Integration von Server-Sent Events (SSE) in LiveComponents. ## Quick Start Eine LiveComponent SSE-fähig zu machen erfordert **2 einfache Schritte**: ### 1. Action-Attribute hinzufügen ```php use App\Framework\LiveComponents\Attributes\Action; #[Action] public function markAsRead(string $notificationId): ComponentData { // ... your logic ... } ``` ### 2. getSseChannel() Methode implementieren ```php public function getSseChannel(): string { return "user:{$this->userId}"; } ``` **Das war's!** 🎉 Die Component verbindet sich automatisch zum SSE-Stream und empfängt Echtzeit-Updates. ## Step-by-Step Tutorial ### Schritt 1: Bestehende Component vorbereiten Nehmen wir eine einfache NotificationComponent: ```php #[LiveComponent('notifications')] final readonly class NotificationComponent implements LiveComponentContract { private ComponentId $id; private NotificationState $state; public function __construct( ComponentId $id, ?ComponentData $initialData = null, array $notifications = [] ) { $this->id = $id; if ($initialData !== null) { $this->state = NotificationState::fromComponentData($initialData); return; } $this->state = new NotificationState($notifications); } public function getId(): ComponentId { return $this->id; } public function getData(): ComponentData { return $this->state->toComponentData(); } public function getRenderData(): ComponentRenderData { return new ComponentRenderData( templatePath: 'notifications', data: [ 'notifications' => $this->state->notifications, 'unread_count' => $this->state->getUnreadCount() ] ); } // Action methods... public function markAsRead(string $notificationId): ComponentData { $newState = $this->state->withNotificationRead($notificationId); return $newState->toComponentData(); } public function deleteNotification(string $notificationId): ComponentData { $newState = $this->state->withNotificationDeleted($notificationId); return $newState->toComponentData(); } } ``` ### Schritt 2: Action-Attribute hinzufügen Füge `#[Action]` zu allen Action-Methoden hinzu: ```php use App\Framework\LiveComponents\Attributes\Action; #[Action] public function markAsRead(string $notificationId): ComponentData { $newState = $this->state->withNotificationRead($notificationId); return $newState->toComponentData(); } #[Action] public function deleteNotification(string $notificationId): ComponentData { $newState = $this->state->withNotificationDeleted($notificationId); return $newState->toComponentData(); } ``` **Warum?** Das `#[Action]` Attribute markiert Methoden als sichere, von außen aufrufbare Actions. ### Schritt 3: getSseChannel() implementieren Wähle die passende Channel-Strategie für deinen Use Case: #### Option A: User-Specific Channel ```php /** * Get SSE channel for real-time notification updates * * Uses user-specific channel for private notifications. * Falls back to 'global' for guest users. */ public function getSseChannel(): string { // Extract user ID from component instance ID // ComponentId format: "notifications:user-123" $instanceId = $this->id->instanceId; if ($instanceId === 'global' || $instanceId === 'guest') { return 'global'; } // User-specific channel for private notifications return "user:{$instanceId}"; } ``` #### Option B: Context-Specific Channel ```php /** * Get SSE channel for context-based notifications * * Examples: * - "notifications:post-123" → "notifications:post-123" * - "notifications:project-456" → "notifications:project-456" */ public function getSseChannel(): string { $context = $this->id->instanceId; return "notifications:{$context}"; } ``` #### Option C: Global Channel ```php /** * Get SSE channel for system-wide notifications */ public function getSseChannel(): string { return 'global'; } ``` ### Schritt 4: Testen Die Component ist jetzt SSE-fähig! Teste es: ```php // Test-Script use App\Framework\LiveComponents\ValueObjects\ComponentId; $component = new NotificationComponent( id: ComponentId::fromString('notifications:user-123') ); // Check if getSseChannel() exists and returns correct channel $channel = $component->getSseChannel(); assert($channel === 'user:user-123'); // Check HTML rendering includes data-sse-channel attribute $html = $componentRegistry->renderWithWrapper($component); assert(str_contains($html, 'data-sse-channel="user:user-123"')); ``` ### Schritt 5: Verifizieren im Browser 1. **Öffne Browser DevTools** (F12) 2. **Network Tab** → Filter: "EventSource" oder "eventsource" 3. **Lade die Seite** mit deiner Component 4. **Prüfe**: Du solltest eine SSE-Verbindung zu `/sse/events/{channel}` sehen 5. **Trigger Action**: Klicke auf einen Button der eine Action aufruft 6. **Prüfe Console**: Du solltest SSE-Events sehen ``` [LiveComponent] Setting up SSE for notifications:user-123 on channel user:user-123 [SSE] Connected to user:user-123 [LiveComponent] SSE update received for notifications:user-123 ``` ## Channel-Strategien ### 1. Global Channel **Verwendung**: System-weite Updates für alle Benutzer ```php public function getSseChannel(): string { return 'global'; } ``` **Wann verwenden?** - ✅ Public Activity Feeds - ✅ System Announcements - ✅ Global Statistics - ❌ User-spezifische Daten **Beispiel**: Public Activity Feed ```php #[LiveComponent('activity-feed')] final readonly class ActivityFeedComponent implements LiveComponentContract { public function getSseChannel(): string { // Alle Benutzer sehen dasselbe Feed return 'global'; } } ``` ### 2. User-Specific Channels **Verwendung**: Benutzer-spezifische Updates ```php public function getSseChannel(): string { $instanceId = $this->id->instanceId; // z.B. "user-123" if ($instanceId === 'guest' || $instanceId === 'global') { return 'global'; } return "user:{$instanceId}"; } ``` **Wann verwenden?** - ✅ Private Benachrichtigungen - ✅ Personal Dashboards - ✅ User-spezifische Activity Feeds - ❌ Public Daten **Beispiel**: Private Notification Center ```php #[LiveComponent('notification-center')] final readonly class NotificationCenterComponent implements LiveComponentContract { public function getSseChannel(): string { $instanceId = $this->id->instanceId; if ($instanceId === 'global' || $instanceId === 'guest') { return 'global'; // Fallback für Gäste } // User-spezifischer Channel: user:user-123 return "user:{$instanceId}"; } } ``` ### 3. Context-Based Channels **Verwendung**: Kontext-spezifische Updates (Posts, Articles, Rooms) ```php public function getSseChannel(): string { $context = $this->id->instanceId; // z.B. "post-123", "article-456" return "comments:{$context}"; } ``` **Wann verwenden?** - ✅ Post/Article Comments - ✅ Document Collaboration - ✅ Context-specific Updates - ❌ User-übergreifende Updates **Beispiel**: Comment Thread ```php #[LiveComponent('comment-thread')] final readonly class CommentThreadComponent implements LiveComponentContract { public function getSseChannel(): string { // Extract context from instance ID // "comment-thread:post-123" → "comments:post-123" $context = $this->id->instanceId; return "comments:{$context}"; } } ``` ### 4. Presence Channels **Verwendung**: Real-time Präsenz-Tracking ```php public function getSseChannel(): string { $room = $this->id->instanceId; // z.B. "lobby", "room-5" return "presence:{$room}"; } ``` **Wann verwenden?** - ✅ Live Presence Indicators - ✅ Active User Lists - ✅ Collaborative Editing Sessions - ❌ Asynchrone Updates **Beispiel**: Live Presence ```php #[LiveComponent('live-presence')] final readonly class LivePresenceComponent implements LiveComponentContract { public function getSseChannel(): string { // Extract room from instance ID // "live-presence:lobby" → "presence:lobby" $room = $this->id->instanceId; return "presence:{$room}"; } } ``` ## Beispiel-Implementierungen ### Beispiel 1: Notification Center (User-Specific) ```php use App\Framework\LiveComponents\Attributes\Action; use App\Framework\LiveComponents\Attributes\LiveComponent; use App\Framework\LiveComponents\Contracts\LiveComponentContract; use App\Framework\LiveComponents\ValueObjects\ComponentData; use App\Framework\LiveComponents\ValueObjects\ComponentId; use App\Framework\LiveComponents\ValueObjects\ComponentRenderData; #[LiveComponent('notification-center')] final readonly class NotificationCenterComponent implements LiveComponentContract { private ComponentId $id; private NotificationCenterState $state; public function __construct( ComponentId $id, ?ComponentData $initialData = null, array $notifications = [] ) { $this->id = $id; if ($initialData !== null) { $this->state = NotificationCenterState::fromComponentData($initialData); return; } $this->state = new NotificationCenterState($notifications); } public function getId(): ComponentId { return $this->id; } public function getData(): ComponentData { return $this->state->toComponentData(); } public function getRenderData(): ComponentRenderData { return new ComponentRenderData( templatePath: 'notification-center', data: [ 'notifications' => $this->state->notifications, 'unread_count' => $this->state->getUnreadCount() ] ); } #[Action] public function markAsRead(string $notificationId): ComponentData { $newState = $this->state->withNotificationRead($notificationId); return $newState->toComponentData(); } #[Action] public function deleteNotification(string $notificationId): ComponentData { $newState = $this->state->withNotificationDeleted($notificationId); return $newState->toComponentData(); } #[Action] public function markAllAsRead(): ComponentData { $newState = $this->state->withAllMarkedAsRead(); return $newState->toComponentData(); } /** * Get SSE channel for real-time notifications * * Uses user-specific channel for private notifications. * Falls back to 'global' for guest users. */ public function getSseChannel(): string { $instanceId = $this->id->instanceId; if ($instanceId === 'global' || $instanceId === 'guest') { return 'global'; } return "user:{$instanceId}"; } } ``` ### Beispiel 2: Comment Thread (Context-Specific) ```php #[LiveComponent('comment-thread')] final readonly class CommentThreadComponent implements LiveComponentContract { private ComponentId $id; private CommentThreadState $state; public function __construct( ComponentId $id, ?ComponentData $initialData = null, array $comments = [] ) { $this->id = $id; if ($initialData !== null) { $this->state = CommentThreadState::fromComponentData($initialData); return; } $this->state = new CommentThreadState($comments); } public function getId(): ComponentId { return $this->id; } public function getData(): ComponentData { return $this->state->toComponentData(); } public function getRenderData(): ComponentRenderData { return new ComponentRenderData( templatePath: 'comment-thread', data: [ 'comments' => $this->state->buildCommentTree(), 'total_comments' => count($this->state->comments) ] ); } #[Action] public function addComment( string $content, string $authorId, string $authorName, ?string $parentId = null ): ComponentData { $newState = $this->state->withCommentAdded($content, $authorId, $authorName, $parentId); return $newState->toComponentData(); } #[Action] public function deleteComment(string $commentId): ComponentData { $newState = $this->state->withCommentDeleted($commentId); return $newState->toComponentData(); } /** * Get SSE channel for real-time comment updates * * Uses context-based channel for post/article-specific comments. * * Examples: * - "comment-thread:post-123" → "comments:post-123" * - "comment-thread:article-456" → "comments:article-456" */ public function getSseChannel(): string { $context = $this->id->instanceId; return "comments:{$context}"; } } ``` ### Beispiel 3: Activity Feed (Context or User) ```php #[LiveComponent('activity-feed')] final readonly class ActivityFeedComponent implements LiveComponentContract { private ComponentId $id; private ActivityFeedState $state; // ... constructor, getId(), getData(), getRenderData() ... #[Action] public function addActivity( string $activityType, string $userId, string $userName, string $content ): ComponentData { // ... implementation ... } #[Action] public function markAsRead(string $activityId): ComponentData { // ... implementation ... } #[Action] public function setFilter(string $filter): ComponentData { // ... implementation ... } /** * Get SSE channel for real-time activity updates * * Uses context-based channel for flexible activity tracking. * * Examples: * - "activity-feed:user-123" → "activity:user-123" (user-specific) * - "activity-feed:global" → "activity:global" (public feed) * - "activity-feed:team-5" → "activity:team-5" (team-specific) */ public function getSseChannel(): string { $context = $this->id->instanceId; return "activity:{$context}"; } } ``` ### Beispiel 4: Live Presence (Presence Channel) ```php #[LiveComponent('live-presence')] final readonly class LivePresenceComponent implements LiveComponentContract { private ComponentId $id; private LivePresenceState $state; // ... constructor, getId(), getData(), getRenderData() ... #[Action] public function userJoined(string $userId, string $userName): ComponentData { $newState = $this->state->withUserJoined($userId, $userName); return $newState->toComponentData(); } #[Action] public function userLeft(string $userId): ComponentData { $newState = $this->state->withUserLeft($userId); return $newState->toComponentData(); } /** * Get SSE channel for real-time presence updates * * Uses presence-scoped channel for room-based tracking. * * Examples: * - "live-presence:lobby" → "presence:lobby" * - "live-presence:room-5" → "presence:room-5" * - "live-presence:global" → "presence:global" */ public function getSseChannel(): string { $room = $this->id->instanceId; return "presence:{$room}"; } } ``` ## Testing ### Unit Tests Test die getSseChannel() Methode: ```php use App\Framework\LiveComponents\ValueObjects\ComponentId; it('returns correct SSE channel for user', function () { $component = new NotificationCenterComponent( id: ComponentId::fromString('notification-center:user-123') ); expect($component->getSseChannel())->toBe('user:user-123'); }); it('returns global channel for guest users', function () { $component = new NotificationCenterComponent( id: ComponentId::fromString('notification-center:guest') ); expect($component->getSseChannel())->toBe('global'); }); ``` ### Integration Tests Test die vollständige SSE-Integration: ```php it('component renders with data-sse-channel attribute', function () { $component = new NotificationCenterComponent( id: ComponentId::fromString('notification-center:user-123') ); $html = $this->componentRegistry->renderWithWrapper($component); expect($html)->toContain('data-sse-channel="user:user-123"'); }); it('component receives SSE updates', function () { // 1. Create component $component = new NotificationCenterComponent( id: ComponentId::fromString('notification-center:user-123') ); // 2. Trigger action (dispatches ComponentUpdatedEvent) $newState = $component->markAsRead('notification-1'); // 3. Verify SSE event was broadcasted $this->assertSseBroadcasted( channel: 'user:user-123', componentId: 'notification-center:user-123', eventType: 'component-update' ); }); ``` ### Manual Browser Testing 1. **Öffne Browser DevTools** 2. **Network Tab** → Filter: "eventsource" 3. **Lade Seite** mit SSE-Component 4. **Prüfe Connection**: `/sse/events/{channel}` sollte sichtbar sein 5. **Trigger Action**: Klicke Button 6. **Prüfe Console**: SSE-Events sollten geloggt werden ## Troubleshooting ### Problem: Component verbindet sich nicht zu SSE **Symptome**: - Keine SSE-Connection in Network Tab - Keine Console-Logs über SSE **Diagnose**: ```php // Check 1: Hat Component getSseChannel()? if (method_exists($component, 'getSseChannel')) { echo "✅ getSseChannel() exists\n"; echo "Channel: " . $component->getSseChannel() . "\n"; } else { echo "❌ getSseChannel() missing\n"; } // Check 2: Wird data-sse-channel im HTML gerendert? $html = $componentRegistry->renderWithWrapper($component); if (str_contains($html, 'data-sse-channel')) { echo "✅ data-sse-channel attribute present\n"; } else { echo "❌ data-sse-channel attribute missing\n"; } ``` **Lösung**: 1. Implementiere `getSseChannel()` Methode 2. Stelle sicher dass Component über `ComponentRegistry::renderWithWrapper()` gerendert wird ### Problem: Updates kommen nicht an **Symptome**: - SSE-Connection existiert - Aber Component updated nicht bei Actions **Diagnose**: ```javascript // Browser Console const sseClient = window.liveComponentManager.sseClients.get('user:user-123'); console.log('SSE Connected:', sseClient?.isConnected); console.log('Component Channels:', Array.from(window.liveComponentManager.componentChannels)); ``` **Lösung**: 1. Prüfe ob Actions `#[Action]` Attribute haben 2. Prüfe ob `ComponentUpdatedEvent` dispatched wird 3. Prüfe Channel-Matching: Component channel === SSE channel ### Problem: Reconnection-Loops **Symptome**: - SSE disconnected/reconnected ständig - Console voll mit Reconnect-Meldungen **Diagnose**: ```bash # Server-Logs prüfen docker exec php tail -f storage/logs/app.log | grep SSE ``` **Lösung**: - Server-Side Exceptions fixen (meist in `/sse/events/{channel}` Route) - Heartbeat-Timeout erhöhen falls Server langsam antwortet ### Problem: Multiple Connections für Same Channel **Symptome**: - Network Tab zeigt mehrere `/sse/events/{channel}` Connections - Memory Leaks **Diagnose**: ```javascript // Browser Console console.log('SSE Clients:', window.liveComponentManager.sseClients.size); // Sollte 1 sein für 1 Channel ``` **Lösung**: - LiveComponentManager macht automatisch Connection Pooling - Prüfe ob Components richtig cleanup'd werden (keine Memory Leaks) ## Best Practices ### ✅ DO: Spezifische Channels ```php // ✅ Good: Spezifisch public function getSseChannel(): string { return "comments:post-{$this->postId}"; } // ❌ Bad: Zu generisch public function getSseChannel(): string { return "all-updates"; // Zu breit, zu viele Events } ``` ### ✅ DO: Dokumentiere Channel-Strategie ```php /** * Get SSE channel for real-time updates * * Uses user-specific channel for private notifications. * Falls back to 'global' for guest users. * * Channel format: "user:{userId}" or "global" * * Examples: * - "notification-center:user-123" → "user:user-123" * - "notification-center:guest" → "global" */ public function getSseChannel(): string { // ... implementation } ``` ### ✅ DO: Verwende $this->id->instanceId ```php // ✅ Good: Korrekte Property $instanceId = $this->id->instanceId; // ❌ Bad: Falsches Property (existiert nicht) $instanceId = $this->id->identifier; ``` ### ✅ DO: Fallback für Global/Guest ```php public function getSseChannel(): string { $instanceId = $this->id->instanceId; // Fallback für Gäste if ($instanceId === 'global' || $instanceId === 'guest') { return 'global'; } return "user:{$instanceId}"; } ``` ### ✅ DO: Test SSE Integration ```php it('has getSseChannel method', function () { $component = new MyComponent(ComponentId::fromString('my:test')); expect(method_exists($component, 'getSseChannel'))->toBeTrue(); }); it('returns correct channel', function () { $component = new MyComponent(ComponentId::fromString('my:user-123')); expect($component->getSseChannel())->toBe('user:user-123'); }); ``` ### ❌ DON'T: Hardcode User IDs ```php // ❌ Bad: Hardcoded User ID public function getSseChannel(): string { return "user:123"; // Falsch! } // ✅ Good: Dynamisch aus Component ID extrahieren public function getSseChannel(): string { return "user:{$this->id->instanceId}"; } ``` ### ❌ DON'T: Vergiss #[Action] Attribute ```php // ❌ Bad: Kein Action-Attribute public function markAsRead(string $id): ComponentData { // Wird nicht als Action erkannt } // ✅ Good: Mit Action-Attribute #[Action] public function markAsRead(string $id): ComponentData { // Wird als Action erkannt und dispatched ComponentUpdatedEvent } ``` ## Advanced Topics ### Custom SSE Events Broadcasting Manchmal möchtest du SSE-Updates von außerhalb einer Component-Action triggern: ```php use App\Framework\Sse\ValueObjects\SseChannel; use App\Framework\Sse\ValueObjects\SseEvent; use App\Framework\Sse\SseConnectionPool; final readonly class NotificationService { public function __construct( private SseConnectionPool $connectionPool ) {} public function sendNotification(string $userId, string $message): void { // 1. Save notification to database $this->notificationRepository->create($userId, $message); // 2. Broadcast SSE update $channel = new SseChannel("user:{$userId}"); $event = SseEvent::componentUpdate( componentId: "notification-center:user-{$userId}", state: ['notifications' => $this->getNotifications($userId)], html: $this->renderNotifications($userId) ); $this->connectionPool->broadcast($channel, $event); } } ``` ### Partial Fragment Updates Für Performance kannst du nur Teile der Component updaten: ```php $event = SseEvent::componentFragments( componentId: "notification-center:user-123", fragments: [ '#unread-count' => '5', '#notification-list' => '