- 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
14 KiB
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:
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:
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
$stateParameter) - 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:
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:
- Client-Side MutationObserver erkennt DOM-Entfernung
- JavaScript ruft
/live-component/{id}/destroyEndpunkt auf - 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:
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.sendBeaconoderfetch - 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:
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:
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:
// 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:
#[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
// ✅ 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
// ✅ 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()
// ✅ 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
// ✅ 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):
// ✅ 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
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