Files
michaelschiemer/docs/livecomponents/livecomponent-lifecycle-hooks.md
Michael Schiemer 36ef2a1e2c
Some checks failed
🚀 Build & Deploy Image / Determine Build Necessity (push) Failing after 10m14s
🚀 Build & Deploy Image / Build Runtime Base Image (push) Has been skipped
🚀 Build & Deploy Image / Build Docker Image (push) Has been skipped
🚀 Build & Deploy Image / Run Tests & Quality Checks (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Staging (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Production (push) Has been skipped
Security Vulnerability Scan / Check for Dependency Changes (push) Failing after 11m25s
Security Vulnerability Scan / Composer Security Audit (push) Has been cancelled
fix: Gitea Traefik routing and connection pool optimization
- Remove middleware reference from Gitea Traefik labels (caused routing issues)
- Optimize Gitea connection pool settings (MAX_IDLE_CONNS=30, authentication_timeout=180s)
- Add explicit service reference in Traefik labels
- Fix intermittent 504 timeouts by improving PostgreSQL connection handling

Fixes Gitea unreachability via git.michaelschiemer.de
2025-11-09 14:46:15 +01:00

573 lines
14 KiB
Markdown

# 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