# LiveComponent Lifecycle Hooks Dokumentation des Lifecycle Hook Systems für LiveComponents. ## Übersicht Das Lifecycle Hook System bietet opt-in Callbacks für wichtige Lebenszyklus-Ereignisse einer LiveComponent: - **`onMount()`**: Aufgerufen einmalig nach erster Erstellung (server-side) - **`onUpdate()`**: Aufgerufen nach jeder State-Änderung (server-side) - **`onDestroy()`**: Aufgerufen vor Entfernung aus DOM (client-side mit server-call) ## LifecycleAware Interface Components müssen das `LifecycleAware` Interface implementieren um Lifecycle Hooks zu nutzen: ```php use App\Framework\LiveComponents\Contracts\LifecycleAware; use App\Framework\LiveComponents\Contracts\LiveComponentContract; #[LiveComponent(name: 'example')] final readonly class ExampleComponent implements LiveComponentContract, LifecycleAware { public function onMount(): void { // Initialization logic } public function onUpdate(): void { // React to state changes } public function onDestroy(): void { // Cleanup logic } } ``` **Wichtig**: Das Interface ist optional - Components die es nicht implementieren funktionieren weiterhin normal. ## Lifecycle Flow ``` 1. Component Creation (Initial Page Load) ↓ onMount() - called once ↓ 2. User Action (Button Click, Input Change) ↓ Action Execution ↓ State Validation ↓ onUpdate() - called after state change ↓ 3. Component Removal (Navigate Away, Remove Element) ↓ onDestroy() - called before removal ↓ Client-Side Cleanup ``` ## Hook Details ### onMount() **Wann aufgerufen**: Einmalig nach erster Component-Erstellung (server-side) **Trigger**: ComponentRegistry ruft Hook auf wenn `$state === null` (initial creation ohne Re-Hydration) **Use Cases**: - Timer oder Intervals starten - Datenbank-Connections öffnen - Events oder WebSockets subscriben - Externe Libraries initialisieren - Component Mount für Analytics loggen - Background Processes starten **Beispiel**: ```php public function onMount(): void { // Log component initialization error_log("TimerComponent mounted: {$this->id->toString()}"); // Initialize external resources $this->cache->remember("timer_{$this->id}", fn() => time()); // Subscribe to events $this->eventBus->subscribe('timer:tick', $this->handleTick(...)); } ``` **Wichtig**: - Wird NUR bei initialer Erstellung aufgerufen (kein `$state` Parameter) - Wird NICHT aufgerufen bei Re-Hydration mit existierendem State - Fehler werden geloggt aber brechen Component-Erstellung nicht ab ### onUpdate() **Wann aufgerufen**: Nach jeder Action die State aktualisiert (server-side) **Trigger**: LiveComponentHandler ruft Hook nach State-Validierung auf in `handle()` und `handleUpload()` Methoden **Use Cases**: - Auf State-Änderungen reagieren - Externe Ressourcen aktualisieren - Mit externen Services synchronisieren - State-Konsistenz validieren - State-Transitions loggen - Cache invalidieren **Beispiel**: ```php public function onUpdate(): void { $seconds = $this->data->get('seconds', 0); $isRunning = $this->data->get('isRunning', false); // Log state transitions error_log("Timer updated: {$seconds}s, running: " . ($isRunning ? 'yes' : 'no')); // Update external resources if ($isRunning) { $this->cache->set("timer_{$this->id}_last_active", time()); } // Trigger side effects if ($seconds >= 60) { $this->eventBus->dispatch(new TimerReachedMinuteEvent($this->id)); } } ``` **Wichtig**: - Wird nach JEDER Action aufgerufen (auch wenn State unverändert bleibt) - Wird nach State-Validierung aufgerufen (State ist garantiert valid) - Fehler werden geloggt aber brechen Action nicht ab ### onDestroy() **Wann aufgerufen**: Vor Component-Entfernung aus DOM (client-side mit server-call) **Trigger**: 1. Client-Side MutationObserver erkennt DOM-Entfernung 2. JavaScript ruft `/live-component/{id}/destroy` Endpunkt auf 3. Server-Side Controller ruft `onDestroy()` Hook auf **Use Cases**: - Timers und Intervals stoppen - Datenbank-Connections schließen - Events unsubscriben - Externe Ressourcen aufräumen - State vor Removal persistieren - Component-Entfernung loggen **Beispiel**: ```php public function onDestroy(): void { // Log component removal error_log("TimerComponent destroyed: {$this->id->toString()}"); // Persist final state $this->storage->save("timer_{$this->id}_final_state", $this->data->toArray()); // Cleanup subscriptions $this->eventBus->unsubscribe('timer:tick'); // Close connections $this->connection?->close(); } ``` **Wichtig**: - Best-effort delivery via `navigator.sendBeacon` oder `fetch` - Fehler brechen Destroy nicht ab (Component wird trotzdem entfernt) - Kann fehlschlagen bei Page Unload (Browser beendet Request) - Nur für kritisches Cleanup verwenden ## Client-Side Integration ### MutationObserver Setup Der LiveComponentManager JavaScript-Code überwacht automatisch DOM-Entfernungen: ```javascript setupLifecycleObserver(element, componentId) { const observer = new MutationObserver((mutations) => { if (!document.contains(element)) { this.callDestroyHook(componentId); observer.disconnect(); } }); if (element.parentNode) { observer.observe(element.parentNode, { childList: true, subtree: false }); } config.observer = observer; } ``` ### Server-Call mit navigator.sendBeacon Best-effort delivery auch während Page Unload: ```javascript async callDestroyHook(componentId) { const payload = JSON.stringify({ state: currentState, _csrf_token: csrfToken }); const url = `/live-component/${componentId}/destroy`; const blob = new Blob([payload], { type: 'application/json' }); // Try sendBeacon first for page unload reliability if (navigator.sendBeacon && navigator.sendBeacon(url, blob)) { console.log(`onDestroy() called via sendBeacon`); } else { // Fallback to fetch for normal removal await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }, body: payload }); } // Local cleanup this.destroy(componentId); } ``` ## Error Handling Alle Lifecycle Hooks haben robustes Error Handling: ```php // In LiveComponentHandler try { $component->onMount(); } catch (\Throwable $e) { // Log error but don't fail component creation error_log("Lifecycle hook onMount() failed for " . get_class($component) . ": " . $e->getMessage()); } ``` **Best Practices**: - Hooks sollten nie kritische Exceptions werfen - Internes Error Handling in Hook-Implementierungen - Logging für Debugging und Monitoring - Graceful Degradation bei Hook-Fehlern ## Timer Component Beispiel Vollständiges Beispiel einer Component mit allen Lifecycle Hooks: ```php #[LiveComponent(name: 'timer')] final readonly class TimerComponent implements LiveComponentContract, LifecycleAware { private ComponentData $data; public function __construct( private ComponentId $id, ?ComponentData $initialData = null ) { $this->data = $initialData ?? ComponentData::fromArray([ 'seconds' => 0, 'isRunning' => false, 'startedAt' => null, 'logs' => [] ]); } // Lifecycle Hooks public function onMount(): void { error_log("TimerComponent mounted: {$this->id->toString()}"); $this->addLog('Component mounted - Timer ready'); } public function onUpdate(): void { $seconds = $this->data->get('seconds', 0); $isRunning = $this->data->get('isRunning', false); error_log("TimerComponent updated: {$seconds}s, running: " . ($isRunning ? 'yes' : 'no')); } public function onDestroy(): void { error_log("TimerComponent destroyed: {$this->id->toString()}"); $this->addLog('Component destroyed - Cleanup completed'); } // Actions public function start(): ComponentData { $state = $this->data->toArray(); $state['isRunning'] = true; $state['startedAt'] = time(); $this->addLog('Timer started', $state); return ComponentData::fromArray($state); } public function stop(): ComponentData { $state = $this->data->toArray(); $state['isRunning'] = false; $this->addLog('Timer stopped', $state); return ComponentData::fromArray($state); } public function tick(): ComponentData { $state = $this->data->toArray(); if ($state['isRunning']) { $state['seconds'] = ($state['seconds'] ?? 0) + 1; } return ComponentData::fromArray($state); } // Standard Interface Methods public function getId(): ComponentId { return $this->id; } public function getData(): ComponentData { return $this->data; } public function getRenderData(): RenderData { return new RenderData( templatePath: 'livecomponent-timer', data: [ 'seconds' => $this->data->get('seconds', 0), 'isRunning' => $this->data->get('isRunning', false), 'formattedTime' => $this->formatTime($this->data->get('seconds', 0)), 'logs' => $this->data->get('logs', []) ] ); } // Helper Methods private function formatTime(int $seconds): string { $minutes = floor($seconds / 60); $remainingSeconds = $seconds % 60; return sprintf('%02d:%02d', $minutes, $remainingSeconds); } private function addLog(string $message, array &$state = null): void { if ($state === null) { $state = $this->data->toArray(); } $logs = $state['logs'] ?? []; $logs[] = [ 'time' => date('H:i:s'), 'message' => $message ]; // Keep only last 5 logs $state['logs'] = array_slice($logs, -5); } } ``` ## Best Practices ### 1. Minimal onMount() Logic ```php // ✅ Good: Light initialization public function onMount(): void { error_log("Component {$this->id} mounted"); $this->cache->set("mounted_{$this->id}", time(), 3600); } // ❌ Bad: Heavy computation public function onMount(): void { $this->processAllData(); // Slow! $this->generateReport(); // Blocks creation! } ``` ### 2. onUpdate() Performance ```php // ✅ Good: Quick updates public function onUpdate(): void { if ($this->data->get('isActive')) { $this->cache->touch("active_{$this->id}"); } } // ❌ Bad: Heavy synchronous operations public function onUpdate(): void { $this->syncWithExternalAPI(); // Slow! $this->recalculateEverything(); // Blocks action! } ``` ### 3. Critical Cleanup in onDestroy() ```php // ✅ Good: Essential cleanup public function onDestroy(): void { $this->connection?->close(); $this->persistState(); } // ❌ Bad: Nice-to-have cleanup public function onDestroy(): void { $this->updateStatistics(); // May fail during page unload $this->sendAnalytics(); // Not guaranteed to complete } ``` ### 4. Error Handling ```php // ✅ Good: Internal error handling public function onMount(): void { try { $this->externalService->connect(); } catch (\Exception $e) { error_log("Connection failed: " . $e->getMessage()); // Continue with degraded functionality } } // ❌ Bad: Letting exceptions bubble up public function onMount(): void { $this->externalService->connect(); // May throw! // Breaks component creation if it fails } ``` ### 5. Idempotency Hooks sollten idempotent sein (mehrfach ausführbar ohne Seiteneffekte): ```php // ✅ Good: Idempotent public function onMount(): void { if (!$this->cache->has("initialized_{$this->id}")) { $this->cache->set("initialized_{$this->id}", true); $this->initialize(); } } // ❌ Bad: Side effects on every call public function onMount(): void { $this->counter++; // Breaks on re-hydration! } ``` ## Testing Lifecycle Hooks ```php use Tests\Framework\LiveComponents\ComponentTestCase; uses(ComponentTestCase::class); beforeEach(function () { $this->setUpComponentTest(); }); it('calls onMount on initial creation', function () { $mountCalled = false; $component = new class (...) implements LifecycleAware { public function onMount(): void { $this->mountCalled = true; } }; // Initial creation (no state) $registry = $this->container->get(ComponentRegistry::class); $resolved = $registry->resolve($component->getId(), null); expect($mountCalled)->toBeTrue(); }); it('calls onUpdate after action', function () { $updateCalled = false; $component = new class (...) implements LifecycleAware { public function onUpdate(): void { $this->updateCalled = true; } }; $handler = $this->container->get(LiveComponentHandler::class); $params = ActionParameters::fromArray([ '_csrf_token' => $this->generateCsrfToken($component) ]); $handler->handle($component, 'action', $params); expect($updateCalled)->toBeTrue(); }); ``` ## Demo Eine vollständige Demo ist verfügbar unter: - **URL**: https://localhost/livecomponent-timer - **Component**: `src/Application/LiveComponents/Timer/TimerComponent.php` - **Template**: `src/Framework/View/templates/livecomponent-timer.view.php` Die Demo zeigt: - Alle drei Lifecycle Hooks in Aktion - Client-Side Tick Interval Management - Lifecycle Log-Tracking - Browser Console Logging für Hook-Aufrufe ## Zusammenfassung Das Lifecycle Hook System bietet: - ✅ **Opt-in Design**: Components können hooks nutzen ohne Breaking Changes - ✅ **Server-Side Hooks**: onMount() und onUpdate() mit voller Backend-Integration - ✅ **Client-Side Cleanup**: onDestroy() mit MutationObserver und sendBeacon - ✅ **Robustes Error Handling**: Fehler brechen Lifecycle nicht ab - ✅ **Best-Effort Delivery**: onDestroy() versucht Server-Call auch bei Page Unload - ✅ **Framework-Integration**: Nahtlos integriert mit ComponentRegistry und LiveComponentHandler **Use Cases**: - Resource Management (Connections, Timers, Subscriptions) - State Persistence und Synchronization - Analytics und Logging - External Service Integration - Performance Monitoring