- 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.
573 lines
14 KiB
Markdown
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
|