Files
michaelschiemer/docs/claude/sse-integration-guide.md
Michael Schiemer fc3d7e6357 feat(Production): Complete production deployment infrastructure
- Add comprehensive health check system with multiple endpoints
- Add Prometheus metrics endpoint
- Add production logging configurations (5 strategies)
- Add complete deployment documentation suite:
  * QUICKSTART.md - 30-minute deployment guide
  * DEPLOYMENT_CHECKLIST.md - Printable verification checklist
  * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle
  * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference
  * production-logging.md - Logging configuration guide
  * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation
  * README.md - Navigation hub
  * DEPLOYMENT_SUMMARY.md - Executive summary
- Add deployment scripts and automation
- Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment
- Update README with production-ready features

All production infrastructure is now complete and ready for deployment.
2025-10-25 19:18:37 +02:00

979 lines
24 KiB
Markdown

# 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' => '<span class="badge">5</span>',
'#notification-list' => '<div>...</div>'
]
);
$this->connectionPool->broadcast($channel, $event);
```
### Server-Side Channel Filtering
Validiere Channel-Zugriff auf Server-Side:
```php
#[Route('/sse/events/{channel}', method: Method::GET)]
public function events(string $channel, HttpRequest $request): StreamedResponse
{
// Validiere User-Specific Channels
if (str_starts_with($channel, 'user:')) {
$requestedUserId = substr($channel, 5);
$actualUserId = $request->user->id;
if ($requestedUserId !== $actualUserId) {
throw new UnauthorizedException('Cannot access other user channels');
}
}
// ... continue with SSE connection
}
```
## Zusammenfassung
SSE-Integration in LiveComponents ist **einfach und automatisch**:
1.**Add `#[Action]` Attribute** zu allen Action-Methoden
2.**Implement `getSseChannel()`** mit passender Channel-Strategie
3.**Test** die Integration mit Unit- und Browser-Tests
4.**Component verbindet sich automatisch** und empfängt Updates
**Das war's!** Keine JavaScript-Code erforderlich, keine manuelle Event-Handling. Das Framework kümmert sich um:
- Auto-Connection beim Component-Mount
- Auto-Reconnection bei Verbindungsproblemen
- Connection Pooling für Performance
- DOM-Updates via LiveComponentManager
**Next Steps**:
- Siehe [SSE System Documentation](./sse-system.md) für technische Details
- Siehe [LiveComponents System](./livecomponents-system.md) für Component-Entwicklung