fix: Gitea Traefik routing and connection pool optimization
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
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
- 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
This commit is contained in:
572
docs/livecomponents/livecomponent-lifecycle-hooks.md
Normal file
572
docs/livecomponents/livecomponent-lifecycle-hooks.md
Normal file
@@ -0,0 +1,572 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user