- 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.
24 KiB
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
use App\Framework\LiveComponents\Attributes\Action;
#[Action]
public function markAsRead(string $notificationId): ComponentData
{
// ... your logic ...
}
2. getSseChannel() Methode implementieren
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:
#[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:
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
/**
* 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
/**
* 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
/**
* 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:
// 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
- Öffne Browser DevTools (F12)
- Network Tab → Filter: "EventSource" oder "eventsource"
- Lade die Seite mit deiner Component
- Prüfe: Du solltest eine SSE-Verbindung zu
/sse/events/{channel}sehen - Trigger Action: Klicke auf einen Button der eine Action aufruft
- 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
public function getSseChannel(): string
{
return 'global';
}
Wann verwenden?
- ✅ Public Activity Feeds
- ✅ System Announcements
- ✅ Global Statistics
- ❌ User-spezifische Daten
Beispiel: Public Activity Feed
#[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
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
#[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)
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
#[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
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
#[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)
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)
#[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)
#[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)
#[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:
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:
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
- Öffne Browser DevTools
- Network Tab → Filter: "eventsource"
- Lade Seite mit SSE-Component
- Prüfe Connection:
/sse/events/{channel}sollte sichtbar sein - Trigger Action: Klicke Button
- 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:
// 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:
- Implementiere
getSseChannel()Methode - 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:
// 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:
- Prüfe ob Actions
#[Action]Attribute haben - Prüfe ob
ComponentUpdatedEventdispatched wird - Prüfe Channel-Matching: Component channel === SSE channel
Problem: Reconnection-Loops
Symptome:
- SSE disconnected/reconnected ständig
- Console voll mit Reconnect-Meldungen
Diagnose:
# 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:
// 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
// ✅ 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
/**
* 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
// ✅ Good: Korrekte Property
$instanceId = $this->id->instanceId;
// ❌ Bad: Falsches Property (existiert nicht)
$instanceId = $this->id->identifier;
✅ DO: Fallback für Global/Guest
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
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
// ❌ 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
// ❌ 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:
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:
$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:
#[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:
- ✅ Add
#[Action]Attribute zu allen Action-Methoden - ✅ Implement
getSseChannel()mit passender Channel-Strategie - ✅ Test die Integration mit Unit- und Browser-Tests
- ✅ 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 für technische Details
- Siehe LiveComponents System für Component-Entwicklung