- 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.
979 lines
24 KiB
Markdown
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
|