Files
michaelschiemer/docs/claude/livecomponent-lifecycle-hooks.md
Michael Schiemer fc3d7e6357 feat(Production): Complete production deployment infrastructure
- 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.
2025-10-25 19:18:37 +02:00

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 $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:

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:

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:

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:

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