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:
76
docs/claude/README.md
Normal file
76
docs/claude/README.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# Claude Documentation
|
||||
|
||||
Dieses Verzeichnis enthält AI-spezifische Dokumentation für Claude Code und andere AI-Agenten, die mit dem Framework arbeiten.
|
||||
|
||||
## Zweck
|
||||
|
||||
Die Dokumentation in diesem Verzeichnis ist speziell für AI-Agenten (wie Claude Code, Cursor AI, etc.) konzipiert und enthält:
|
||||
- Framework-spezifische Kontexte und Personas
|
||||
- MCP (Model Context Protocol) Integration
|
||||
- Development Commands für AI-Agenten
|
||||
- Coding Guidelines für AI-generierten Code
|
||||
- Code-Generierungs-Beispiele
|
||||
|
||||
## Allgemeine Framework-Dokumentation
|
||||
|
||||
Für allgemeine Framework-Dokumentation, die sowohl für Entwickler als auch AI-Agenten relevant ist, siehe:
|
||||
- [Framework Dokumentation](../README.md) - Hauptdokumentation
|
||||
- [Features](../features/) - Feature-spezifische Dokumentation
|
||||
- [Guides](../guides/) - Entwickleranleitungen
|
||||
- [LiveComponents](../livecomponents/) - LiveComponents Dokumentation
|
||||
|
||||
## Wichtige Dokumentationen
|
||||
|
||||
### MCP Integration
|
||||
- [MCP Integration](mcp-integration.md) - Model Context Protocol Server und Tools
|
||||
|
||||
### Framework Personas
|
||||
- [Framework Personas](framework-personas.md) - AI Personas für Framework-Entwicklung
|
||||
|
||||
### Development Commands
|
||||
- [Development Commands](development-commands.md) - Claude Code Commands und Workflows
|
||||
|
||||
### Coding Guidelines
|
||||
- [Guidelines](guidelines.md) - AI Coding Guidelines und Best Practices
|
||||
- [Architecture](architecture.md) - Framework-Architektur für AI-Agenten
|
||||
- [Naming Conventions](naming-conventions.md) - Namenskonventionen
|
||||
|
||||
### Code Generation Examples
|
||||
- [Examples](examples/) - Code-Generierungs-Beispiele
|
||||
|
||||
## Weitere AI-spezifische Dokumentationen
|
||||
|
||||
- [Performance Profiling](performance-profiling.md) - Performance-Analyse für AI-Agenten
|
||||
- [Structured Logging](structured-logging.md) - Logging-Patterns
|
||||
- [PHP 8.5 Integration](php85-framework-integration.md) - PHP 8.5 Features
|
||||
- [PostgreSQL Features](postgresql-features.md) - PostgreSQL-spezifische Features
|
||||
- [SSE System](sse-system.md) - Server-Sent Events
|
||||
- [SSE Integration Guide](sse-integration-guide.md) - SSE Integration
|
||||
- [Sockets Module](sockets-module.md) - Socket-Kommunikation
|
||||
- [Magic Links System](magiclinks-system.md) - Magic Links Implementation
|
||||
- [View Caching System](view-caching-system.md) - View-Caching
|
||||
- [View Refactoring Plan](view-refactoring-plan.md) - View-Refactoring
|
||||
- [X-Component Syntax](x-component-syntax.md) - X-Component Template Syntax
|
||||
- [XComponent Processor](xcomponent-processor.md) - XComponent Processing
|
||||
- [Animation System](animationsystem.md) - Animation Framework
|
||||
- [Chips & Cookies](chips-cookies.md) - Cookie Management
|
||||
- [Curl OOP API](curl-oop-api.md) - HTTP Client API
|
||||
- [Deployment Architecture](deployment-architecture.md) - Deployment-Architektur
|
||||
- [ML Framework Architecture](ml-framework-architecture.md) - Machine Learning Framework
|
||||
- [POSIX System](posix-system.md) - POSIX-Integration
|
||||
- [Typed String System](typed-string-system.md) - Typed String Value Objects
|
||||
- [Framework Refactoring Recommendations](framework-refactoring-recommendations.md) - Refactoring-Empfehlungen
|
||||
|
||||
## Verwendung
|
||||
|
||||
Diese Dokumentation wird automatisch von AI-Agenten verwendet, die mit dem Framework arbeiten. Sie sollte nicht manuell bearbeitet werden, es sei denn, es handelt sich um AI-spezifische Kontexte oder Beispiele.
|
||||
|
||||
## Migration
|
||||
|
||||
Viele Dokumentationen wurden von diesem Verzeichnis in die allgemeine Dokumentationsstruktur migriert:
|
||||
- Feature-Dokumentationen → `docs/features/`
|
||||
- Guides → `docs/guides/`
|
||||
- LiveComponents → `docs/livecomponents/`
|
||||
|
||||
Siehe [Dokumentationsanalyse](../DOCUMENTATION-ANALYSIS.md) für Details zur Reorganisation.
|
||||
|
||||
585
docs/claude/animationsystem.md
Normal file
585
docs/claude/animationsystem.md
Normal file
@@ -0,0 +1,585 @@
|
||||
# Animationssystem für Console-Modul
|
||||
|
||||
## Übersicht
|
||||
|
||||
Das Animationssystem bietet eine umfassende Lösung für Animationen im Console-Modul. Es unterstützt sowohl UI-Elemente in der TUI als auch Text-Animationen im Console-Output. Das System ist modular aufgebaut, erweiterbar und vollständig in den EventLoop integriert.
|
||||
|
||||
## Features
|
||||
|
||||
- **Mehrere Animationstypen**: Fade-In/Out, Slide, Typewriter, Marquee, Pulse
|
||||
- **Keyframe-basierte Animationen**: Komplexe Animationen mit Easing-Functions
|
||||
- **Composite Animationen**: Kombination mehrerer Animationen (parallel oder sequenziell)
|
||||
- **EventLoop Integration**: Automatische Updates im Render-Loop
|
||||
- **TUI & Console-Output**: Unterstützung für beide Anwendungsfälle
|
||||
- **Factory & Builder Pattern**: Einfache Erstellung von Animationen
|
||||
|
||||
## Architektur
|
||||
|
||||
### Core-Komponenten
|
||||
|
||||
#### Animation Interface
|
||||
|
||||
Basis-Interface für alle Animationen:
|
||||
|
||||
```php
|
||||
interface Animation
|
||||
{
|
||||
public function start(): void;
|
||||
public function stop(): void;
|
||||
public function pause(): void;
|
||||
public function resume(): void;
|
||||
public function update(float $deltaTime): bool;
|
||||
public function isActive(): bool;
|
||||
public function getProgress(): float;
|
||||
public function getDuration(): float;
|
||||
public function getDelay(): float;
|
||||
public function isLooping(): bool;
|
||||
public function onStart(callable $callback): self;
|
||||
public function onComplete(callable $callback): self;
|
||||
public function onPause(callable $callback): self;
|
||||
public function onResume(callable $callback): self;
|
||||
}
|
||||
```
|
||||
|
||||
#### AnimationManager
|
||||
|
||||
Verwaltet mehrere Animationen gleichzeitig und aktualisiert sie automatisch:
|
||||
|
||||
```php
|
||||
$animationManager = new AnimationManager();
|
||||
|
||||
// Animation hinzufügen
|
||||
$animationManager->add($animation);
|
||||
|
||||
// Animationen aktualisieren (wird automatisch vom EventLoop aufgerufen)
|
||||
$animationManager->update($deltaTime);
|
||||
|
||||
// Animation entfernen
|
||||
$animationManager->remove($animation);
|
||||
|
||||
// Alle Animationen löschen
|
||||
$animationManager->clear();
|
||||
```
|
||||
|
||||
#### Easing Functions
|
||||
|
||||
Unterstützte Easing-Functions:
|
||||
|
||||
- `LINEAR` - Lineare Interpolation
|
||||
- `EASE_IN` - Langsam starten
|
||||
- `EASE_OUT` - Langsam enden
|
||||
- `EASE_IN_OUT` - Langsam starten und enden
|
||||
- `EASE_IN_QUAD` - Quadratische Beschleunigung
|
||||
- `EASE_OUT_QUAD` - Quadratische Verzögerung
|
||||
- `EASE_IN_OUT_QUAD` - Quadratische Beschleunigung und Verzögerung
|
||||
- `BOUNCE` - Bounce-Effekt
|
||||
- `ELASTIC` - Elastischer Effekt
|
||||
|
||||
## Animation-Typen
|
||||
|
||||
### FadeAnimation
|
||||
|
||||
Fade-In/Out Effekte für Text oder UI-Elemente:
|
||||
|
||||
```php
|
||||
use App\Framework\Console\Animation\AnimationFactory;
|
||||
use App\Framework\Console\Animation\EasingFunction;
|
||||
|
||||
// Fade-In
|
||||
$fadeIn = AnimationFactory::fadeIn(1.0, EasingFunction::EASE_IN);
|
||||
|
||||
// Fade-Out
|
||||
$fadeOut = AnimationFactory::fadeOut(1.0, EasingFunction::EASE_OUT);
|
||||
|
||||
// Custom Fade
|
||||
$fade = AnimationFactory::fade(
|
||||
duration: 2.0,
|
||||
startOpacity: 0.0,
|
||||
endOpacity: 1.0,
|
||||
easing: EasingFunction::EASE_IN_OUT
|
||||
);
|
||||
```
|
||||
|
||||
### SlideAnimation
|
||||
|
||||
Slide-Effekte in verschiedene Richtungen:
|
||||
|
||||
```php
|
||||
use App\Framework\Console\Animation\Types\SlideDirection;
|
||||
|
||||
// Von links
|
||||
$slideFromLeft = AnimationFactory::slideInFromLeft(
|
||||
distance: 20,
|
||||
duration: 1.0
|
||||
);
|
||||
|
||||
// Von rechts
|
||||
$slideFromRight = AnimationFactory::slideInFromRight(
|
||||
distance: 20,
|
||||
duration: 1.0
|
||||
);
|
||||
|
||||
// Custom Slide
|
||||
$slide = AnimationFactory::slide(
|
||||
direction: SlideDirection::UP,
|
||||
distance: 10,
|
||||
duration: 0.5,
|
||||
easing: EasingFunction::EASE_OUT
|
||||
);
|
||||
```
|
||||
|
||||
### TypewriterAnimation
|
||||
|
||||
Typewriter-Effekt für Text:
|
||||
|
||||
```php
|
||||
$typewriter = AnimationFactory::typewriter(
|
||||
text: 'Hello, this is a typewriter animation!',
|
||||
charactersPerSecond: 10.0
|
||||
);
|
||||
|
||||
// Schnell
|
||||
$fast = AnimationFactory::typewriter('Fast text', 20.0);
|
||||
|
||||
// Langsam
|
||||
$slow = AnimationFactory::typewriter('Slow text', 5.0);
|
||||
```
|
||||
|
||||
### MarqueeAnimation
|
||||
|
||||
Scrolling Text (Marquee):
|
||||
|
||||
```php
|
||||
$marquee = AnimationFactory::marquee(
|
||||
text: 'This is a scrolling marquee text',
|
||||
width: 80,
|
||||
speed: 1.0,
|
||||
loop: true
|
||||
);
|
||||
|
||||
// Schnell
|
||||
$fastMarquee = AnimationFactory::marquee('Fast scrolling', 50, 5.0);
|
||||
|
||||
// Langsam
|
||||
$slowMarquee = AnimationFactory::marquee('Slow scrolling', 50, 0.5);
|
||||
```
|
||||
|
||||
### PulseAnimation
|
||||
|
||||
Pulsing-Effekte für Hervorhebungen:
|
||||
|
||||
```php
|
||||
$pulse = AnimationFactory::pulse(
|
||||
duration: 1.0,
|
||||
scaleStart: 1.0,
|
||||
scaleEnd: 1.2,
|
||||
pulseSpeed: 2.0
|
||||
);
|
||||
|
||||
// Sanft
|
||||
$gentle = PulseAnimation::gentle(2.0);
|
||||
|
||||
// Stark
|
||||
$strong = PulseAnimation::strong(1.0);
|
||||
```
|
||||
|
||||
### KeyframeAnimation
|
||||
|
||||
Keyframe-basierte Animationen mit komplexen Interpolationen:
|
||||
|
||||
```php
|
||||
use App\Framework\Console\Animation\AnimationFrame;
|
||||
|
||||
$keyframes = [
|
||||
new AnimationFrame(0.0, 0, EasingFunction::EASE_IN),
|
||||
new AnimationFrame(0.5, 100, EasingFunction::EASE_IN_OUT),
|
||||
new AnimationFrame(1.0, 200, EasingFunction::EASE_OUT),
|
||||
];
|
||||
|
||||
$keyframeAnimation = AnimationFactory::keyframe(
|
||||
keyframes: $keyframes,
|
||||
duration: 2.0,
|
||||
loop: false
|
||||
);
|
||||
```
|
||||
|
||||
### CompositeAnimation
|
||||
|
||||
Kombination mehrerer Animationen:
|
||||
|
||||
```php
|
||||
use App\Framework\Console\Animation\CompositeAnimation;
|
||||
use App\Framework\Console\Animation\SequenceType;
|
||||
|
||||
$fadeIn = AnimationFactory::fadeIn(0.5);
|
||||
$slide = AnimationFactory::slideInFromLeft(20, 0.5);
|
||||
|
||||
// Parallel (gleichzeitig)
|
||||
$parallel = new CompositeAnimation(
|
||||
animations: [$fadeIn, $slide],
|
||||
sequenceType: SequenceType::PARALLEL,
|
||||
loop: false
|
||||
);
|
||||
|
||||
// Sequenziell (nacheinander)
|
||||
$sequential = new CompositeAnimation(
|
||||
animations: [$fadeIn, $slide],
|
||||
sequenceType: SequenceType::SEQUENTIAL,
|
||||
loop: false
|
||||
);
|
||||
```
|
||||
|
||||
### SpinnerAnimation
|
||||
|
||||
Animation-basierter Spinner:
|
||||
|
||||
```php
|
||||
use App\Framework\Console\Animation\Types\SpinnerAnimation;
|
||||
use App\Framework\Console\SpinnerStyle;
|
||||
|
||||
$spinner = AnimationFactory::spinner(
|
||||
frames: SpinnerStyle::DOTS,
|
||||
message: 'Loading...',
|
||||
frameInterval: 0.1
|
||||
);
|
||||
|
||||
// Oder direkt
|
||||
$spinner = SpinnerAnimation::fromStyle(
|
||||
style: SpinnerStyle::BARS,
|
||||
message: 'Processing...',
|
||||
frameInterval: 0.1
|
||||
);
|
||||
```
|
||||
|
||||
## Verwendung in Console-Output
|
||||
|
||||
### Einfache Text-Animationen
|
||||
|
||||
```php
|
||||
use App\Framework\Console\ConsoleOutput;
|
||||
|
||||
$output = new ConsoleOutput();
|
||||
|
||||
// Fade-In
|
||||
$output->animateFadeIn('Hello World!', 1.0);
|
||||
|
||||
// Fade-Out
|
||||
$output->animateFadeOut('Goodbye!', 1.0);
|
||||
|
||||
// Typewriter
|
||||
$output->animateTypewriter('This appears character by character', 10.0);
|
||||
|
||||
// Marquee
|
||||
$output->animateMarquee('Scrolling text', 80, 1.0);
|
||||
|
||||
// Custom Animation
|
||||
$customAnimation = AnimationFactory::fadeIn(2.0);
|
||||
$output->animateText('Custom animated text', $customAnimation);
|
||||
|
||||
// Animationen aktualisieren
|
||||
$output->updateAnimations(0.016); // ~60 FPS
|
||||
```
|
||||
|
||||
### Animation Manager Zugriff
|
||||
|
||||
```php
|
||||
$animationManager = $output->getAnimationManager();
|
||||
|
||||
// Animationen direkt hinzufügen
|
||||
$animationManager->add($animation);
|
||||
|
||||
// Animationen aktualisieren
|
||||
$animationManager->update($deltaTime);
|
||||
```
|
||||
|
||||
## Verwendung in TUI
|
||||
|
||||
### TuiAnimationRenderer
|
||||
|
||||
```php
|
||||
use App\Framework\Console\Animation\TuiAnimationRenderer;
|
||||
use App\Framework\Console\Animation\AnimationFactory;
|
||||
use App\Framework\Console\Animation\Types\SlideDirection;
|
||||
|
||||
$animationRenderer = $tuiRenderer->getAnimationRenderer();
|
||||
|
||||
if ($animationRenderer !== null) {
|
||||
// Element animieren
|
||||
$animationRenderer->fadeInElement('menu-item-1', 0.5);
|
||||
$animationRenderer->slideInElement(
|
||||
'button-1',
|
||||
SlideDirection::LEFT,
|
||||
20,
|
||||
0.5
|
||||
);
|
||||
$animationRenderer->pulseElement('highlight-1', 1.0);
|
||||
|
||||
// Custom Animation
|
||||
$customAnimation = AnimationFactory::fadeIn(1.0);
|
||||
$animationRenderer->animateElement('element-id', $customAnimation);
|
||||
|
||||
// Animation stoppen
|
||||
$animationRenderer->stopElement('element-id');
|
||||
|
||||
// Animationswerte abrufen
|
||||
$opacity = $animationRenderer->getElementOpacity('element-id');
|
||||
$position = $animationRenderer->getElementPosition('element-id');
|
||||
$scale = $animationRenderer->getElementScale('element-id');
|
||||
}
|
||||
```
|
||||
|
||||
### Integration in TuiRenderer
|
||||
|
||||
Der `TuiRenderer` aktualisiert Animationen automatisch im Render-Loop:
|
||||
|
||||
```php
|
||||
// In TuiRenderer::render()
|
||||
if ($this->animationRenderer !== null) {
|
||||
$this->animationRenderer->update(0.016); // ~60 FPS
|
||||
}
|
||||
```
|
||||
|
||||
## Factory & Builder Pattern
|
||||
|
||||
### AnimationFactory
|
||||
|
||||
Einfache Erstellung von Animationen:
|
||||
|
||||
```php
|
||||
use App\Framework\Console\Animation\AnimationFactory;
|
||||
|
||||
$fadeIn = AnimationFactory::fadeIn(1.0);
|
||||
$fadeOut = AnimationFactory::fadeOut(1.0);
|
||||
$slide = AnimationFactory::slideInFromLeft(20, 1.0);
|
||||
$typewriter = AnimationFactory::typewriter('Text', 10.0);
|
||||
$marquee = AnimationFactory::marquee('Text', 80, 1.0);
|
||||
$pulse = AnimationFactory::pulse(1.0);
|
||||
$spinner = AnimationFactory::spinner(SpinnerStyle::DOTS, 'Loading...');
|
||||
$keyframe = AnimationFactory::keyframe($keyframes, 2.0);
|
||||
```
|
||||
|
||||
### AnimationBuilder
|
||||
|
||||
Fluent Builder für komplexe Konfigurationen:
|
||||
|
||||
```php
|
||||
use App\Framework\Console\Animation\AnimationBuilder;
|
||||
|
||||
$animation = AnimationBuilder::fade(0.0, 1.0)
|
||||
->duration(2.0)
|
||||
->delay(0.5)
|
||||
->loop(false)
|
||||
->easing(EasingFunction::EASE_IN_OUT)
|
||||
->onStart(function ($anim) {
|
||||
echo "Animation started\n";
|
||||
})
|
||||
->onComplete(function ($anim) {
|
||||
echo "Animation completed\n";
|
||||
})
|
||||
->onPause(function ($anim) {
|
||||
echo "Animation paused\n";
|
||||
})
|
||||
->onResume(function ($anim) {
|
||||
echo "Animation resumed\n";
|
||||
})
|
||||
->build();
|
||||
```
|
||||
|
||||
## EventLoop Integration
|
||||
|
||||
Der `EventLoop` kann automatisch Animationen aktualisieren:
|
||||
|
||||
```php
|
||||
use App\Framework\Console\Components\EventLoop\EventLoop;
|
||||
use App\Framework\Console\Components\EventLoop\EventLoopConfig;
|
||||
use App\Framework\Console\Animation\AnimationManager;
|
||||
|
||||
$animationManager = new AnimationManager();
|
||||
$eventBuffer = new EventBuffer();
|
||||
$config = new EventLoopConfig();
|
||||
|
||||
// EventLoop mit AnimationManager erstellen
|
||||
$eventLoop = new EventLoop($eventBuffer, $config, $animationManager);
|
||||
|
||||
// Animationen werden automatisch aktualisiert
|
||||
// Oder manuell:
|
||||
$eventLoop->updateAnimations(0.016);
|
||||
```
|
||||
|
||||
## Callbacks
|
||||
|
||||
Animationen unterstützen Callbacks für verschiedene Events:
|
||||
|
||||
```php
|
||||
$animation = AnimationFactory::fadeIn(1.0)
|
||||
->onStart(function (Animation $anim) {
|
||||
echo "Animation started\n";
|
||||
})
|
||||
->onComplete(function (Animation $anim) {
|
||||
echo "Animation completed\n";
|
||||
})
|
||||
->onPause(function (Animation $anim) {
|
||||
echo "Animation paused\n";
|
||||
})
|
||||
->onResume(function (Animation $anim) {
|
||||
echo "Animation resumed\n";
|
||||
});
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Performance
|
||||
|
||||
1. **Animation Manager wiederverwenden**: Erstellen Sie einen `AnimationManager` pro Anwendung und verwenden Sie ihn für alle Animationen.
|
||||
|
||||
2. **Animationen entfernen**: Entfernen Sie abgeschlossene Animationen, um Speicher zu sparen:
|
||||
```php
|
||||
$animation->onComplete(function ($anim) use ($manager) {
|
||||
$manager->remove($anim);
|
||||
});
|
||||
```
|
||||
|
||||
3. **Update-Frequenz**: Aktualisieren Sie Animationen mit ~60 FPS (0.016 Sekunden):
|
||||
```php
|
||||
$animationManager->update(0.016);
|
||||
```
|
||||
|
||||
### Code-Organisation
|
||||
|
||||
1. **Factory verwenden**: Verwenden Sie `AnimationFactory` für einfache Animationen statt direkte Instanziierung.
|
||||
|
||||
2. **Builder für komplexe Animationen**: Verwenden Sie `AnimationBuilder` für komplexe Konfigurationen.
|
||||
|
||||
3. **Keyframes für komplexe Animationen**: Verwenden Sie `KeyframeAnimation` für komplexe Animationen mit mehreren Zuständen.
|
||||
|
||||
### Fehlerbehandlung
|
||||
|
||||
```php
|
||||
try {
|
||||
$animation = AnimationFactory::fadeIn(1.0);
|
||||
$animationManager->add($animation);
|
||||
$animation->start();
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
// Handle invalid animation parameters
|
||||
error_log("Animation error: " . $e->getMessage());
|
||||
}
|
||||
```
|
||||
|
||||
## Beispiel: Demo Command
|
||||
|
||||
Ein vollständiges Beispiel finden Sie in:
|
||||
|
||||
`src/Framework/Console/Examples/AnimationDemoCommand.php`
|
||||
|
||||
Ausführen mit:
|
||||
|
||||
```bash
|
||||
php console.php demo:animation
|
||||
```
|
||||
|
||||
## Erweiterung
|
||||
|
||||
### Neue Animation-Typen hinzufügen
|
||||
|
||||
1. Erstellen Sie eine neue Klasse in `src/Framework/Console/Animation/Types/`
|
||||
2. Implementieren Sie das `Animation` Interface oder erweitern Sie `BaseAnimation`
|
||||
3. Implementieren Sie `updateAnimation(float $progress)` und `getCurrentValue()`
|
||||
4. Fügen Sie Factory-Methoden zu `AnimationFactory` hinzu
|
||||
|
||||
Beispiel:
|
||||
|
||||
```php
|
||||
final class CustomAnimation extends BaseAnimation
|
||||
{
|
||||
protected function updateAnimation(float $progress): void
|
||||
{
|
||||
// Custom animation logic
|
||||
}
|
||||
|
||||
public function getCurrentValue(): mixed
|
||||
{
|
||||
// Return current animated value
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API-Referenz
|
||||
|
||||
### Animation Interface
|
||||
|
||||
- `start(): void` - Startet die Animation
|
||||
- `stop(): void` - Stoppt die Animation
|
||||
- `pause(): void` - Pausiert die Animation
|
||||
- `resume(): void` - Setzt die Animation fort
|
||||
- `update(float $deltaTime): bool` - Aktualisiert die Animation
|
||||
- `isActive(): bool` - Prüft ob Animation aktiv ist
|
||||
- `getProgress(): float` - Gibt Fortschritt zurück (0.0-1.0)
|
||||
- `getDuration(): float` - Gibt Dauer zurück
|
||||
- `getDelay(): float` - Gibt Verzögerung zurück
|
||||
- `isLooping(): bool` - Prüft ob Animation loopt
|
||||
- `onStart(callable $callback): self` - Setzt Start-Callback
|
||||
- `onComplete(callable $callback): self` - Setzt Complete-Callback
|
||||
- `onPause(callable $callback): self` - Setzt Pause-Callback
|
||||
- `onResume(callable $callback): self` - Setzt Resume-Callback
|
||||
|
||||
### AnimationManager
|
||||
|
||||
- `add(Animation $animation): void` - Fügt Animation hinzu
|
||||
- `remove(Animation $animation): void` - Entfernt Animation
|
||||
- `update(float $deltaTime): void` - Aktualisiert alle Animationen
|
||||
- `clear(): void` - Löscht alle Animationen
|
||||
- `getActiveAnimations(): array` - Gibt aktive Animationen zurück
|
||||
- `getActiveCount(): int` - Gibt Anzahl aktiver Animationen zurück
|
||||
- `getTotalCount(): int` - Gibt Gesamtanzahl zurück
|
||||
|
||||
### ConsoleOutput
|
||||
|
||||
- `animateFadeIn(string $text, float $duration = 1.0): void`
|
||||
- `animateFadeOut(string $text, float $duration = 1.0): void`
|
||||
- `animateTypewriter(string $text, float $charactersPerSecond = 10.0): void`
|
||||
- `animateMarquee(string $text, int $width = 80, float $speed = 1.0): void`
|
||||
- `animateText(string $text, Animation $animation): void`
|
||||
- `updateAnimations(float $deltaTime = 0.016): void`
|
||||
- `getAnimationManager(): AnimationManager`
|
||||
|
||||
### TuiAnimationRenderer
|
||||
|
||||
- `animateElement(string $elementId, Animation $animation): void`
|
||||
- `fadeInElement(string $elementId, float $duration = 0.5): void`
|
||||
- `fadeOutElement(string $elementId, float $duration = 0.5): void`
|
||||
- `pulseElement(string $elementId, float $duration = 1.0): void`
|
||||
- `slideInElement(string $elementId, SlideDirection $direction, int $distance, float $duration = 0.5): void`
|
||||
- `stopElement(string $elementId): void`
|
||||
- `getElementOpacity(string $elementId): float`
|
||||
- `getElementPosition(string $elementId): int`
|
||||
- `getElementScale(string $elementId): float`
|
||||
- `hasAnimation(string $elementId): bool`
|
||||
- `update(float $deltaTime): void`
|
||||
- `clear(): void`
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Animationen laufen nicht
|
||||
|
||||
1. **AnimationManager prüfen**: Stellen Sie sicher, dass ein `AnimationManager` vorhanden ist.
|
||||
2. **Update-Aufrufe**: Stellen Sie sicher, dass `update()` regelmäßig aufgerufen wird.
|
||||
3. **Animation starten**: Rufen Sie `start()` auf der Animation auf.
|
||||
|
||||
### Performance-Probleme
|
||||
|
||||
1. **Zu viele Animationen**: Reduzieren Sie die Anzahl gleichzeitiger Animationen.
|
||||
2. **Update-Frequenz**: Erhöhen Sie das Update-Intervall (z.B. 0.033 für 30 FPS).
|
||||
3. **Abgeschlossene Animationen**: Entfernen Sie abgeschlossene Animationen.
|
||||
|
||||
### Animationen werden nicht angezeigt
|
||||
|
||||
1. **TUI-Integration**: Für TUI-Animationen muss `TuiAnimationRenderer` verwendet werden.
|
||||
2. **Console-Output**: Für Text-Animationen muss `ConsoleOutput::animateText()` verwendet werden.
|
||||
3. **Update-Loop**: Stellen Sie sicher, dass der Update-Loop läuft.
|
||||
|
||||
## Weitere Ressourcen
|
||||
|
||||
- **Plan-Dokument**: `console-modul-refactoring.plan.md`
|
||||
- **Demo Command**: `src/Framework/Console/Examples/AnimationDemoCommand.php`
|
||||
- **Framework Guidelines**: `docs/claude/guidelines.md`
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,297 +0,0 @@
|
||||
# Configuration Best Practices
|
||||
|
||||
Leitlinien für effektives Configuration Management im Custom PHP Framework.
|
||||
|
||||
## Problem: .env Bloat
|
||||
|
||||
**Symptom**: Zunehmende Anzahl von Environment-Variablen in `.env.example`
|
||||
|
||||
**Negative Auswirkungen**:
|
||||
- Schwer zu überschauen, welche Variablen wirklich wichtig sind
|
||||
- Viele Parameter, die Entwickler nie ändern werden
|
||||
- Unklare Defaults für optionale Parameter
|
||||
- Erhöhter Wartungsaufwand
|
||||
|
||||
## Lösung: Sensible Defaults + Minimal Configuration
|
||||
|
||||
### Prinzip 1: "Convention over Configuration"
|
||||
|
||||
**Regel**: Nur Werte, die Entwickler WIRKLICH ändern müssen oder wollen, gehören in `.env`
|
||||
|
||||
**Gute .env Kandidaten**:
|
||||
- ✅ Credentials (DB_PASSWORD, API_KEYS)
|
||||
- ✅ Umgebungs-spezifische Werte (APP_ENV, APP_URL)
|
||||
- ✅ Infrastruktur-Konfiguration (DB_HOST, REDIS_HOST)
|
||||
- ✅ Feature Toggles für Production (APP_DEBUG)
|
||||
|
||||
**Schlechte .env Kandidaten**:
|
||||
- ❌ Performance Tuning Parameter (CACHE_TTL=60)
|
||||
- ❌ Interne Framework-Defaults (CACHE_SIZE=100)
|
||||
- ❌ Rarely Changed Feature Flags (FEATURE_ENABLED=true)
|
||||
- ❌ Debug-only Flags (VERBOSE_LOGGING=false)
|
||||
|
||||
### Prinzip 2: Sensible Defaults im Code
|
||||
|
||||
**Vor der Refaktorierung** (4 .env Variablen):
|
||||
```env
|
||||
FILESYSTEM_VALIDATOR_CACHE=true # Rarely disabled
|
||||
FILESYSTEM_VALIDATOR_CACHE_TTL=60 # Never changed
|
||||
FILESYSTEM_VALIDATOR_CACHE_SIZE=100 # Never changed
|
||||
FILESYSTEM_STORAGE_CACHE=true # Rarely disabled
|
||||
```
|
||||
|
||||
**Nach der Refaktorierung** (1 .env Variable):
|
||||
```env
|
||||
# Filesystem Performance (caching enabled by default)
|
||||
# Set to true only for debugging performance issues
|
||||
# FILESYSTEM_DISABLE_CACHE=false
|
||||
```
|
||||
|
||||
**Im Code** (FilesystemInitializer.php):
|
||||
```php
|
||||
// Sensible defaults hardcoded
|
||||
$disableCache = filter_var($_ENV['FILESYSTEM_DISABLE_CACHE'] ?? 'false', FILTER_VALIDATE_BOOLEAN);
|
||||
|
||||
if ($disableCache) {
|
||||
return $validator;
|
||||
}
|
||||
|
||||
return new CachedFileValidator(
|
||||
validator: $validator,
|
||||
cacheTtl: 60, // Sensible default: 1 minute
|
||||
maxCacheSize: 100 // Sensible default: 100 entries
|
||||
);
|
||||
```
|
||||
|
||||
**Vorteile**:
|
||||
- ✅ 75% weniger .env Variablen (4 → 1)
|
||||
- ✅ Klarere Intention: "disable only for debugging"
|
||||
- ✅ Production-optimized by default
|
||||
- ✅ Einfacher für neue Entwickler
|
||||
|
||||
### Prinzip 3: Opt-Out statt Opt-In für Performance
|
||||
|
||||
**Schlechtes Pattern** (Opt-In):
|
||||
```env
|
||||
FEATURE_CACHE=false # Muss explizit aktiviert werden
|
||||
```
|
||||
→ Entwickler vergessen es zu aktivieren → Performance-Probleme
|
||||
|
||||
**Gutes Pattern** (Opt-Out):
|
||||
```env
|
||||
# FEATURE_DISABLE_CACHE=false # Caching standardmäßig aktiv
|
||||
```
|
||||
→ Production-optimiert by default → Explizit deaktivieren nur für Debugging
|
||||
|
||||
### Prinzip 4: Gruppierung und Kommentare
|
||||
|
||||
**Schlechtes Beispiel**:
|
||||
```env
|
||||
CACHE_ENABLED=true
|
||||
CACHE_TTL=60
|
||||
CACHE_SIZE=100
|
||||
DB_CACHE=true
|
||||
FILE_CACHE=true
|
||||
API_CACHE=true
|
||||
```
|
||||
|
||||
**Gutes Beispiel**:
|
||||
```env
|
||||
# Cache System (optimized by default)
|
||||
# Disable only for debugging: DISABLE_ALL_CACHES=true
|
||||
# DISABLE_ALL_CACHES=false
|
||||
|
||||
# Database-specific cache tuning (advanced)
|
||||
# DB_CACHE_TTL=3600
|
||||
```
|
||||
|
||||
## Refactoring-Strategie
|
||||
|
||||
### Schritt 1: Identifiziere "Never Changed" Parameter
|
||||
|
||||
Prüfe in der Codebase:
|
||||
```bash
|
||||
# Suche nach $_ENV usage
|
||||
grep -r "ENV\['" src/
|
||||
|
||||
# Identifiziere Parameter, die nie geändert werden
|
||||
# Kandidaten für Hardcoding
|
||||
```
|
||||
|
||||
### Schritt 2: Extrahiere Sensible Defaults
|
||||
|
||||
**Vor**:
|
||||
```php
|
||||
$ttl = (int) ($_ENV['CACHE_TTL'] ?? 60);
|
||||
$size = (int) ($_ENV['CACHE_SIZE'] ?? 100);
|
||||
```
|
||||
|
||||
**Nach**:
|
||||
```php
|
||||
// Sensible defaults based on production experience
|
||||
const DEFAULT_CACHE_TTL = 60; // 1 minute
|
||||
const DEFAULT_CACHE_SIZE = 100; // 100 entries
|
||||
|
||||
$ttl = self::DEFAULT_CACHE_TTL;
|
||||
$size = self::DEFAULT_CACHE_SIZE;
|
||||
```
|
||||
|
||||
### Schritt 3: Konsolidiere Related Flags
|
||||
|
||||
**Vor** (3 separate flags):
|
||||
```env
|
||||
FILESYSTEM_VALIDATOR_CACHE=true
|
||||
FILESYSTEM_STORAGE_CACHE=true
|
||||
FILESYSTEM_CACHE_CLEARSTATCACHE=true
|
||||
```
|
||||
|
||||
**Nach** (1 master flag):
|
||||
```env
|
||||
# FILESYSTEM_DISABLE_CACHE=false
|
||||
```
|
||||
|
||||
**Im Code**:
|
||||
```php
|
||||
$disableCache = filter_var($_ENV['FILESYSTEM_DISABLE_CACHE'] ?? 'false', FILTER_VALIDATE_BOOLEAN);
|
||||
|
||||
// Affects all filesystem caching
|
||||
$useValidatorCache = !$disableCache;
|
||||
$useStorageCache = !$disableCache;
|
||||
$optimizeClearstatcache = !$disableCache;
|
||||
```
|
||||
|
||||
### Schritt 4: Dokumentiere Advanced Tuning
|
||||
|
||||
Für Experten, die wirklich tunen wollen:
|
||||
|
||||
```php
|
||||
// Advanced tuning (usually not needed)
|
||||
$ttl = (int) ($_ENV['FILESYSTEM_VALIDATOR_CACHE_TTL'] ?? 60);
|
||||
$size = (int) ($_ENV['FILESYSTEM_VALIDATOR_CACHE_SIZE'] ?? 100);
|
||||
```
|
||||
|
||||
**In Dokumentation** (nicht in .env.example):
|
||||
```markdown
|
||||
## Advanced Configuration
|
||||
|
||||
For expert performance tuning, these environment variables can be used:
|
||||
|
||||
- `FILESYSTEM_VALIDATOR_CACHE_TTL`: Cache TTL in seconds (default: 60)
|
||||
- `FILESYSTEM_VALIDATOR_CACHE_SIZE`: Max cache entries (default: 100)
|
||||
|
||||
⚠️ **Warning**: Only change these if you have measured performance issues.
|
||||
```
|
||||
|
||||
## Entscheidungsbaum
|
||||
|
||||
```
|
||||
Braucht diese Variable in .env?
|
||||
│
|
||||
├─ Ist es eine Credential? → ✅ JA
|
||||
├─ Ist es umgebungs-spezifisch (dev/prod)? → ✅ JA
|
||||
├─ Muss es für Production geändert werden? → ✅ JA
|
||||
├─ Wird es von Entwicklern oft geändert? → ✅ JA
|
||||
│
|
||||
└─ Ansonsten:
|
||||
├─ Ist es ein Performance Tuning Parameter? → ❌ NEIN (Sensible Default im Code)
|
||||
├─ Ist es ein internes Detail? → ❌ NEIN (Hardcode im Code)
|
||||
├─ Ist es ein Debug Flag? → ⚠️ MAYBE (Opt-Out Pattern erwägen)
|
||||
└─ Ist es ein Feature Toggle? → ⚠️ MAYBE (Convention over Configuration)
|
||||
```
|
||||
|
||||
## .env Organisation
|
||||
|
||||
### Empfohlene Struktur
|
||||
|
||||
```env
|
||||
# ============================================================
|
||||
# CORE APPLICATION
|
||||
# ============================================================
|
||||
APP_ENV=development
|
||||
APP_DEBUG=true
|
||||
APP_URL=https://localhost
|
||||
|
||||
# ============================================================
|
||||
# DATABASE
|
||||
# ============================================================
|
||||
DB_HOST=db
|
||||
DB_PORT=5432
|
||||
DB_DATABASE=michaelschiemer
|
||||
DB_USERNAME=postgres
|
||||
DB_PASSWORD=your_secure_password
|
||||
|
||||
# ============================================================
|
||||
# EXTERNAL SERVICES
|
||||
# ============================================================
|
||||
SPOTIFY_CLIENT_ID=your_client_id
|
||||
SPOTIFY_CLIENT_SECRET=your_client_secret
|
||||
|
||||
# ============================================================
|
||||
# PERFORMANCE & DEBUGGING (optimized by default)
|
||||
# ============================================================
|
||||
# Uncomment only for debugging:
|
||||
# FILESYSTEM_DISABLE_CACHE=false
|
||||
# DISABLE_QUERY_LOG=false
|
||||
# VERBOSE_ERROR_PAGES=false
|
||||
```
|
||||
|
||||
## Beispiele aus dem Framework
|
||||
|
||||
### ✅ Gutes Beispiel: Filesystem Performance
|
||||
|
||||
**Vorher** (4 Variablen):
|
||||
```env
|
||||
FILESYSTEM_VALIDATOR_CACHE=true
|
||||
FILESYSTEM_VALIDATOR_CACHE_TTL=60
|
||||
FILESYSTEM_VALIDATOR_CACHE_SIZE=100
|
||||
FILESYSTEM_STORAGE_CACHE=true
|
||||
```
|
||||
|
||||
**Nachher** (1 Variable):
|
||||
```env
|
||||
# FILESYSTEM_DISABLE_CACHE=false
|
||||
```
|
||||
|
||||
**Einsparung**: 75% weniger Variablen, gleiche Funktionalität
|
||||
|
||||
### ❌ Anti-Pattern: Zu granulare Konfiguration
|
||||
|
||||
```env
|
||||
# DON'T DO THIS
|
||||
ENABLE_USER_CACHE=true
|
||||
USER_CACHE_TTL=60
|
||||
USER_CACHE_SIZE=100
|
||||
ENABLE_POST_CACHE=true
|
||||
POST_CACHE_TTL=120
|
||||
POST_CACHE_SIZE=200
|
||||
ENABLE_COMMENT_CACHE=true
|
||||
COMMENT_CACHE_TTL=30
|
||||
COMMENT_CACHE_SIZE=50
|
||||
```
|
||||
|
||||
**Besser**:
|
||||
```env
|
||||
# Cache System (enabled by default with sensible defaults)
|
||||
# DISABLE_ALL_CACHES=false
|
||||
```
|
||||
|
||||
**Im Code** (wenn wirklich Tuning nötig):
|
||||
```php
|
||||
// Expert tuning available via environment, but not advertised
|
||||
$userCacheTtl = (int) ($_ENV['USER_CACHE_TTL'] ?? 60);
|
||||
$postCacheTtl = (int) ($_ENV['POST_CACHE_TTL'] ?? 120);
|
||||
```
|
||||
|
||||
## Zusammenfassung
|
||||
|
||||
**Goldene Regel**: Jede .env Variable muss rechtfertigen, warum sie NICHT ein Sensible Default im Code sein kann.
|
||||
|
||||
**Checklist für neue .env Variablen**:
|
||||
- [ ] Ist es eine Credential oder Secret? → .env
|
||||
- [ ] Ist es umgebungs-spezifisch? → .env
|
||||
- [ ] Ändern Entwickler es regelmäßig? → .env
|
||||
- [ ] Ist es ein Performance Parameter? → Sensible Default im Code
|
||||
- [ ] Ist es ein Debug Flag? → Opt-Out Pattern (commented out in .env.example)
|
||||
- [ ] Ist es ein internes Detail? → Hardcode im Code, nicht konfigurierbar
|
||||
|
||||
**Ergebnis**: Übersichtliche .env.example, production-optimized by default, trotzdem flexibel für Experten.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,249 +0,0 @@
|
||||
# Error Handling & Debugging
|
||||
|
||||
This guide covers error handling patterns and debugging strategies in the framework.
|
||||
|
||||
## Exception Handling
|
||||
|
||||
All custom exceptions in the framework must extend `FrameworkException` to ensure consistent error handling, logging, and recovery mechanisms.
|
||||
|
||||
### The FrameworkException System
|
||||
|
||||
The framework provides a sophisticated exception system with:
|
||||
- **ExceptionContext**: Rich context information for debugging
|
||||
- **ErrorCode**: Categorized error codes with recovery hints
|
||||
- **RetryAfter**: Support for recoverable operations
|
||||
- **Fluent Interface**: Easy context building
|
||||
|
||||
### Creating Custom Exceptions
|
||||
|
||||
```php
|
||||
namespace App\Domain\User\Exceptions;
|
||||
|
||||
use App\Framework\Exception\FrameworkException;
|
||||
use App\Framework\Exception\Core\DatabaseErrorCode;
|
||||
use App\Framework\Exception\ExceptionContext;
|
||||
|
||||
final class UserNotFoundException extends FrameworkException
|
||||
{
|
||||
public static function byId(UserId $id): self
|
||||
{
|
||||
return self::create(
|
||||
DatabaseErrorCode::ENTITY_NOT_FOUND,
|
||||
"User with ID '{$id->toString()}' not found"
|
||||
)->withData([
|
||||
'user_id' => $id->toString(),
|
||||
'search_type' => 'by_id'
|
||||
]);
|
||||
}
|
||||
|
||||
public static function byEmail(Email $email): self
|
||||
{
|
||||
$context = ExceptionContext::forOperation('user.lookup', 'UserRepository')
|
||||
->withData(['email' => $email->getMasked()]);
|
||||
|
||||
return self::fromContext(
|
||||
"User with email not found",
|
||||
$context,
|
||||
DatabaseErrorCode::ENTITY_NOT_FOUND
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Using ErrorCode Enums
|
||||
|
||||
The framework provides category-specific error code enums for better organization and type safety:
|
||||
|
||||
```php
|
||||
use App\Framework\Exception\Core\DatabaseErrorCode;
|
||||
use App\Framework\Exception\Core\AuthErrorCode;
|
||||
use App\Framework\Exception\Core\HttpErrorCode;
|
||||
use App\Framework\Exception\Core\SecurityErrorCode;
|
||||
use App\Framework\Exception\Core\ValidationErrorCode;
|
||||
|
||||
// Database errors
|
||||
DatabaseErrorCode::CONNECTION_FAILED
|
||||
DatabaseErrorCode::QUERY_FAILED
|
||||
DatabaseErrorCode::TRANSACTION_FAILED
|
||||
DatabaseErrorCode::CONSTRAINT_VIOLATION
|
||||
|
||||
// Authentication errors
|
||||
AuthErrorCode::CREDENTIALS_INVALID
|
||||
AuthErrorCode::TOKEN_EXPIRED
|
||||
AuthErrorCode::SESSION_EXPIRED
|
||||
AuthErrorCode::ACCOUNT_LOCKED
|
||||
|
||||
// HTTP errors
|
||||
HttpErrorCode::BAD_REQUEST
|
||||
HttpErrorCode::NOT_FOUND
|
||||
HttpErrorCode::METHOD_NOT_ALLOWED
|
||||
HttpErrorCode::RATE_LIMIT_EXCEEDED
|
||||
|
||||
// Security errors
|
||||
SecurityErrorCode::CSRF_TOKEN_INVALID
|
||||
SecurityErrorCode::SQL_INJECTION_DETECTED
|
||||
SecurityErrorCode::XSS_DETECTED
|
||||
SecurityErrorCode::PATH_TRAVERSAL_DETECTED
|
||||
|
||||
// Validation errors
|
||||
ValidationErrorCode::INVALID_INPUT
|
||||
ValidationErrorCode::REQUIRED_FIELD_MISSING
|
||||
ValidationErrorCode::BUSINESS_RULE_VIOLATION
|
||||
ValidationErrorCode::INVALID_FORMAT
|
||||
|
||||
// Using error codes in exceptions:
|
||||
throw FrameworkException::create(
|
||||
DatabaseErrorCode::QUERY_FAILED,
|
||||
"Failed to execute user query"
|
||||
)->withContext(
|
||||
ExceptionContext::forOperation('user.find', 'UserRepository')
|
||||
->withData(['query' => 'SELECT * FROM users WHERE id = ?'])
|
||||
->withDebug(['bind_params' => [$userId]])
|
||||
);
|
||||
```
|
||||
|
||||
### Exception Context Building
|
||||
|
||||
```php
|
||||
// Method 1: Using factory methods
|
||||
$exception = FrameworkException::forOperation(
|
||||
'payment.process',
|
||||
'PaymentService',
|
||||
'Payment processing failed',
|
||||
HttpErrorCode::BAD_GATEWAY
|
||||
)->withData([
|
||||
'amount' => $amount->toArray(),
|
||||
'gateway' => 'stripe',
|
||||
'customer_id' => $customerId
|
||||
])->withMetadata([
|
||||
'attempt' => 1,
|
||||
'idempotency_key' => $idempotencyKey
|
||||
]);
|
||||
|
||||
// Method 2: Building context separately
|
||||
$context = ExceptionContext::empty()
|
||||
->withOperation('order.validate', 'OrderService')
|
||||
->withData([
|
||||
'order_id' => $orderId,
|
||||
'total' => $total->toDecimal()
|
||||
])
|
||||
->withDebug([
|
||||
'validation_rules' => ['min_amount', 'max_items'],
|
||||
'failed_rule' => 'min_amount'
|
||||
]);
|
||||
|
||||
throw FrameworkException::fromContext(
|
||||
'Order validation failed',
|
||||
$context,
|
||||
ValidationErrorCode::BUSINESS_RULE_VIOLATION
|
||||
);
|
||||
```
|
||||
|
||||
### Recoverable Exceptions
|
||||
|
||||
```php
|
||||
// Creating recoverable exceptions with retry hints
|
||||
final class RateLimitException extends FrameworkException
|
||||
{
|
||||
public static function exceeded(int $retryAfter): self
|
||||
{
|
||||
return self::create(
|
||||
HttpErrorCode::RATE_LIMIT_EXCEEDED,
|
||||
'API rate limit exceeded'
|
||||
)->withRetryAfter($retryAfter)
|
||||
->withData(['retry_after_seconds' => $retryAfter]);
|
||||
}
|
||||
}
|
||||
|
||||
// Using in code
|
||||
try {
|
||||
$response = $apiClient->request($endpoint);
|
||||
} catch (RateLimitException $e) {
|
||||
if ($e->isRecoverable()) {
|
||||
$waitTime = $e->getRetryAfter();
|
||||
// Schedule retry after $waitTime seconds
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
```
|
||||
|
||||
### Exception Categories
|
||||
|
||||
```php
|
||||
// Check exception category for handling strategies
|
||||
try {
|
||||
$result = $operation->execute();
|
||||
} catch (FrameworkException $e) {
|
||||
if ($e->isCategory('AUTH')) {
|
||||
// Handle authentication errors
|
||||
return $this->redirectToLogin();
|
||||
}
|
||||
|
||||
if ($e->isCategory('VAL')) {
|
||||
// Handle validation errors
|
||||
return $this->validationErrorResponse($e);
|
||||
}
|
||||
|
||||
if ($e->isErrorCode(DatabaseErrorCode::CONNECTION_FAILED)) {
|
||||
// Handle specific database connection errors
|
||||
$this->notifyOps($e);
|
||||
}
|
||||
|
||||
throw $e;
|
||||
}
|
||||
```
|
||||
|
||||
### Simple Exceptions for Quick Use
|
||||
|
||||
```php
|
||||
// When you don't need the full context system
|
||||
throw FrameworkException::simple('Quick error message');
|
||||
|
||||
// With previous exception
|
||||
} catch (\PDOException $e) {
|
||||
throw FrameworkException::simple(
|
||||
'Database operation failed',
|
||||
$e,
|
||||
500
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Exception Data Sanitization
|
||||
|
||||
The framework automatically sanitizes sensitive data in exceptions:
|
||||
|
||||
```php
|
||||
// Sensitive keys are automatically redacted
|
||||
$exception->withData([
|
||||
'username' => 'john@example.com',
|
||||
'password' => 'secret123', // Will be logged as '[REDACTED]'
|
||||
'api_key' => 'sk_live_...' // Will be logged as '[REDACTED]'
|
||||
]);
|
||||
```
|
||||
|
||||
### Best Practices
|
||||
|
||||
1. **Always extend FrameworkException** for custom exceptions
|
||||
2. **Use ErrorCode enum** for categorizable errors
|
||||
3. **Provide rich context** with operation, component, and data
|
||||
4. **Use factory methods** for consistent exception creation
|
||||
5. **Sanitize sensitive data** (automatic for common keys)
|
||||
6. **Make exceptions domain-specific** (UserNotFoundException vs generic NotFoundException)
|
||||
7. **Include recovery hints** for recoverable errors
|
||||
|
||||
## Logging Best Practices
|
||||
|
||||
TODO: Document logging patterns and levels
|
||||
|
||||
## Debug Strategies
|
||||
|
||||
TODO: Document debugging approaches and tools
|
||||
|
||||
## Error Recovery Patterns
|
||||
|
||||
TODO: Document error recovery and graceful degradation
|
||||
|
||||
## Common Error Scenarios
|
||||
|
||||
TODO: List common errors and solutions
|
||||
@@ -1,963 +0,0 @@
|
||||
# Event System
|
||||
|
||||
This guide covers the event-driven architecture of the framework.
|
||||
|
||||
## Overview
|
||||
|
||||
The framework provides a sophisticated event system built on two core components:
|
||||
|
||||
- **EventBus**: Simple, lightweight interface for basic event dispatching
|
||||
- **EventDispatcher**: Full-featured implementation with handler management, priorities, and propagation control
|
||||
|
||||
**Key Features**:
|
||||
- Attribute-based event handler registration via `#[OnEvent]`
|
||||
- Priority-based handler execution
|
||||
- Event propagation control (stop propagation)
|
||||
- Support for inheritance and interface-based event matching
|
||||
- Automatic handler discovery via framework's attribute system
|
||||
- Domain events for business logic decoupling
|
||||
- Application lifecycle events for framework integration
|
||||
|
||||
## EventBus vs EventDispatcher
|
||||
|
||||
### EventBus Interface
|
||||
|
||||
**Purpose**: Simple contract for event dispatching
|
||||
|
||||
```php
|
||||
interface EventBus
|
||||
{
|
||||
public function dispatch(object $event): void;
|
||||
}
|
||||
```
|
||||
|
||||
**When to use**:
|
||||
- Simple event dispatching without return values
|
||||
- Fire-and-forget events
|
||||
- When you don't need handler results
|
||||
- Dependency injection when you want interface-based type hints
|
||||
|
||||
**Example Usage**:
|
||||
```php
|
||||
final readonly class OrderService
|
||||
{
|
||||
public function __construct(
|
||||
private EventBus $eventBus
|
||||
) {}
|
||||
|
||||
public function createOrder(CreateOrderCommand $command): Order
|
||||
{
|
||||
$order = Order::create($command);
|
||||
|
||||
// Fire event without caring about results
|
||||
$this->eventBus->dispatch(new OrderCreatedEvent($order));
|
||||
|
||||
return $order;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### EventDispatcher Implementation
|
||||
|
||||
**Purpose**: Full-featured event system with handler management and results
|
||||
|
||||
**Key Features**:
|
||||
- Returns array of handler results
|
||||
- Priority-based handler execution (highest priority first)
|
||||
- Stop propagation support
|
||||
- Manual handler registration via `addHandler()` or `listen()`
|
||||
- Automatic handler discovery via `#[OnEvent]` attribute
|
||||
- Inheritance and interface matching
|
||||
|
||||
**When to use**:
|
||||
- Need handler return values for processing
|
||||
- Want priority control over execution order
|
||||
- Need to stop event propagation conditionally
|
||||
- Require manual handler registration
|
||||
- Building complex event workflows
|
||||
|
||||
**Example Usage**:
|
||||
```php
|
||||
final readonly class PaymentProcessor
|
||||
{
|
||||
public function __construct(
|
||||
private EventDispatcher $dispatcher
|
||||
) {}
|
||||
|
||||
public function processPayment(Payment $payment): PaymentResult
|
||||
{
|
||||
// Dispatch and collect results from all handlers
|
||||
$results = $this->dispatcher->dispatch(
|
||||
new PaymentProcessingEvent($payment)
|
||||
);
|
||||
|
||||
// Process handler results
|
||||
foreach ($results as $result) {
|
||||
if ($result instanceof PaymentValidationFailure) {
|
||||
throw new PaymentException($result->reason);
|
||||
}
|
||||
}
|
||||
|
||||
return new PaymentResult($payment, $results);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Implementation Relationship
|
||||
|
||||
```php
|
||||
// EventDispatcher implements EventBus interface
|
||||
final class EventDispatcher implements EventBus
|
||||
{
|
||||
public function dispatch(object $event): array
|
||||
{
|
||||
// Full implementation with handler management
|
||||
}
|
||||
}
|
||||
|
||||
// In DI Container
|
||||
$container->singleton(EventBus::class, EventDispatcher::class);
|
||||
$container->singleton(EventDispatcher::class, EventDispatcher::class);
|
||||
```
|
||||
|
||||
**Recommendation**: Use `EventDispatcher` for type hints when you need full features, use `EventBus` interface when you want loose coupling and don't need results.
|
||||
|
||||
## Domain Events
|
||||
|
||||
### What are Domain Events?
|
||||
|
||||
Domain Events represent significant state changes or occurrences in your business domain. They enable loose coupling between domain components.
|
||||
|
||||
**Characteristics**:
|
||||
- Immutable (`readonly` classes)
|
||||
- Named in past tense (OrderCreated, UserRegistered, PaymentCompleted)
|
||||
- Contain only relevant domain data
|
||||
- No business logic (pure data objects)
|
||||
- Include timestamp for audit trail
|
||||
|
||||
### Creating Domain Events
|
||||
|
||||
```php
|
||||
namespace App\Domain\Order\Events;
|
||||
|
||||
use App\Domain\Order\ValueObjects\OrderId;
|
||||
use App\Framework\Core\ValueObjects\Timestamp;
|
||||
|
||||
final readonly class OrderCreatedEvent
|
||||
{
|
||||
public readonly Timestamp $occurredAt;
|
||||
|
||||
public function __construct(
|
||||
public OrderId $orderId,
|
||||
public UserId $userId,
|
||||
public Money $total,
|
||||
?Timestamp $occurredAt = null
|
||||
) {
|
||||
$this->occurredAt = $occurredAt ?? Timestamp::now();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Best Practices**:
|
||||
- Use Value Objects for event properties
|
||||
- Include timestamp for audit trail
|
||||
- Keep events focused (single responsibility)
|
||||
- Version events for backward compatibility
|
||||
- Document event payload in PHPDoc
|
||||
|
||||
### Framework Lifecycle Events
|
||||
|
||||
The framework provides built-in events for application lifecycle:
|
||||
|
||||
#### ApplicationBooted
|
||||
|
||||
**Triggered**: After application bootstrap completes
|
||||
|
||||
```php
|
||||
final readonly class ApplicationBooted
|
||||
{
|
||||
public function __construct(
|
||||
public \DateTimeImmutable $bootTime,
|
||||
public string $environment,
|
||||
public Version $version,
|
||||
public \DateTimeImmutable $occurredAt = new \DateTimeImmutable()
|
||||
) {}
|
||||
}
|
||||
```
|
||||
|
||||
**Use Cases**:
|
||||
- Initialize services after boot
|
||||
- Start background workers
|
||||
- Setup scheduled tasks
|
||||
- Warm caches
|
||||
|
||||
#### BeforeHandleRequest
|
||||
|
||||
**Triggered**: Before HTTP request processing
|
||||
|
||||
```php
|
||||
final readonly class BeforeHandleRequest
|
||||
{
|
||||
public function __construct(
|
||||
public Request $request,
|
||||
public Timestamp $timestamp,
|
||||
public array $context = []
|
||||
) {}
|
||||
}
|
||||
```
|
||||
|
||||
**Use Cases**:
|
||||
- Request logging
|
||||
- Performance monitoring
|
||||
- Security checks
|
||||
- Request transformation
|
||||
|
||||
#### AfterHandleRequest
|
||||
|
||||
**Triggered**: After HTTP request processing
|
||||
|
||||
```php
|
||||
final readonly class AfterHandleRequest
|
||||
{
|
||||
public function __construct(
|
||||
public Request $request,
|
||||
public Response $response,
|
||||
public Duration $processingTime,
|
||||
public Timestamp $timestamp,
|
||||
public array $context = []
|
||||
) {}
|
||||
}
|
||||
```
|
||||
|
||||
**Use Cases**:
|
||||
- Response logging
|
||||
- Performance metrics
|
||||
- Analytics collection
|
||||
- Cleanup operations
|
||||
|
||||
### Example: User Registration Event
|
||||
|
||||
```php
|
||||
namespace App\Domain\User\Events;
|
||||
|
||||
final readonly class UserRegisteredEvent
|
||||
{
|
||||
public readonly Timestamp $occurredAt;
|
||||
|
||||
public function __construct(
|
||||
public UserId $userId,
|
||||
public Email $email,
|
||||
public UserName $userName,
|
||||
?Timestamp $occurredAt = null
|
||||
) {
|
||||
$this->occurredAt = $occurredAt ?? Timestamp::now();
|
||||
}
|
||||
}
|
||||
|
||||
// Dispatching the event
|
||||
final readonly class UserService
|
||||
{
|
||||
public function __construct(
|
||||
private UserRepository $repository,
|
||||
private EventBus $eventBus
|
||||
) {}
|
||||
|
||||
public function register(RegisterUserCommand $command): User
|
||||
{
|
||||
$user = User::create(
|
||||
$command->email,
|
||||
$command->userName,
|
||||
$command->password
|
||||
);
|
||||
|
||||
$this->repository->save($user);
|
||||
|
||||
// Dispatch domain event
|
||||
$this->eventBus->dispatch(
|
||||
new UserRegisteredEvent(
|
||||
$user->id,
|
||||
$user->email,
|
||||
$user->userName
|
||||
)
|
||||
);
|
||||
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Event Handler Registration
|
||||
|
||||
### Attribute-Based Registration
|
||||
|
||||
**Primary Method**: Use `#[OnEvent]` attribute for automatic discovery
|
||||
|
||||
```php
|
||||
use App\Framework\Core\Events\OnEvent;
|
||||
|
||||
final readonly class UserEventHandlers
|
||||
{
|
||||
public function __construct(
|
||||
private EmailService $emailService,
|
||||
private Logger $logger
|
||||
) {}
|
||||
|
||||
#[OnEvent(priority: 100)]
|
||||
public function sendWelcomeEmail(UserRegisteredEvent $event): void
|
||||
{
|
||||
$this->emailService->send(
|
||||
to: $event->email,
|
||||
template: 'welcome',
|
||||
data: ['userName' => $event->userName->value]
|
||||
);
|
||||
}
|
||||
|
||||
#[OnEvent(priority: 50)]
|
||||
public function logUserRegistration(UserRegisteredEvent $event): void
|
||||
{
|
||||
$this->logger->info('User registered', [
|
||||
'user_id' => $event->userId->toString(),
|
||||
'email' => $event->email->value,
|
||||
'occurred_at' => $event->occurredAt->format('Y-m-d H:i:s')
|
||||
]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Attribute Parameters**:
|
||||
- `priority` (optional): Handler execution order (higher = earlier, default: 0)
|
||||
- `stopPropagation` (optional): Stop execution after this handler (default: false)
|
||||
|
||||
### Priority-Based Execution
|
||||
|
||||
Handlers execute in **priority order** (highest to lowest):
|
||||
|
||||
```php
|
||||
#[OnEvent(priority: 200)] // Executes first
|
||||
public function criticalHandler(OrderCreatedEvent $event): void { }
|
||||
|
||||
#[OnEvent(priority: 100)] // Executes second
|
||||
public function highPriorityHandler(OrderCreatedEvent $event): void { }
|
||||
|
||||
#[OnEvent(priority: 50)] // Executes third
|
||||
public function normalHandler(OrderCreatedEvent $event): void { }
|
||||
|
||||
#[OnEvent] // Executes last (default priority: 0)
|
||||
public function defaultHandler(OrderCreatedEvent $event): void { }
|
||||
```
|
||||
|
||||
**Use Cases for Priorities**:
|
||||
- **Critical (200+)**: Security checks, validation, fraud detection
|
||||
- **High (100-199)**: Transaction logging, audit trail
|
||||
- **Normal (50-99)**: Business logic, notifications
|
||||
- **Low (0-49)**: Analytics, metrics, cleanup
|
||||
|
||||
### Stop Propagation
|
||||
|
||||
**Purpose**: Prevent subsequent handlers from executing
|
||||
|
||||
```php
|
||||
#[OnEvent(priority: 100, stopPropagation: true)]
|
||||
public function validatePayment(PaymentProcessingEvent $event): PaymentValidationResult
|
||||
{
|
||||
$result = $this->validator->validate($event->payment);
|
||||
|
||||
if (!$result->isValid()) {
|
||||
// Stop propagation - no further handlers execute
|
||||
return new PaymentValidationFailure($result->errors);
|
||||
}
|
||||
|
||||
return new PaymentValidationSuccess();
|
||||
}
|
||||
|
||||
#[OnEvent(priority: 50)]
|
||||
public function processPayment(PaymentProcessingEvent $event): void
|
||||
{
|
||||
// This handler only executes if validation passes
|
||||
// (previous handler didn't set stopPropagation result)
|
||||
$this->gateway->charge($event->payment);
|
||||
}
|
||||
```
|
||||
|
||||
**Important**: `stopPropagation` stops execution **after** the current handler completes.
|
||||
|
||||
### Manual Handler Registration
|
||||
|
||||
**Alternative**: Register handlers programmatically
|
||||
|
||||
```php
|
||||
final readonly class EventInitializer
|
||||
{
|
||||
public function __construct(
|
||||
private EventDispatcher $dispatcher
|
||||
) {}
|
||||
|
||||
#[Initializer]
|
||||
public function registerHandlers(): void
|
||||
{
|
||||
// Using listen() method
|
||||
$this->dispatcher->listen(
|
||||
OrderCreatedEvent::class,
|
||||
function (OrderCreatedEvent $event) {
|
||||
// Handle event
|
||||
},
|
||||
priority: 100
|
||||
);
|
||||
|
||||
// Using addHandler() method
|
||||
$this->dispatcher->addHandler(
|
||||
eventClass: UserRegisteredEvent::class,
|
||||
handler: [$this->userService, 'onUserRegistered'],
|
||||
priority: 50
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**When to use manual registration**:
|
||||
- Dynamic handler registration based on configuration
|
||||
- Third-party library integration
|
||||
- Closure-based handlers for simple cases
|
||||
- Runtime handler modification
|
||||
|
||||
### Handler Discovery
|
||||
|
||||
The framework automatically discovers handlers marked with `#[OnEvent]`:
|
||||
|
||||
```php
|
||||
// In Application Bootstrap
|
||||
final readonly class EventSystemInitializer
|
||||
{
|
||||
#[Initializer]
|
||||
public function initialize(EventDispatcher $dispatcher): void
|
||||
{
|
||||
// Automatic discovery finds all #[OnEvent] methods
|
||||
$discoveredHandlers = $this->attributeScanner->findMethodsWithAttribute(
|
||||
OnEvent::class
|
||||
);
|
||||
|
||||
foreach ($discoveredHandlers as $handler) {
|
||||
$dispatcher->addHandler(
|
||||
eventClass: $handler->getEventClass(),
|
||||
handler: [$handler->instance, $handler->method],
|
||||
priority: $handler->attribute->priority ?? 0
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**No Manual Setup Required**: Framework handles discovery automatically during initialization.
|
||||
|
||||
## Event Middleware
|
||||
|
||||
**Concept**: Middleware pattern for event processing (transform, filter, log, etc.)
|
||||
|
||||
### Custom Event Middleware
|
||||
|
||||
```php
|
||||
interface EventMiddleware
|
||||
{
|
||||
public function process(object $event, callable $next): mixed;
|
||||
}
|
||||
|
||||
final readonly class LoggingEventMiddleware implements EventMiddleware
|
||||
{
|
||||
public function __construct(
|
||||
private Logger $logger
|
||||
) {}
|
||||
|
||||
public function process(object $event, callable $next): mixed
|
||||
{
|
||||
$eventClass = get_class($event);
|
||||
$startTime = microtime(true);
|
||||
|
||||
$this->logger->debug("Event dispatched: {$eventClass}");
|
||||
|
||||
try {
|
||||
$result = $next($event);
|
||||
|
||||
$duration = (microtime(true) - $startTime) * 1000;
|
||||
$this->logger->debug("Event processed: {$eventClass}", [
|
||||
'duration_ms' => $duration
|
||||
]);
|
||||
|
||||
return $result;
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->error("Event failed: {$eventClass}", [
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Middleware Pipeline
|
||||
|
||||
```php
|
||||
final class EventDispatcherWithMiddleware
|
||||
{
|
||||
/** @var EventMiddleware[] */
|
||||
private array $middleware = [];
|
||||
|
||||
public function addMiddleware(EventMiddleware $middleware): void
|
||||
{
|
||||
$this->middleware[] = $middleware;
|
||||
}
|
||||
|
||||
public function dispatch(object $event): array
|
||||
{
|
||||
$pipeline = array_reduce(
|
||||
array_reverse($this->middleware),
|
||||
fn($next, $middleware) => fn($event) => $middleware->process($event, $next),
|
||||
fn($event) => $this->eventDispatcher->dispatch($event)
|
||||
);
|
||||
|
||||
return $pipeline($event);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Validation Middleware
|
||||
|
||||
```php
|
||||
final readonly class ValidationEventMiddleware implements EventMiddleware
|
||||
{
|
||||
public function __construct(
|
||||
private ValidatorInterface $validator
|
||||
) {}
|
||||
|
||||
public function process(object $event, callable $next): mixed
|
||||
{
|
||||
// Validate event before dispatching
|
||||
$errors = $this->validator->validate($event);
|
||||
|
||||
if (!empty($errors)) {
|
||||
throw new InvalidEventException(
|
||||
"Event validation failed: " . implode(', ', $errors)
|
||||
);
|
||||
}
|
||||
|
||||
return $next($event);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Async Event Processing
|
||||
|
||||
### Queue-Based Async Handling
|
||||
|
||||
```php
|
||||
use App\Framework\Queue\Queue;
|
||||
use App\Framework\Queue\ValueObjects\JobPayload;
|
||||
|
||||
final readonly class AsyncEventHandler
|
||||
{
|
||||
public function __construct(
|
||||
private Queue $queue
|
||||
) {}
|
||||
|
||||
#[OnEvent(priority: 50)]
|
||||
public function processAsync(OrderCreatedEvent $event): void
|
||||
{
|
||||
// Dispatch to queue for async processing
|
||||
$job = new ProcessOrderEmailJob(
|
||||
orderId: $event->orderId,
|
||||
userEmail: $event->userEmail
|
||||
);
|
||||
|
||||
$this->queue->push(
|
||||
JobPayload::immediate($job)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Background Job
|
||||
final readonly class ProcessOrderEmailJob
|
||||
{
|
||||
public function __construct(
|
||||
public OrderId $orderId,
|
||||
public Email $userEmail
|
||||
) {}
|
||||
|
||||
public function handle(EmailService $emailService): void
|
||||
{
|
||||
// Process email asynchronously
|
||||
$emailService->sendOrderConfirmation(
|
||||
$this->orderId,
|
||||
$this->userEmail
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Event Buffering Pattern
|
||||
|
||||
```php
|
||||
final class EventBuffer
|
||||
{
|
||||
private array $bufferedEvents = [];
|
||||
|
||||
#[OnEvent]
|
||||
public function bufferEvent(DomainEvent $event): void
|
||||
{
|
||||
$this->bufferedEvents[] = $event;
|
||||
}
|
||||
|
||||
public function flush(EventDispatcher $dispatcher): void
|
||||
{
|
||||
foreach ($this->bufferedEvents as $event) {
|
||||
$dispatcher->dispatch($event);
|
||||
}
|
||||
|
||||
$this->bufferedEvents = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Usage in transaction
|
||||
try {
|
||||
$this->entityManager->beginTransaction();
|
||||
|
||||
// Buffer events during transaction
|
||||
$this->eventBuffer->bufferEvent(new OrderCreatedEvent(/* ... */));
|
||||
$this->eventBuffer->bufferEvent(new InventoryReservedEvent(/* ... */));
|
||||
|
||||
$this->entityManager->commit();
|
||||
|
||||
// Flush events after successful commit
|
||||
$this->eventBuffer->flush($this->dispatcher);
|
||||
} catch (\Exception $e) {
|
||||
$this->entityManager->rollback();
|
||||
// Events are not flushed on rollback
|
||||
throw $e;
|
||||
}
|
||||
```
|
||||
|
||||
## Event Best Practices
|
||||
|
||||
### 1. Event Naming
|
||||
|
||||
**✅ Good**: Past tense, descriptive
|
||||
```php
|
||||
OrderCreatedEvent
|
||||
UserRegisteredEvent
|
||||
PaymentCompletedEvent
|
||||
InventoryReservedEvent
|
||||
```
|
||||
|
||||
**❌ Bad**: Present/future tense, vague
|
||||
```php
|
||||
CreateOrderEvent
|
||||
RegisterUserEvent
|
||||
CompletePayment
|
||||
ReserveInventory
|
||||
```
|
||||
|
||||
### 2. Event Granularity
|
||||
|
||||
**✅ Focused Events**:
|
||||
```php
|
||||
final readonly class OrderCreatedEvent { /* ... */ }
|
||||
final readonly class OrderShippedEvent { /* ... */ }
|
||||
final readonly class OrderCancelledEvent { /* ... */ }
|
||||
```
|
||||
|
||||
**❌ God Events**:
|
||||
```php
|
||||
final readonly class OrderEvent
|
||||
{
|
||||
public function __construct(
|
||||
public string $action, // 'created', 'shipped', 'cancelled'
|
||||
public Order $order
|
||||
) {}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Immutability
|
||||
|
||||
**✅ Readonly Events**:
|
||||
```php
|
||||
final readonly class UserUpdatedEvent
|
||||
{
|
||||
public function __construct(
|
||||
public UserId $userId,
|
||||
public Email $newEmail
|
||||
) {}
|
||||
}
|
||||
```
|
||||
|
||||
**❌ Mutable Events**:
|
||||
```php
|
||||
final class UserUpdatedEvent
|
||||
{
|
||||
public UserId $userId;
|
||||
public Email $newEmail;
|
||||
|
||||
public function setEmail(Email $email): void
|
||||
{
|
||||
$this->newEmail = $email; // Bad: Events should be immutable
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Handler Independence
|
||||
|
||||
**✅ Independent Handlers**:
|
||||
```php
|
||||
#[OnEvent]
|
||||
public function sendEmail(OrderCreatedEvent $event): void
|
||||
{
|
||||
// Self-contained - doesn't depend on other handlers
|
||||
$this->emailService->send(/* ... */);
|
||||
}
|
||||
|
||||
#[OnEvent]
|
||||
public function updateInventory(OrderCreatedEvent $event): void
|
||||
{
|
||||
// Independent - no shared state with email handler
|
||||
$this->inventoryService->reserve(/* ... */);
|
||||
}
|
||||
```
|
||||
|
||||
**❌ Coupled Handlers**:
|
||||
```php
|
||||
private bool $emailSent = false;
|
||||
|
||||
#[OnEvent(priority: 100)]
|
||||
public function sendEmail(OrderCreatedEvent $event): void
|
||||
{
|
||||
$this->emailService->send(/* ... */);
|
||||
$this->emailSent = true; // Bad: Shared state
|
||||
}
|
||||
|
||||
#[OnEvent(priority: 50)]
|
||||
public function logEmailSent(OrderCreatedEvent $event): void
|
||||
{
|
||||
if ($this->emailSent) { // Bad: Depends on other handler
|
||||
$this->logger->info('Email sent');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Error Handling
|
||||
|
||||
**✅ Graceful Degradation**:
|
||||
```php
|
||||
#[OnEvent]
|
||||
public function sendNotification(OrderCreatedEvent $event): void
|
||||
{
|
||||
try {
|
||||
$this->notificationService->send(/* ... */);
|
||||
} catch (NotificationException $e) {
|
||||
// Log error but don't fail the entire event dispatch
|
||||
$this->logger->error('Notification failed', [
|
||||
'order_id' => $event->orderId->toString(),
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Avoid Circular Events
|
||||
|
||||
**❌ Circular Dependency**:
|
||||
```php
|
||||
#[OnEvent]
|
||||
public function onOrderCreated(OrderCreatedEvent $event): void
|
||||
{
|
||||
// Bad: Dispatching event from event handler can cause loops
|
||||
$this->eventBus->dispatch(new OrderProcessedEvent($event->orderId));
|
||||
}
|
||||
|
||||
#[OnEvent]
|
||||
public function onOrderProcessed(OrderProcessedEvent $event): void
|
||||
{
|
||||
// Bad: Creates circular dependency
|
||||
$this->eventBus->dispatch(new OrderCreatedEvent(/* ... */));
|
||||
}
|
||||
```
|
||||
|
||||
**✅ Use Command/Service Layer**:
|
||||
```php
|
||||
#[OnEvent]
|
||||
public function onOrderCreated(OrderCreatedEvent $event): void
|
||||
{
|
||||
// Good: Use service to perform additional work
|
||||
$this->orderProcessingService->processOrder($event->orderId);
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Performance Considerations
|
||||
|
||||
**Heavy Operations → Queue**:
|
||||
```php
|
||||
#[OnEvent]
|
||||
public function generateInvoicePdf(OrderCreatedEvent $event): void
|
||||
{
|
||||
// Heavy operation - push to queue
|
||||
$this->queue->push(
|
||||
JobPayload::immediate(
|
||||
new GenerateInvoiceJob($event->orderId)
|
||||
)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Light Operations → Synchronous**:
|
||||
```php
|
||||
#[OnEvent]
|
||||
public function logOrderCreation(OrderCreatedEvent $event): void
|
||||
{
|
||||
// Light operation - execute immediately
|
||||
$this->logger->info('Order created', [
|
||||
'order_id' => $event->orderId->toString()
|
||||
]);
|
||||
}
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Event Sourcing Pattern
|
||||
|
||||
```php
|
||||
final class OrderEventStore
|
||||
{
|
||||
#[OnEvent]
|
||||
public function appendEvent(DomainEvent $event): void
|
||||
{
|
||||
$this->eventStore->append([
|
||||
'event_type' => get_class($event),
|
||||
'event_data' => json_encode($event),
|
||||
'occurred_at' => $event->occurredAt->format('Y-m-d H:i:s'),
|
||||
'aggregate_id' => $event->getAggregateId()
|
||||
]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Saga Pattern
|
||||
|
||||
```php
|
||||
final readonly class OrderSaga
|
||||
{
|
||||
#[OnEvent(priority: 100)]
|
||||
public function onOrderCreated(OrderCreatedEvent $event): void
|
||||
{
|
||||
// Step 1: Reserve inventory
|
||||
$this->inventoryService->reserve($event->items);
|
||||
}
|
||||
|
||||
#[OnEvent(priority: 90)]
|
||||
public function onInventoryReserved(InventoryReservedEvent $event): void
|
||||
{
|
||||
// Step 2: Process payment
|
||||
$this->paymentService->charge($event->orderId);
|
||||
}
|
||||
|
||||
#[OnEvent(priority: 80)]
|
||||
public function onPaymentCompleted(PaymentCompletedEvent $event): void
|
||||
{
|
||||
// Step 3: Ship order
|
||||
$this->shippingService->ship($event->orderId);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### CQRS Pattern
|
||||
|
||||
```php
|
||||
// Command Side - Dispatches Events
|
||||
final readonly class CreateOrderHandler
|
||||
{
|
||||
public function handle(CreateOrderCommand $command): Order
|
||||
{
|
||||
$order = Order::create($command);
|
||||
$this->repository->save($order);
|
||||
|
||||
$this->eventBus->dispatch(
|
||||
new OrderCreatedEvent($order)
|
||||
);
|
||||
|
||||
return $order;
|
||||
}
|
||||
}
|
||||
|
||||
// Query Side - Maintains Read Model
|
||||
#[OnEvent]
|
||||
public function updateOrderReadModel(OrderCreatedEvent $event): void
|
||||
{
|
||||
$this->orderReadModel->insert([
|
||||
'order_id' => $event->orderId->toString(),
|
||||
'user_id' => $event->userId->toString(),
|
||||
'total' => $event->total->toDecimal(),
|
||||
'status' => 'created'
|
||||
]);
|
||||
}
|
||||
```
|
||||
|
||||
## Framework Integration
|
||||
|
||||
### With Queue System
|
||||
|
||||
```php
|
||||
#[OnEvent]
|
||||
public function queueHeavyTask(OrderCreatedEvent $event): void
|
||||
{
|
||||
$this->queue->push(
|
||||
JobPayload::immediate(
|
||||
new ProcessOrderAnalyticsJob($event->orderId)
|
||||
)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### With Scheduler
|
||||
|
||||
```php
|
||||
#[OnEvent]
|
||||
public function scheduleReminder(OrderCreatedEvent $event): void
|
||||
{
|
||||
$this->scheduler->schedule(
|
||||
'send-order-reminder-' . $event->orderId,
|
||||
OneTimeSchedule::at(Timestamp::now()->addDays(3)),
|
||||
fn() => $this->emailService->sendReminder($event->orderId)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### With Cache System
|
||||
|
||||
```php
|
||||
#[OnEvent]
|
||||
public function invalidateCache(OrderCreatedEvent $event): void
|
||||
{
|
||||
$this->cache->forget(
|
||||
CacheTag::fromString("user_{$event->userId}")
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
The Event System provides:
|
||||
- ✅ **Decoupled Architecture**: Loose coupling via events
|
||||
- ✅ **Flexible Handling**: Priority-based, propagation control
|
||||
- ✅ **Automatic Discovery**: `#[OnEvent]` attribute registration
|
||||
- ✅ **Multiple Patterns**: Domain events, application events, async processing
|
||||
- ✅ **Framework Integration**: Works with Queue, Scheduler, Cache
|
||||
- ✅ **Testing Support**: Easy to unit test and integration test
|
||||
- ✅ **Performance**: Efficient handler execution with priority optimization
|
||||
|
||||
**When to Use Events**:
|
||||
- Decoupling domain logic from side effects (email, notifications)
|
||||
- Cross-module communication without tight coupling
|
||||
- Audit trail and event sourcing
|
||||
- Async processing of non-critical operations
|
||||
- CQRS and saga patterns
|
||||
|
||||
**When NOT to Use Events**:
|
||||
- Synchronous business logic requiring immediate results
|
||||
- Simple function calls (don't over-engineer)
|
||||
- Performance-critical paths (events have overhead)
|
||||
- Operations requiring transactional guarantees across handlers
|
||||
@@ -1,965 +0,0 @@
|
||||
# Filesystem Patterns
|
||||
|
||||
Comprehensive guide to the Custom PHP Framework's Filesystem module.
|
||||
|
||||
## Overview
|
||||
|
||||
The Filesystem module provides a robust, type-safe, and security-focused abstraction for file operations. Built on framework principles of immutability, readonly classes, and value objects.
|
||||
|
||||
**Core Components**:
|
||||
- `FileStorage` - Main filesystem operations interface
|
||||
- `FileValidator` - Security-focused path and content validation
|
||||
- `SerializerRegistry` - Automatic serializer detection and management
|
||||
- `FileOperationContext` - Rich logging context for operations
|
||||
- `TemporaryDirectory` - Safe temporary file management
|
||||
|
||||
---
|
||||
|
||||
## FileStorage
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```php
|
||||
use App\Framework\Filesystem\FileStorage;
|
||||
use App\Framework\Filesystem\FileValidator;
|
||||
use App\Framework\Logging\Logger;
|
||||
|
||||
// Simple storage without validation or logging
|
||||
$storage = new FileStorage('/var/www/storage');
|
||||
|
||||
// Write file
|
||||
$storage->put('documents/report.txt', 'File contents');
|
||||
|
||||
// Read file
|
||||
$content = $storage->get('documents/report.txt');
|
||||
|
||||
// Check existence
|
||||
if ($storage->exists('documents/report.txt')) {
|
||||
// File exists
|
||||
}
|
||||
|
||||
// Delete file
|
||||
$storage->delete('documents/report.txt');
|
||||
|
||||
// Copy file
|
||||
$storage->copy('source.txt', 'destination.txt');
|
||||
|
||||
// List directory
|
||||
$files = $storage->files('documents');
|
||||
```
|
||||
|
||||
### With Validator Integration
|
||||
|
||||
```php
|
||||
// Create validator with security defaults
|
||||
$validator = FileValidator::createDefault();
|
||||
|
||||
// Initialize storage with validator
|
||||
$storage = new FileStorage(
|
||||
baseDirectory: '/var/www/uploads',
|
||||
validator: $validator
|
||||
);
|
||||
|
||||
// All operations are now validated
|
||||
try {
|
||||
$storage->put('file.txt', 'content'); // Validates path, extension, size
|
||||
} catch (FileValidationException $e) {
|
||||
// Handle validation failure
|
||||
}
|
||||
```
|
||||
|
||||
### With Logger Integration
|
||||
|
||||
```php
|
||||
use App\Framework\Logging\Logger;
|
||||
|
||||
$storage = new FileStorage(
|
||||
baseDirectory: '/var/www/storage',
|
||||
validator: FileValidator::createDefault(),
|
||||
logger: $container->get(Logger::class)
|
||||
);
|
||||
|
||||
// Operations are automatically logged with severity levels:
|
||||
// - high severity: DELETE, MOVE → warning level
|
||||
// - large operations (>10MB) → info level
|
||||
// - normal operations → debug level
|
||||
$storage->delete('important.txt'); // Logs as WARNING
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## FileValidator
|
||||
|
||||
### Security-Focused Validation
|
||||
|
||||
The `FileValidator` provides multiple layers of security validation:
|
||||
|
||||
**1. Path Traversal Prevention**
|
||||
- Detects `../`, `..\\`, URL-encoded variants
|
||||
- Prevents null bytes in paths
|
||||
- Optional base directory restriction
|
||||
|
||||
**2. Extension Filtering**
|
||||
- Whitelist (allowedExtensions) - only specific extensions allowed
|
||||
- Blacklist (blockedExtensions) - dangerous extensions blocked
|
||||
- Case-insensitive matching
|
||||
|
||||
**3. File Size Limits**
|
||||
- Maximum file size enforcement
|
||||
- Uses `FileSize` value object for type safety
|
||||
- Human-readable error messages
|
||||
|
||||
### Factory Methods
|
||||
|
||||
```php
|
||||
// Default validator - blocks dangerous extensions, 100MB max
|
||||
$validator = FileValidator::createDefault();
|
||||
// Blocks: exe, bat, sh, cmd, com
|
||||
// Max size: 100MB
|
||||
|
||||
// Strict validator - only whitelisted extensions allowed
|
||||
$validator = FileValidator::createStrict(['txt', 'pdf', 'docx']);
|
||||
// Only allows: txt, pdf, docx
|
||||
// Max size: 50MB
|
||||
|
||||
// Upload validator - secure upload configuration
|
||||
$validator = FileValidator::forUploads();
|
||||
// Allows: jpg, jpeg, png, gif, pdf, txt, csv, json
|
||||
// Blocks: exe, bat, sh, cmd, com, php, phtml
|
||||
// Max size: 10MB
|
||||
|
||||
// Image validator - image files only
|
||||
$validator = FileValidator::forImages();
|
||||
// Allows: jpg, jpeg, png, gif, webp, svg
|
||||
// Max size: 5MB
|
||||
|
||||
// Custom max size
|
||||
$validator = FileValidator::forUploads(FileSize::fromMegabytes(50));
|
||||
```
|
||||
|
||||
### Validation Methods
|
||||
|
||||
```php
|
||||
// Individual validation methods
|
||||
$validator->validatePath($path); // Path traversal, null bytes
|
||||
$validator->validateExtension($path); // Extension whitelist/blacklist
|
||||
$validator->validateFileSize($size); // Size limits
|
||||
$validator->validateExists($path); // File existence
|
||||
$validator->validateReadable($path); // Read permissions
|
||||
$validator->validateWritable($path); // Write permissions
|
||||
|
||||
// Composite validation for common operations
|
||||
$validator->validateRead($path); // Path + exists + readable
|
||||
$validator->validateWrite($path, $size); // Path + extension + size + writable
|
||||
$validator->validateUpload($path, $size); // Path + extension + size
|
||||
|
||||
// Query methods (non-throwing)
|
||||
$isAllowed = $validator->isExtensionAllowed('pdf');
|
||||
$allowedExts = $validator->getAllowedExtensions();
|
||||
$blockedExts = $validator->getBlockedExtensions();
|
||||
$maxSize = $validator->getMaxFileSize();
|
||||
```
|
||||
|
||||
### Custom Validator Configuration
|
||||
|
||||
```php
|
||||
use App\Framework\Core\ValueObjects\FileSize;
|
||||
|
||||
$validator = new FileValidator(
|
||||
allowedExtensions: ['json', 'xml', 'yaml'],
|
||||
blockedExtensions: null, // Don't use blacklist with whitelist
|
||||
maxFileSize: FileSize::fromMegabytes(25),
|
||||
baseDirectory: '/var/www/uploads' // Restrict to this directory
|
||||
);
|
||||
```
|
||||
|
||||
### Exception Handling
|
||||
|
||||
```php
|
||||
use App\Framework\Filesystem\Exceptions\FileValidationException;
|
||||
use App\Framework\Filesystem\Exceptions\FileNotFoundException;
|
||||
use App\Framework\Filesystem\Exceptions\FilePermissionException;
|
||||
|
||||
try {
|
||||
$validator->validateUpload('../../../etc/passwd', FileSize::fromKilobytes(1));
|
||||
} catch (FileValidationException $e) {
|
||||
// Path traversal detected
|
||||
error_log($e->getMessage());
|
||||
// "Path traversal attempt detected"
|
||||
}
|
||||
|
||||
try {
|
||||
$validator->validateUpload('malware.exe', FileSize::fromKilobytes(100));
|
||||
} catch (FileValidationException $e) {
|
||||
// Blocked extension
|
||||
// "File extension '.exe' is blocked"
|
||||
}
|
||||
|
||||
try {
|
||||
$validator->validateRead('/nonexistent/file.txt');
|
||||
} catch (FileNotFoundException $e) {
|
||||
// File not found
|
||||
}
|
||||
|
||||
try {
|
||||
$validator->validateWrite('/readonly/path/file.txt');
|
||||
} catch (FilePermissionException $e) {
|
||||
// Permission denied
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## SerializerRegistry
|
||||
|
||||
### Auto-Detection and Management
|
||||
|
||||
The `SerializerRegistry` automatically detects and manages file serializers based on extensions and MIME types.
|
||||
|
||||
### Default Registry
|
||||
|
||||
```php
|
||||
use App\Framework\Filesystem\SerializerRegistry;
|
||||
|
||||
// Creates registry with common serializers pre-registered
|
||||
$registry = SerializerRegistry::createDefault();
|
||||
|
||||
// Automatically includes:
|
||||
// - JsonSerializer (.json, application/json)
|
||||
// - XmlSerializer (.xml, application/xml, text/xml)
|
||||
// - CsvSerializer (.csv, text/csv)
|
||||
// - YamlSerializer (.yaml, .yml, application/yaml)
|
||||
```
|
||||
|
||||
### Auto-Detection
|
||||
|
||||
```php
|
||||
// Detect serializer from file path
|
||||
$serializer = $registry->detectFromPath('/data/config.json');
|
||||
// Returns: JsonSerializer
|
||||
|
||||
$serializer = $registry->detectFromPath('/exports/data.csv');
|
||||
// Returns: CsvSerializer
|
||||
|
||||
// Get by extension
|
||||
$serializer = $registry->getByExtension('xml');
|
||||
|
||||
// Get by MIME type
|
||||
$serializer = $registry->getByMimeType('application/json');
|
||||
```
|
||||
|
||||
### Custom Serializers
|
||||
|
||||
```php
|
||||
// Implement Serializer interface
|
||||
final readonly class TomlSerializer implements Serializer
|
||||
{
|
||||
public function serialize(mixed $data): string
|
||||
{
|
||||
return Toml::encode($data);
|
||||
}
|
||||
|
||||
public function unserialize(string $data): mixed
|
||||
{
|
||||
return Toml::decode($data);
|
||||
}
|
||||
|
||||
public function getSupportedExtensions(): array
|
||||
{
|
||||
return ['toml'];
|
||||
}
|
||||
|
||||
public function getSupportedMimeTypes(): array
|
||||
{
|
||||
return ['application/toml'];
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return 'toml';
|
||||
}
|
||||
}
|
||||
|
||||
// Register custom serializer
|
||||
$registry->register(new TomlSerializer());
|
||||
|
||||
// Set as default serializer
|
||||
$registry->setDefault(new TomlSerializer());
|
||||
|
||||
// Use auto-detection
|
||||
$serializer = $registry->detectFromPath('config.toml');
|
||||
```
|
||||
|
||||
### Registry Statistics
|
||||
|
||||
```php
|
||||
$stats = $registry->getStatistics();
|
||||
// Returns:
|
||||
// [
|
||||
// 'total_serializers' => 5,
|
||||
// 'total_extensions' => 8,
|
||||
// 'total_mime_types' => 6,
|
||||
// 'has_default' => true,
|
||||
// 'default_serializer' => 'json'
|
||||
// ]
|
||||
|
||||
// Get all registered names
|
||||
$names = $registry->getRegisteredNames();
|
||||
// Returns: ['json', 'xml', 'csv', 'yaml', 'toml']
|
||||
|
||||
// List all serializers
|
||||
$serializers = $registry->getAll();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## FileOperationContext
|
||||
|
||||
### Logging Context with Severity Levels
|
||||
|
||||
`FileOperationContext` provides rich metadata for logging filesystem operations with automatic severity classification.
|
||||
|
||||
### Severity Levels
|
||||
|
||||
**High Severity** (logged as WARNING):
|
||||
- DELETE - File deletion
|
||||
- DELETE_DIRECTORY - Directory deletion
|
||||
- MOVE - File/directory move
|
||||
|
||||
**Medium Severity** (logged as INFO if large, DEBUG otherwise):
|
||||
- WRITE - File write
|
||||
- COPY - File copy
|
||||
- CREATE_DIRECTORY - Directory creation
|
||||
|
||||
**Low Severity** (logged as DEBUG):
|
||||
- READ - File read
|
||||
- LIST_DIRECTORY - Directory listing
|
||||
- GET_METADATA - Metadata retrieval
|
||||
- All other read operations
|
||||
|
||||
### Factory Methods
|
||||
|
||||
```php
|
||||
use App\Framework\Filesystem\ValueObjects\FileOperationContext;
|
||||
use App\Framework\Filesystem\ValueObjects\FileOperation;
|
||||
use App\Framework\Core\ValueObjects\FileSize;
|
||||
|
||||
// Simple operation
|
||||
$context = FileOperationContext::forOperation(
|
||||
FileOperation::DELETE,
|
||||
'/path/to/file.txt'
|
||||
);
|
||||
|
||||
// Operation with destination (copy, move)
|
||||
$context = FileOperationContext::forOperationWithDestination(
|
||||
FileOperation::COPY,
|
||||
'/source/file.txt',
|
||||
'/dest/file.txt'
|
||||
);
|
||||
|
||||
// Write operation with size tracking
|
||||
$context = FileOperationContext::forWrite(
|
||||
'/path/to/file.txt',
|
||||
FileSize::fromKilobytes(150),
|
||||
userId: 'user123'
|
||||
);
|
||||
|
||||
// Read operation with size tracking
|
||||
$context = FileOperationContext::forRead(
|
||||
'/path/to/file.txt',
|
||||
FileSize::fromMegabytes(5)
|
||||
);
|
||||
```
|
||||
|
||||
### Context Enhancement
|
||||
|
||||
```php
|
||||
// Add metadata
|
||||
$context = $context->withMetadata([
|
||||
'source' => 'upload',
|
||||
'mime_type' => 'application/pdf',
|
||||
'original_filename' => 'document.pdf'
|
||||
]);
|
||||
|
||||
// Add user ID
|
||||
$context = $context->withUserId('admin');
|
||||
|
||||
// Metadata merges with existing data
|
||||
$context = $context->withMetadata(['key1' => 'value1'])
|
||||
->withMetadata(['key2' => 'value2']);
|
||||
// Results in: ['key1' => 'value1', 'key2' => 'value2']
|
||||
```
|
||||
|
||||
### Context Queries
|
||||
|
||||
```php
|
||||
// Check severity
|
||||
if ($context->isHighSeverity()) {
|
||||
// High severity operation (DELETE, MOVE, DELETE_DIRECTORY)
|
||||
}
|
||||
|
||||
// Check operation type
|
||||
if ($context->isWriteOperation()) {
|
||||
// Write operation (WRITE, DELETE, MOVE, etc.)
|
||||
}
|
||||
|
||||
// Check operation size
|
||||
if ($context->isLargeOperation()) {
|
||||
// Large operation (>10MB)
|
||||
}
|
||||
```
|
||||
|
||||
### Logging Integration
|
||||
|
||||
```php
|
||||
// Convert to array for structured logging
|
||||
$logData = $context->toArray();
|
||||
// Returns:
|
||||
// [
|
||||
// 'operation' => 'write',
|
||||
// 'operation_name' => 'file.write',
|
||||
// 'path' => '/path/to/file.txt',
|
||||
// 'timestamp' => '2025-01-15T10:30:00+00:00',
|
||||
// 'severity' => 'medium',
|
||||
// 'bytes_affected' => 153600,
|
||||
// 'bytes_affected_human' => '150 KB',
|
||||
// 'user_id' => 'user123',
|
||||
// 'metadata' => ['source' => 'upload']
|
||||
// ]
|
||||
|
||||
// Human-readable string
|
||||
$message = $context->toString();
|
||||
// Returns: "Write file contents, path: /path/to/file.txt, bytes: 150 KB, user: user123"
|
||||
```
|
||||
|
||||
### Automatic Logging in FileStorage
|
||||
|
||||
```php
|
||||
// FileStorage automatically logs operations based on severity:
|
||||
private function logOperation(FileOperationContext $context): void
|
||||
{
|
||||
if ($this->logger === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$logContext = LogContext::fromArray($context->toArray());
|
||||
|
||||
if ($context->isHighSeverity()) {
|
||||
// DELETE, MOVE, DELETE_DIRECTORY
|
||||
$this->logger->framework->warning($context->toString(), $logContext);
|
||||
} elseif ($context->isLargeOperation()) {
|
||||
// Operations >10MB
|
||||
$this->logger->framework->info($context->toString(), $logContext);
|
||||
} else {
|
||||
// Normal operations
|
||||
$this->logger->framework->debug($context->toString(), $logContext);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## TemporaryDirectory
|
||||
|
||||
### Safe Temporary File Management
|
||||
|
||||
`TemporaryDirectory` provides automatic cleanup and safe handling of temporary files.
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```php
|
||||
use App\Framework\Filesystem\TemporaryDirectory;
|
||||
|
||||
// Create with auto-cleanup on destruct
|
||||
$temp = TemporaryDirectory::create();
|
||||
|
||||
// Get path to temp directory
|
||||
$path = $temp->path();
|
||||
|
||||
// Get FilePath for file in temp directory
|
||||
$filePath = $temp->filePath('test.txt');
|
||||
|
||||
// Write files normally
|
||||
file_put_contents($filePath->toString(), 'content');
|
||||
|
||||
// Auto-deletes on destruct
|
||||
unset($temp); // Directory and all contents deleted
|
||||
```
|
||||
|
||||
### Advanced Configuration
|
||||
|
||||
```php
|
||||
// Custom name
|
||||
$temp = TemporaryDirectory::create()
|
||||
->name('my-temp-dir')
|
||||
->force(); // Overwrite if exists
|
||||
|
||||
// Custom location
|
||||
$temp = TemporaryDirectory::create()
|
||||
->location('/custom/tmp')
|
||||
->name('test-dir')
|
||||
->force();
|
||||
|
||||
// Disable auto-delete
|
||||
$temp = TemporaryDirectory::create()
|
||||
->doNotDeleteAutomatically();
|
||||
|
||||
// Manual cleanup
|
||||
$temp->empty(); // Empty directory contents
|
||||
$temp->delete(); // Delete directory and contents
|
||||
```
|
||||
|
||||
### Fluent Interface
|
||||
|
||||
```php
|
||||
$temp = TemporaryDirectory::create()
|
||||
->name('upload-processing')
|
||||
->location('/var/tmp')
|
||||
->force()
|
||||
->create(); // Explicitly create
|
||||
|
||||
$processedFile = $temp->filePath('processed.txt');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Always Use FileValidator for User Input
|
||||
|
||||
```php
|
||||
// ❌ UNSAFE - no validation
|
||||
$storage = new FileStorage('/uploads');
|
||||
$storage->put($_FILES['file']['name'], file_get_contents($_FILES['file']['tmp_name']));
|
||||
|
||||
// ✅ SAFE - with validation
|
||||
$validator = FileValidator::forUploads();
|
||||
$storage = new FileStorage('/uploads', validator: $validator);
|
||||
|
||||
try {
|
||||
$validator->validateUpload(
|
||||
$_FILES['file']['name'],
|
||||
FileSize::fromBytes($_FILES['file']['size'])
|
||||
);
|
||||
$storage->put($_FILES['file']['name'], file_get_contents($_FILES['file']['tmp_name']));
|
||||
} catch (FileValidationException $e) {
|
||||
// Handle validation error
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Use Appropriate Validators
|
||||
|
||||
```php
|
||||
// Image uploads
|
||||
$validator = FileValidator::forImages(FileSize::fromMegabytes(5));
|
||||
|
||||
// Document uploads
|
||||
$validator = FileValidator::forUploads(FileSize::fromMegabytes(20));
|
||||
|
||||
// Strict validation for config files
|
||||
$validator = FileValidator::createStrict(['json', 'yaml', 'toml']);
|
||||
```
|
||||
|
||||
### 3. Log High-Severity Operations
|
||||
|
||||
```php
|
||||
// Always use logger for production systems
|
||||
$storage = new FileStorage(
|
||||
baseDirectory: '/var/www/storage',
|
||||
validator: FileValidator::createDefault(),
|
||||
logger: $logger // Critical for audit trail
|
||||
);
|
||||
|
||||
// High-severity operations are automatically logged as WARNING
|
||||
$storage->delete('/important/document.pdf');
|
||||
```
|
||||
|
||||
### 4. Use TemporaryDirectory for Processing
|
||||
|
||||
```php
|
||||
// Process uploads safely
|
||||
$temp = TemporaryDirectory::create();
|
||||
|
||||
try {
|
||||
// Extract archive to temp directory
|
||||
$archive->extractTo($temp->path());
|
||||
|
||||
// Process files
|
||||
foreach ($temp->files() as $file) {
|
||||
$this->processFile($file);
|
||||
}
|
||||
|
||||
// Move processed files to final location
|
||||
$storage->copy($temp->filePath('result.txt'), '/final/result.txt');
|
||||
} finally {
|
||||
// Auto-cleanup on scope exit
|
||||
unset($temp);
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Combine SerializerRegistry with FileStorage
|
||||
|
||||
```php
|
||||
$registry = SerializerRegistry::createDefault();
|
||||
$storage = new FileStorage('/data', validator: FileValidator::createDefault());
|
||||
|
||||
// Auto-detect serializer and deserialize
|
||||
$serializer = $registry->detectFromPath('config.json');
|
||||
$data = $serializer->unserialize($storage->get('config.json'));
|
||||
|
||||
// Serialize and store
|
||||
$content = $serializer->serialize(['key' => 'value']);
|
||||
$storage->put('output.json', $content);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Path Traversal Prevention
|
||||
|
||||
```php
|
||||
// FileValidator blocks these automatically:
|
||||
$validator->validatePath('../../../etc/passwd'); // ❌ BLOCKED
|
||||
$validator->validatePath('..\\..\\windows\\system32'); // ❌ BLOCKED
|
||||
$validator->validatePath('%2e%2e/etc/passwd'); // ❌ BLOCKED (URL-encoded)
|
||||
$validator->validatePath("/path/with\0nullbyte"); // ❌ BLOCKED (null byte)
|
||||
```
|
||||
|
||||
### Extension Filtering
|
||||
|
||||
```php
|
||||
// Always use whitelist for user uploads
|
||||
$validator = FileValidator::createStrict(['jpg', 'png', 'pdf']);
|
||||
|
||||
// Or use specialized validators
|
||||
$validator = FileValidator::forImages(); // Images only
|
||||
$validator = FileValidator::forUploads(); // Blocks dangerous extensions
|
||||
|
||||
// NEVER trust client-provided MIME types
|
||||
// Use extension-based validation instead
|
||||
```
|
||||
|
||||
### Base Directory Restriction
|
||||
|
||||
```php
|
||||
// Restrict all operations to base directory
|
||||
$validator = new FileValidator(
|
||||
allowedExtensions: null,
|
||||
blockedExtensions: ['exe', 'sh', 'bat'],
|
||||
maxFileSize: FileSize::fromMegabytes(100),
|
||||
baseDirectory: '/var/www/uploads' // Cannot escape this directory
|
||||
);
|
||||
|
||||
// Attempts to escape are blocked
|
||||
$validator->validatePath('/var/www/uploads/../../../etc/passwd'); // ❌ BLOCKED
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Large File Operations
|
||||
|
||||
```php
|
||||
// FileOperationContext detects large operations (>10MB)
|
||||
if ($context->isLargeOperation()) {
|
||||
// Triggers INFO-level logging instead of DEBUG
|
||||
// Consider background processing for very large files
|
||||
}
|
||||
|
||||
// Use streaming for large files
|
||||
$storage->stream('large-file.mp4', function($stream) {
|
||||
while (!feof($stream)) {
|
||||
echo fread($stream, 8192);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Caching Validator Results
|
||||
|
||||
```php
|
||||
// Validator operations are fast, but for high-volume scenarios:
|
||||
$isValid = $validator->isExtensionAllowed('pdf'); // Non-throwing check
|
||||
|
||||
if ($isValid) {
|
||||
// Proceed with operation
|
||||
} else {
|
||||
// Reject early without exception overhead
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 Performance Optimizations
|
||||
|
||||
The Filesystem module includes advanced performance optimizations achieved through caching strategies and syscall reduction.
|
||||
|
||||
**Framework Integration**: All performance optimizations are **automatically enabled by default** via `FilesystemInitializer` with sensible production settings. Disable only for debugging:
|
||||
|
||||
```env
|
||||
# Filesystem Performance (caching enabled by default)
|
||||
# Set to true only for debugging performance issues
|
||||
# FILESYSTEM_DISABLE_CACHE=false
|
||||
```
|
||||
|
||||
**Default Settings**:
|
||||
- FileValidator caching: ENABLED (TTL: 60s, Max: 100 entries)
|
||||
- FileStorage directory caching: ENABLED (session-based cache)
|
||||
- clearstatcache() optimization: ENABLED (minimal syscalls)
|
||||
|
||||
### CachedFileValidator - Result Cache
|
||||
|
||||
**Performance Gain**: 99% faster for cached validations
|
||||
|
||||
**Description**: LRU cache decorator for FileValidator that caches validation results (both successes and failures) to avoid repeated expensive validation operations.
|
||||
|
||||
**Automatic Integration**: Enabled by default via `FilesystemInitializer` when resolving `FileValidator::class` from DI container. Disable only for debugging via `FILESYSTEM_DISABLE_CACHE=true` in `.env`.
|
||||
|
||||
**Features**:
|
||||
- LRU eviction when cache size exceeds limit (default: 100 entries)
|
||||
- Configurable TTL (default: 60 seconds)
|
||||
- Caches path and extension validation (not file size/existence checks)
|
||||
- Automatic cache invalidation on TTL expiry
|
||||
- Cache statistics for monitoring
|
||||
|
||||
**Usage**:
|
||||
```php
|
||||
use App\Framework\Filesystem\CachedFileValidator;
|
||||
use App\Framework\Filesystem\FileValidator;
|
||||
|
||||
$validator = FileValidator::createDefault();
|
||||
$cachedValidator = new CachedFileValidator(
|
||||
validator: $validator,
|
||||
cacheTtl: 60, // 60 seconds TTL
|
||||
maxCacheSize: 100 // Max 100 cached results
|
||||
);
|
||||
|
||||
// First call - cache miss (validates fully)
|
||||
$cachedValidator->validatePath('/path/to/file.txt');
|
||||
|
||||
// Second call - cache hit (99% faster)
|
||||
$cachedValidator->validatePath('/path/to/file.txt');
|
||||
|
||||
// Get cache statistics
|
||||
$stats = $cachedValidator->getCacheStats();
|
||||
// ['path_cache_size' => 1, 'extension_cache_size' => 0, ...]
|
||||
```
|
||||
|
||||
**What's Cached**:
|
||||
- ✅ Path validation (traversal checks, null bytes)
|
||||
- ✅ Extension validation (allowed/blocked lists)
|
||||
- ✅ Composite validations (validateRead, validateWrite, validateUpload)
|
||||
|
||||
**What's NOT Cached**:
|
||||
- ❌ File size validation (size can change)
|
||||
- ❌ File existence checks (files can be created/deleted)
|
||||
- ❌ Permission checks (permissions can change)
|
||||
|
||||
**Performance Characteristics**:
|
||||
- Cache hit latency: <0.1ms
|
||||
- Cache miss latency: ~1ms (original validation)
|
||||
- Memory usage: ~100KB for 100 entries
|
||||
|
||||
### CachedFileStorage - Directory Cache
|
||||
|
||||
**Performance Gain**: 25% fewer syscalls for write operations
|
||||
|
||||
**Description**: Decorator for FileStorage that caches directory existence checks to reduce redundant `is_dir()` syscalls during write operations.
|
||||
|
||||
**Automatic Integration**: Enabled by default via `FilesystemInitializer` when resolving `Storage::class`, `FileStorage::class`, or any named storage (`filesystem.storage.*`) from DI container. Disable only for debugging via `FILESYSTEM_DISABLE_CACHE=true` in `.env`.
|
||||
|
||||
**Features**:
|
||||
- Session-based cache (cleared on object destruction)
|
||||
- Write-through caching (directories cached when created or verified)
|
||||
- Conservative strategy (only caches successful operations)
|
||||
- Parent directory recursive caching
|
||||
- O(1) cache lookup performance
|
||||
|
||||
**Usage**:
|
||||
```php
|
||||
use App\Framework\Filesystem\CachedFileStorage;
|
||||
use App\Framework\Filesystem\FileStorage;
|
||||
|
||||
$storage = new FileStorage('/var/www/storage');
|
||||
$cachedStorage = new CachedFileStorage(
|
||||
storage: $storage,
|
||||
basePath: '/var/www/storage'
|
||||
);
|
||||
|
||||
// First write - cache miss (checks directory exists)
|
||||
$cachedStorage->put('nested/deep/file1.txt', 'content1');
|
||||
|
||||
// Second write to same directory - cache hit (skips is_dir check)
|
||||
$cachedStorage->put('nested/deep/file2.txt', 'content2'); // 25% faster
|
||||
|
||||
// Get cache statistics
|
||||
$stats = $cachedStorage->getCacheStats();
|
||||
// ['cached_directories' => 2, 'cache_entries' => [...]]
|
||||
```
|
||||
|
||||
**Cache Strategy**:
|
||||
- Directories cached on first verification or creation
|
||||
- Parent directories automatically cached (if `/a/b/c` exists, `/a/b` and `/a` are cached)
|
||||
- Normalized paths (resolved symlinks, no trailing slashes)
|
||||
- Hash-based cache keys for O(1) lookup
|
||||
|
||||
**Performance Characteristics**:
|
||||
- Cache hit latency: <0.01ms (array lookup)
|
||||
- Cache miss latency: ~1ms (is_dir syscall)
|
||||
- Memory usage: ~50 bytes per cached directory
|
||||
- Syscall reduction: 25% for repeated writes to same directories
|
||||
|
||||
### clearstatcache() Optimization
|
||||
|
||||
**Performance Gain**: ~1-2ms faster read operations
|
||||
|
||||
**Description**: Strategic placement of `clearstatcache()` calls - only before write operations where fresh stat info is critical, removed from read operations where stat cache is valid.
|
||||
|
||||
**Optimization Details**:
|
||||
|
||||
**Before Optimization**:
|
||||
```php
|
||||
// FileStorage::get() - UNNECESSARY
|
||||
public function get(string $path): string
|
||||
{
|
||||
clearstatcache(true, $resolvedPath); // ❌ Unnecessary for reads
|
||||
if (!is_file($resolvedPath)) {
|
||||
throw new FileNotFoundException($path);
|
||||
}
|
||||
|
||||
// Also in error handling
|
||||
clearstatcache(true, $resolvedPath); // ❌ Unnecessary
|
||||
if (!is_file($resolvedPath)) {
|
||||
throw new FileNotFoundException($path);
|
||||
}
|
||||
|
||||
return file_get_contents($resolvedPath);
|
||||
}
|
||||
```
|
||||
|
||||
**After Optimization**:
|
||||
```php
|
||||
// FileStorage::get() - Removed unnecessary clearstatcache
|
||||
public function get(string $path): string
|
||||
{
|
||||
// ✅ No clearstatcache - stat cache is valid for reads
|
||||
if (!is_file($resolvedPath)) {
|
||||
throw new FileNotFoundException($path);
|
||||
}
|
||||
|
||||
return file_get_contents($resolvedPath);
|
||||
}
|
||||
|
||||
// FileStorage::put() - Added necessary clearstatcache
|
||||
public function put(string $path, string $content): void
|
||||
{
|
||||
$dir = dirname($resolvedPath);
|
||||
|
||||
// ✅ Clear stat cache before directory check
|
||||
clearstatcache(true, $dir);
|
||||
|
||||
if (!is_dir($dir)) {
|
||||
mkdir($dir, 0777, true);
|
||||
}
|
||||
|
||||
file_put_contents($resolvedPath, $content);
|
||||
}
|
||||
```
|
||||
|
||||
**Performance Impact**:
|
||||
- Read operations: ~1-2ms faster (removed 2x clearstatcache calls)
|
||||
- Write operations: No performance impact (necessary clearstatcache added)
|
||||
- Stat cache correctness: Maintained for write operations, valid for read operations
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
### Unit Testing with Validators
|
||||
|
||||
```php
|
||||
it('validates file uploads correctly', function () {
|
||||
$validator = FileValidator::forUploads();
|
||||
|
||||
// Valid upload
|
||||
$validator->validateUpload(
|
||||
'/uploads/document.pdf',
|
||||
FileSize::fromMegabytes(2)
|
||||
);
|
||||
expect(true)->toBeTrue(); // No exception
|
||||
|
||||
// Invalid - path traversal
|
||||
try {
|
||||
$validator->validateUpload(
|
||||
'../../../etc/passwd',
|
||||
FileSize::fromKilobytes(1)
|
||||
);
|
||||
expect(true)->toBeFalse('Should throw');
|
||||
} catch (FileValidationException $e) {
|
||||
expect($e->getMessage())->toContain('Path traversal');
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Integration Testing with FileStorage
|
||||
|
||||
```php
|
||||
it('integrates validator with storage', function () {
|
||||
$testDir = sys_get_temp_dir() . '/test_' . uniqid();
|
||||
mkdir($testDir);
|
||||
|
||||
$validator = FileValidator::createStrict(['txt']);
|
||||
$storage = new FileStorage($testDir, validator: $validator);
|
||||
|
||||
// Valid operation
|
||||
$storage->put('allowed.txt', 'content');
|
||||
expect($storage->exists('allowed.txt'))->toBeTrue();
|
||||
|
||||
// Invalid operation
|
||||
try {
|
||||
$storage->put('blocked.exe', 'malicious');
|
||||
expect(true)->toBeFalse('Should throw');
|
||||
} catch (FileValidationException $e) {
|
||||
expect($e->getMessage())->toContain('not allowed');
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
array_map('unlink', glob($testDir . '/*'));
|
||||
rmdir($testDir);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
The Filesystem module provides:
|
||||
|
||||
✅ **Type-Safe Operations** - Value objects throughout (FilePath, FileSize, FileOperation)
|
||||
✅ **Security-First** - Path traversal prevention, extension filtering, size limits
|
||||
✅ **Rich Logging** - Automatic severity classification, detailed context
|
||||
✅ **Auto-Detection** - Serializer registry with extension/MIME type mapping
|
||||
✅ **Immutable Design** - Readonly classes, transformation methods
|
||||
✅ **Framework Compliance** - Follows all framework architectural principles
|
||||
|
||||
**Key Integration Points**:
|
||||
- Works with framework's Logger for audit trails
|
||||
- Uses Value Objects (FileSize, Timestamp, FilePath)
|
||||
- Event system integration available
|
||||
- Queue system integration for async operations
|
||||
- Cache integration for serializer registry
|
||||
|
||||
**Performance Optimizations**:
|
||||
- **CachedFileValidator**: 99% faster validation for repeated paths (LRU cache)
|
||||
- **CachedFileStorage**: 25% fewer syscalls for write operations (directory cache)
|
||||
- **clearstatcache() Optimization**: 1-2ms faster read operations (strategic placement)
|
||||
- Memory-efficient caching strategies
|
||||
- O(1) cache lookup performance
|
||||
|
||||
**Production Ready**:
|
||||
- Comprehensive test coverage (145 tests, 319 assertions)
|
||||
- Security-focused validation
|
||||
- Performance-optimized design with caching
|
||||
- Detailed error messages
|
||||
- Production logging with severity levels
|
||||
735
docs/claude/js-modules-analysis.md
Normal file
735
docs/claude/js-modules-analysis.md
Normal file
@@ -0,0 +1,735 @@
|
||||
# JavaScript Modules Analysis & Recommendations
|
||||
|
||||
**Comprehensive Analysis of Existing JS Modules and Recommendations for New Modules and Refactorings**
|
||||
|
||||
This document provides a detailed analysis of the current JavaScript module ecosystem, identifies areas for improvement, and proposes new modules and refactorings.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Current Module Overview](#current-module-overview)
|
||||
2. [Module Quality Assessment](#module-quality-assessment)
|
||||
3. [Recommended New Modules](#recommended-new-modules)
|
||||
4. [Refactoring Recommendations](#refactoring-recommendations)
|
||||
5. [Priority Matrix](#priority-matrix)
|
||||
6. [Implementation Roadmap](#implementation-roadmap)
|
||||
|
||||
---
|
||||
|
||||
## Current Module Overview
|
||||
|
||||
### Module Categories
|
||||
|
||||
#### 1. **Core Framework Modules**
|
||||
- `livecomponent/` - LiveComponents system (well-structured, modern)
|
||||
- `ui/` - UI components (Modal, Dialog, Lightbox)
|
||||
- `api-manager/` - Web API wrappers (comprehensive)
|
||||
- `sse/` - Server-Sent Events client
|
||||
|
||||
#### 2. **Form & Input Modules**
|
||||
- `form-handling/` - Form validation and submission
|
||||
- `form-autosave.js` - Auto-save functionality
|
||||
|
||||
#### 3. **Navigation & Routing**
|
||||
- `spa-router/` - Single Page Application router
|
||||
|
||||
#### 4. **Animation & Effects Modules**
|
||||
- `canvas-animations/` - Canvas-based animations
|
||||
- `scrollfx/` - Scroll-based animations
|
||||
- `parallax/` - Parallax effects
|
||||
- `smooth-scroll/` - Smooth scrolling
|
||||
- `scroll-timeline/` - Scroll timeline animations
|
||||
- `scroll-loop/` - Infinite scroll loops
|
||||
- `scroll-dependent/` - Scroll-dependent effects
|
||||
- `sticky-fade/` - Sticky fade effects
|
||||
- `sticky-steps/` - Sticky step animations
|
||||
- `inertia-scroll/` - Inertia scrolling
|
||||
- `wheel-boost/` - Wheel boost effects
|
||||
- `noise/` - Noise effects
|
||||
|
||||
#### 5. **Media & Image Modules**
|
||||
- `image-manager/` - Image gallery, upload, modal
|
||||
|
||||
#### 6. **Utility Modules**
|
||||
- `csrf-auto-refresh.js` - CSRF token management
|
||||
- `hot-reload.js` - Hot reload functionality
|
||||
- `performance-profiler/` - Performance profiling
|
||||
- `webpush/` - Web Push notifications
|
||||
|
||||
#### 7. **Admin Modules**
|
||||
- `admin/data-table.js` - Admin data tables
|
||||
|
||||
---
|
||||
|
||||
## Module Quality Assessment
|
||||
|
||||
### ✅ Well-Structured Modules (Keep as-is)
|
||||
|
||||
1. **livecomponent/** - Excellent structure, modern patterns
|
||||
- ✅ Modular architecture
|
||||
- ✅ TypeScript definitions
|
||||
- ✅ Error handling
|
||||
- ✅ Comprehensive documentation
|
||||
|
||||
2. **api-manager/** - Well-organized Web API wrappers
|
||||
- ✅ Consistent API
|
||||
- ✅ Feature detection
|
||||
- ✅ Good separation of concerns
|
||||
|
||||
3. **form-handling/** - Solid form handling
|
||||
- ✅ Clear separation (Handler, Validator, State)
|
||||
- ✅ Progressive enhancement
|
||||
- ⚠️ Could benefit from LiveComponent integration
|
||||
|
||||
4. **ui/** - Clean UI component system
|
||||
- ✅ Reusable components
|
||||
- ✅ Consistent API
|
||||
- ⚠️ Could expand with more components
|
||||
|
||||
### ⚠️ Modules Needing Refactoring
|
||||
|
||||
1. **spa-router/** - Good but could be improved
|
||||
- ⚠️ Mixed concerns (routing + transitions)
|
||||
- ⚠️ Could better integrate with LiveComponents
|
||||
- ⚠️ Module re-initialization could be cleaner
|
||||
|
||||
2. **form-autosave.js** - Standalone file
|
||||
- ⚠️ Should be part of form-handling module
|
||||
- ⚠️ No module system integration
|
||||
|
||||
3. **Scroll Animation Modules** - Too many separate modules
|
||||
- ⚠️ `scrollfx/`, `parallax/`, `scroll-timeline/`, `scroll-loop/`, `scroll-dependent/`, `sticky-fade/`, `sticky-steps/` - Could be unified
|
||||
- ⚠️ Duplicate functionality
|
||||
- ⚠️ Inconsistent APIs
|
||||
|
||||
4. **image-manager/** - Good but could be enhanced
|
||||
- ⚠️ Could integrate with LiveComponent file uploads
|
||||
- ⚠️ EventEmitter pattern could be modernized
|
||||
|
||||
5. **performance-profiler/** - Standalone
|
||||
- ⚠️ Could integrate with LiveComponent DevTools
|
||||
- ⚠️ Should be part of development tools
|
||||
|
||||
### ❌ Modules Needing Major Refactoring
|
||||
|
||||
1. **Multiple Scroll Modules** - Consolidation needed
|
||||
- ❌ 8+ separate scroll-related modules
|
||||
- ❌ Inconsistent patterns
|
||||
- ❌ Hard to maintain
|
||||
|
||||
2. **csrf-auto-refresh.js** - Standalone file
|
||||
- ❌ Should be part of security module
|
||||
- ❌ No module system integration
|
||||
|
||||
---
|
||||
|
||||
## Recommended New Modules
|
||||
|
||||
### 1. **State Management Module** (High Priority)
|
||||
|
||||
**Purpose**: Centralized state management for client-side state
|
||||
|
||||
**Features**:
|
||||
- Reactive state store (similar to Redux/Vuex)
|
||||
- State persistence (localStorage, sessionStorage)
|
||||
- State synchronization across tabs
|
||||
- Time-travel debugging
|
||||
- Integration with LiveComponents
|
||||
|
||||
**Use Cases**:
|
||||
- User preferences
|
||||
- Shopping cart state
|
||||
- UI state (sidebar open/closed, theme)
|
||||
- Form drafts
|
||||
|
||||
**API Example**:
|
||||
```javascript
|
||||
import { StateManager } from './modules/state-manager/index.js';
|
||||
|
||||
const store = StateManager.create({
|
||||
user: { name: '', email: '' },
|
||||
cart: { items: [], total: 0 },
|
||||
ui: { sidebarOpen: false }
|
||||
});
|
||||
|
||||
// Reactive updates
|
||||
store.subscribe('cart', (cart) => {
|
||||
updateCartUI(cart);
|
||||
});
|
||||
|
||||
// Actions
|
||||
store.dispatch('cart.addItem', { id: 1, name: 'Product' });
|
||||
```
|
||||
|
||||
**Priority**: High
|
||||
**Effort**: Medium (2-3 days)
|
||||
|
||||
---
|
||||
|
||||
### 2. **Validation Module** (High Priority)
|
||||
|
||||
**Purpose**: Standalone validation system (not just forms)
|
||||
|
||||
**Features**:
|
||||
- Field-level validation
|
||||
- Schema-based validation (JSON Schema)
|
||||
- Async validation
|
||||
- Custom validation rules
|
||||
- Integration with LiveComponents
|
||||
- Integration with form-handling
|
||||
|
||||
**Use Cases**:
|
||||
- Form validation
|
||||
- API response validation
|
||||
- User input validation
|
||||
- Data transformation validation
|
||||
|
||||
**API Example**:
|
||||
```javascript
|
||||
import { Validator } from './modules/validation/index.js';
|
||||
|
||||
const validator = Validator.create({
|
||||
email: {
|
||||
type: 'email',
|
||||
required: true,
|
||||
message: 'Invalid email address'
|
||||
},
|
||||
age: {
|
||||
type: 'number',
|
||||
min: 18,
|
||||
max: 100
|
||||
}
|
||||
});
|
||||
|
||||
const result = await validator.validate({ email: 'test@example.com', age: 25 });
|
||||
```
|
||||
|
||||
**Priority**: High
|
||||
**Effort**: Medium (2-3 days)
|
||||
|
||||
---
|
||||
|
||||
### 3. **Cache Manager Module** (Medium Priority)
|
||||
|
||||
**Purpose**: Intelligent caching for API responses and computed values
|
||||
|
||||
**Features**:
|
||||
- Memory cache
|
||||
- IndexedDB cache
|
||||
- Cache invalidation strategies
|
||||
- Cache warming
|
||||
- Cache analytics
|
||||
- Integration with RequestDeduplicator
|
||||
|
||||
**Use Cases**:
|
||||
- API response caching
|
||||
- Computed value caching
|
||||
- Image caching
|
||||
- Search result caching
|
||||
|
||||
**API Example**:
|
||||
```javascript
|
||||
import { CacheManager } from './modules/cache-manager/index.js';
|
||||
|
||||
const cache = CacheManager.create({
|
||||
strategy: 'stale-while-revalidate',
|
||||
ttl: 3600000 // 1 hour
|
||||
});
|
||||
|
||||
// Cache API response
|
||||
const data = await cache.get('users', async () => {
|
||||
return await fetch('/api/users').then(r => r.json());
|
||||
});
|
||||
|
||||
// Invalidate cache
|
||||
cache.invalidate('users');
|
||||
```
|
||||
|
||||
**Priority**: Medium
|
||||
**Effort**: Medium (2-3 days)
|
||||
|
||||
---
|
||||
|
||||
### 4. **Event Bus Module** (Medium Priority)
|
||||
|
||||
**Purpose**: Centralized event system for cross-module communication
|
||||
|
||||
**Features**:
|
||||
- Pub/sub pattern
|
||||
- Namespaced events
|
||||
- Event filtering
|
||||
- Event history
|
||||
- Integration with LiveComponents
|
||||
- Integration with SSE
|
||||
|
||||
**Use Cases**:
|
||||
- Component communication
|
||||
- Module communication
|
||||
- Global notifications
|
||||
- Analytics events
|
||||
|
||||
**API Example**:
|
||||
```javascript
|
||||
import { EventBus } from './modules/event-bus/index.js';
|
||||
|
||||
const bus = EventBus.create();
|
||||
|
||||
// Subscribe
|
||||
bus.on('user:logged-in', (user) => {
|
||||
updateUI(user);
|
||||
});
|
||||
|
||||
// Publish
|
||||
bus.emit('user:logged-in', { id: 1, name: 'John' });
|
||||
|
||||
// Namespaced events
|
||||
bus.on('livecomponent:action-executed', (data) => {
|
||||
console.log('Action executed:', data);
|
||||
});
|
||||
```
|
||||
|
||||
**Priority**: Medium
|
||||
**Effort**: Low (1-2 days)
|
||||
|
||||
---
|
||||
|
||||
### 5. **Storage Manager Module** (Low Priority)
|
||||
|
||||
**Purpose**: Unified storage interface (localStorage, sessionStorage, IndexedDB)
|
||||
|
||||
**Features**:
|
||||
- Unified API for all storage types
|
||||
- Automatic serialization
|
||||
- Storage quotas
|
||||
- Storage migration
|
||||
- Storage analytics
|
||||
|
||||
**Note**: Partially exists in `api-manager/StorageManager`, but could be enhanced
|
||||
|
||||
**Priority**: Low (enhance existing)
|
||||
**Effort**: Low (1 day)
|
||||
|
||||
---
|
||||
|
||||
### 6. **Router Enhancement Module** (Medium Priority)
|
||||
|
||||
**Purpose**: Enhanced routing with guards, middleware, and lazy loading
|
||||
|
||||
**Features**:
|
||||
- Route guards (auth, permissions)
|
||||
- Route middleware
|
||||
- Lazy route loading
|
||||
- Route transitions
|
||||
- Route analytics
|
||||
- Integration with LiveComponents
|
||||
|
||||
**API Example**:
|
||||
```javascript
|
||||
import { Router } from './modules/router/index.js';
|
||||
|
||||
const router = Router.create({
|
||||
routes: [
|
||||
{
|
||||
path: '/dashboard',
|
||||
component: 'DashboardComponent',
|
||||
guard: 'auth',
|
||||
middleware: ['analytics']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
if (requiresAuth(to) && !isAuthenticated()) {
|
||||
next('/login');
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**Priority**: Medium
|
||||
**Effort**: Medium (2-3 days)
|
||||
|
||||
---
|
||||
|
||||
### 7. **Animation System Module** (Low Priority)
|
||||
|
||||
**Purpose**: Unified animation system consolidating all scroll/animation modules
|
||||
|
||||
**Features**:
|
||||
- Unified animation API
|
||||
- Scroll-based animations
|
||||
- Timeline animations
|
||||
- Performance optimizations
|
||||
- Integration with Web Animations API
|
||||
|
||||
**Consolidates**:
|
||||
- `scrollfx/`
|
||||
- `parallax/`
|
||||
- `scroll-timeline/`
|
||||
- `scroll-loop/`
|
||||
- `scroll-dependent/`
|
||||
- `sticky-fade/`
|
||||
- `sticky-steps/`
|
||||
- `canvas-animations/`
|
||||
|
||||
**Priority**: Low (refactoring existing)
|
||||
**Effort**: High (5-7 days)
|
||||
|
||||
---
|
||||
|
||||
### 8. **Error Tracking Module** (High Priority)
|
||||
|
||||
**Purpose**: Centralized error tracking and reporting
|
||||
|
||||
**Features**:
|
||||
- Error collection
|
||||
- Error grouping
|
||||
- Error reporting (to backend)
|
||||
- Error analytics
|
||||
- Integration with ErrorBoundary
|
||||
- Source map support
|
||||
|
||||
**API Example**:
|
||||
```javascript
|
||||
import { ErrorTracker } from './modules/error-tracking/index.js';
|
||||
|
||||
const tracker = ErrorTracker.create({
|
||||
endpoint: '/api/errors',
|
||||
sampleRate: 1.0
|
||||
});
|
||||
|
||||
// Automatic error tracking
|
||||
tracker.init();
|
||||
|
||||
// Manual error reporting
|
||||
tracker.captureException(new Error('Something went wrong'), {
|
||||
context: { userId: 123 },
|
||||
tags: { feature: 'checkout' }
|
||||
});
|
||||
```
|
||||
|
||||
**Priority**: High
|
||||
**Effort**: Medium (2-3 days)
|
||||
|
||||
---
|
||||
|
||||
### 9. **Analytics Module** (Medium Priority)
|
||||
|
||||
**Purpose**: Unified analytics system
|
||||
|
||||
**Features**:
|
||||
- Event tracking
|
||||
- Page view tracking
|
||||
- User behavior tracking
|
||||
- Custom events
|
||||
- Integration with LiveComponents
|
||||
- Privacy-compliant (GDPR)
|
||||
|
||||
**API Example**:
|
||||
```javascript
|
||||
import { Analytics } from './modules/analytics/index.js';
|
||||
|
||||
const analytics = Analytics.create({
|
||||
providers: ['google-analytics', 'custom']
|
||||
});
|
||||
|
||||
analytics.track('purchase', {
|
||||
value: 99.99,
|
||||
currency: 'EUR',
|
||||
items: [{ id: 'product-1', quantity: 1 }]
|
||||
});
|
||||
```
|
||||
|
||||
**Priority**: Medium
|
||||
**Effort**: Medium (2-3 days)
|
||||
|
||||
---
|
||||
|
||||
### 10. **Internationalization (i18n) Module** (Low Priority)
|
||||
|
||||
**Purpose**: Internationalization and localization
|
||||
|
||||
**Features**:
|
||||
- Translation management
|
||||
- Pluralization
|
||||
- Date/time formatting
|
||||
- Number formatting
|
||||
- Currency formatting
|
||||
- Integration with LiveComponents
|
||||
|
||||
**API Example**:
|
||||
```javascript
|
||||
import { i18n } from './modules/i18n/index.js';
|
||||
|
||||
i18n.init({
|
||||
locale: 'de-DE',
|
||||
fallback: 'en-US',
|
||||
translations: {
|
||||
'de-DE': { 'welcome': 'Willkommen' },
|
||||
'en-US': { 'welcome': 'Welcome' }
|
||||
}
|
||||
});
|
||||
|
||||
const message = i18n.t('welcome');
|
||||
```
|
||||
|
||||
**Priority**: Low
|
||||
**Effort**: Medium (2-3 days)
|
||||
|
||||
---
|
||||
|
||||
## Refactoring Recommendations
|
||||
|
||||
### 1. **Consolidate Scroll Animation Modules** (High Priority)
|
||||
|
||||
**Current State**: 8+ separate scroll-related modules
|
||||
|
||||
**Proposed Solution**: Create unified `animation-system/` module
|
||||
|
||||
**Benefits**:
|
||||
- Single API for all animations
|
||||
- Reduced bundle size
|
||||
- Easier maintenance
|
||||
- Better performance
|
||||
- Consistent patterns
|
||||
|
||||
**Implementation**:
|
||||
1. Create `animation-system/` module
|
||||
2. Migrate functionality from existing modules
|
||||
3. Maintain backward compatibility during transition
|
||||
4. Deprecate old modules
|
||||
5. Update documentation
|
||||
|
||||
**Priority**: High
|
||||
**Effort**: High (5-7 days)
|
||||
|
||||
---
|
||||
|
||||
### 2. **Integrate form-autosave into form-handling** (Medium Priority)
|
||||
|
||||
**Current State**: Standalone `form-autosave.js` file
|
||||
|
||||
**Proposed Solution**: Add autosave functionality to `form-handling/` module
|
||||
|
||||
**Benefits**:
|
||||
- Better organization
|
||||
- Shared form state
|
||||
- Consistent API
|
||||
- Easier maintenance
|
||||
|
||||
**Implementation**:
|
||||
1. Move autosave logic into `FormHandler`
|
||||
2. Add autosave configuration options
|
||||
3. Integrate with `FormState`
|
||||
4. Update documentation
|
||||
|
||||
**Priority**: Medium
|
||||
**Effort**: Low (1 day)
|
||||
|
||||
---
|
||||
|
||||
### 3. **Enhance SPA Router with LiveComponent Integration** (Medium Priority)
|
||||
|
||||
**Current State**: SPA Router works independently
|
||||
|
||||
**Proposed Solution**: Better integration with LiveComponents
|
||||
|
||||
**Benefits**:
|
||||
- Automatic LiveComponent initialization after navigation
|
||||
- Better state management
|
||||
- Improved performance
|
||||
- Unified API
|
||||
|
||||
**Implementation**:
|
||||
1. Add LiveComponent auto-initialization
|
||||
2. Integrate with LiveComponentManager
|
||||
3. Handle component state during navigation
|
||||
4. Update documentation
|
||||
|
||||
**Priority**: Medium
|
||||
**Effort**: Medium (2-3 days)
|
||||
|
||||
---
|
||||
|
||||
### 4. **Create Security Module** (Medium Priority)
|
||||
|
||||
**Current State**: `csrf-auto-refresh.js` is standalone
|
||||
|
||||
**Proposed Solution**: Create `security/` module
|
||||
|
||||
**Features**:
|
||||
- CSRF token management
|
||||
- XSS protection helpers
|
||||
- Content Security Policy helpers
|
||||
- Security headers validation
|
||||
|
||||
**Implementation**:
|
||||
1. Create `security/` module
|
||||
2. Move CSRF logic
|
||||
3. Add additional security features
|
||||
4. Update documentation
|
||||
|
||||
**Priority**: Medium
|
||||
**Effort**: Low (1-2 days)
|
||||
|
||||
---
|
||||
|
||||
### 5. **Integrate Performance Profiler with DevTools** (Low Priority)
|
||||
|
||||
**Current State**: Standalone `performance-profiler/` module
|
||||
|
||||
**Proposed Solution**: Integrate with LiveComponent DevTools
|
||||
|
||||
**Benefits**:
|
||||
- Unified developer experience
|
||||
- Better visualization
|
||||
- Easier debugging
|
||||
- Reduced bundle size (dev only)
|
||||
|
||||
**Implementation**:
|
||||
1. Move profiler into DevTools
|
||||
2. Integrate with LiveComponent profiling
|
||||
3. Update UI
|
||||
4. Update documentation
|
||||
|
||||
**Priority**: Low
|
||||
**Effort**: Medium (2-3 days)
|
||||
|
||||
---
|
||||
|
||||
### 6. **Modernize Image Manager** (Low Priority)
|
||||
|
||||
**Current State**: Uses EventEmitter pattern
|
||||
|
||||
**Proposed Solution**: Modernize with ES6 classes and better integration
|
||||
|
||||
**Benefits**:
|
||||
- Modern patterns
|
||||
- Better TypeScript support
|
||||
- Integration with LiveComponent file uploads
|
||||
- Improved performance
|
||||
|
||||
**Implementation**:
|
||||
1. Refactor to ES6 classes
|
||||
2. Add TypeScript definitions
|
||||
3. Integrate with LiveComponent file uploads
|
||||
4. Update documentation
|
||||
|
||||
**Priority**: Low
|
||||
**Effort**: Medium (2-3 days)
|
||||
|
||||
---
|
||||
|
||||
## Priority Matrix
|
||||
|
||||
### High Priority (Implement Soon)
|
||||
|
||||
1. **State Management Module** - Needed for complex applications
|
||||
2. **Validation Module** - Reusable validation logic
|
||||
3. **Error Tracking Module** - Production debugging
|
||||
4. **Consolidate Scroll Animation Modules** - Maintenance burden
|
||||
|
||||
### Medium Priority (Implement Next)
|
||||
|
||||
1. **Cache Manager Module** - Performance optimization
|
||||
2. **Event Bus Module** - Cross-module communication
|
||||
3. **Router Enhancement Module** - Better routing features
|
||||
4. **Analytics Module** - Business requirements
|
||||
5. **Integrate form-autosave** - Code organization
|
||||
6. **Enhance SPA Router** - Better integration
|
||||
7. **Create Security Module** - Security best practices
|
||||
|
||||
### Low Priority (Nice to Have)
|
||||
|
||||
1. **Storage Manager Module** - Enhance existing
|
||||
2. **Animation System Module** - Refactoring existing
|
||||
3. **i18n Module** - Internationalization
|
||||
4. **Integrate Performance Profiler** - Developer experience
|
||||
5. **Modernize Image Manager** - Code quality
|
||||
|
||||
---
|
||||
|
||||
## Implementation Roadmap
|
||||
|
||||
### Phase 1: Foundation (Weeks 1-2)
|
||||
|
||||
1. **State Management Module** (3 days)
|
||||
2. **Validation Module** (3 days)
|
||||
3. **Error Tracking Module** (3 days)
|
||||
4. **Event Bus Module** (2 days)
|
||||
|
||||
**Total**: ~11 days
|
||||
|
||||
### Phase 2: Integration & Refactoring (Weeks 3-4)
|
||||
|
||||
1. **Integrate form-autosave** (1 day)
|
||||
2. **Enhance SPA Router** (3 days)
|
||||
3. **Create Security Module** (2 days)
|
||||
4. **Cache Manager Module** (3 days)
|
||||
|
||||
**Total**: ~9 days
|
||||
|
||||
### Phase 3: Advanced Features (Weeks 5-6)
|
||||
|
||||
1. **Router Enhancement Module** (3 days)
|
||||
2. **Analytics Module** (3 days)
|
||||
3. **Consolidate Scroll Animation Modules** (7 days)
|
||||
|
||||
**Total**: ~13 days
|
||||
|
||||
### Phase 4: Polish & Optimization (Weeks 7-8)
|
||||
|
||||
1. **Storage Manager Enhancement** (1 day)
|
||||
2. **Integrate Performance Profiler** (3 days)
|
||||
3. **Modernize Image Manager** (3 days)
|
||||
4. **Documentation updates** (2 days)
|
||||
|
||||
**Total**: ~9 days
|
||||
|
||||
---
|
||||
|
||||
## Module Architecture Principles
|
||||
|
||||
### 1. **Consistency**
|
||||
- All modules should follow the same structure
|
||||
- Consistent naming conventions
|
||||
- Consistent API patterns
|
||||
|
||||
### 2. **Modularity**
|
||||
- Modules should be independent
|
||||
- Clear dependencies
|
||||
- Easy to test in isolation
|
||||
|
||||
### 3. **Integration**
|
||||
- Modules should integrate well with LiveComponents
|
||||
- Shared configuration
|
||||
- Unified event system
|
||||
|
||||
### 4. **Performance**
|
||||
- Lazy loading where possible
|
||||
- Tree-shaking support
|
||||
- Minimal bundle size
|
||||
|
||||
### 5. **Developer Experience**
|
||||
- TypeScript definitions
|
||||
- Comprehensive documentation
|
||||
- Clear error messages
|
||||
- DevTools integration
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Review this analysis** with the team
|
||||
2. **Prioritize modules** based on project needs
|
||||
3. **Create detailed implementation plans** for high-priority modules
|
||||
4. **Start with Phase 1** (Foundation modules)
|
||||
5. **Iterate and refine** based on feedback
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2025-01-XX
|
||||
**Status**: Draft - Pending Review
|
||||
|
||||
@@ -1,893 +0,0 @@
|
||||
# LiveComponent File Uploads
|
||||
|
||||
Komplette Dokumentation des File Upload Systems für LiveComponents mit Drag & Drop, Multi-File Support und Progress Tracking.
|
||||
|
||||
## Übersicht
|
||||
|
||||
Das File Upload System ermöglicht es, Dateien direkt in LiveComponents hochzuladen mit:
|
||||
|
||||
- **Drag & Drop Support** - Intuitive Dateiauswahl durch Ziehen & Ablegen
|
||||
- **Multi-File Uploads** - Mehrere Dateien gleichzeitig hochladen
|
||||
- **Progress Tracking** - Echtzeit-Fortschrittsanzeige pro Datei und gesamt
|
||||
- **Preview Funktionalität** - Bild-Vorschauen und Datei-Icons
|
||||
- **Client-Side Validation** - Validierung vor dem Upload (MIME-Type, Größe, Extension)
|
||||
- **CSRF Protection** - Automatische CSRF-Token-Integration
|
||||
- **Component State Management** - Nahtlose Integration mit LiveComponent State
|
||||
|
||||
## Architektur
|
||||
|
||||
```
|
||||
┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐
|
||||
│ FileUploadWidget │───▶│ ComponentFileUploader│───▶│ Backend Route │
|
||||
│ (UI Component) │ │ (Upload Manager) │ │ /upload endpoint │
|
||||
└─────────────────────┘ └─────────────────────┘ └─────────────────────┘
|
||||
│ │ │
|
||||
Drop Zone UI Multi-File Queue State Update
|
||||
File List Progress Tracking HTML Refresh
|
||||
Progress Bars CSRF Handling Event Dispatch
|
||||
```
|
||||
|
||||
### Komponenten
|
||||
|
||||
1. **Backend** (`src/Framework/LiveComponents/`)
|
||||
- `Controllers/LiveComponentController::handleUpload()` - Upload Route Handler
|
||||
- `LiveComponentHandler::handleUpload()` - Business Logic
|
||||
- `Contracts/SupportsFileUpload` - Interface für uploadbare Components
|
||||
- `ValueObjects/FileUploadProgress` - Progress Tracking VO
|
||||
- `ValueObjects/UploadedComponentFile` - File Metadata VO
|
||||
|
||||
2. **Frontend** (`resources/js/modules/livecomponent/`)
|
||||
- `ComponentFileUploader.js` - Core Upload Manager
|
||||
- `FileUploadWidget.js` - Pre-built UI Component
|
||||
- `DragDropZone` - Drag & Drop Handler
|
||||
- `FileValidator` - Client-Side Validation
|
||||
- `UploadProgress` - Progress Tracking
|
||||
|
||||
3. **Styling** (`resources/css/components/`)
|
||||
- `file-upload-widget.css` - Complete UI Styling
|
||||
|
||||
## Backend Implementation
|
||||
|
||||
### Interface: SupportsFileUpload
|
||||
|
||||
LiveComponents, die Uploads unterstützen, müssen das `SupportsFileUpload` Interface implementieren:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
use App\Framework\LiveComponents\Contracts\SupportsFileUpload;
|
||||
use App\Framework\Http\UploadedFile;
|
||||
use App\Framework\LiveComponents\ComponentEventDispatcher;
|
||||
use App\Framework\LiveComponents\ValueObjects\ComponentData;
|
||||
|
||||
final class DocumentUploadComponent extends AbstractLiveComponent implements SupportsFileUpload
|
||||
{
|
||||
private array $uploadedFiles = [];
|
||||
|
||||
/**
|
||||
* Handle file upload
|
||||
*/
|
||||
public function handleUpload(UploadedFile $file, ?ComponentEventDispatcher $events = null): ComponentData
|
||||
{
|
||||
// 1. Validate file (already done by framework, but you can add custom validation)
|
||||
if (!$this->isValidDocument($file)) {
|
||||
throw new \InvalidArgumentException('Invalid document type');
|
||||
}
|
||||
|
||||
// 2. Process uploaded file
|
||||
$savedPath = $this->saveFile($file);
|
||||
|
||||
// 3. Update component state
|
||||
$this->uploadedFiles[] = [
|
||||
'name' => $file->getClientFilename(),
|
||||
'path' => $savedPath,
|
||||
'size' => $file->getSize(),
|
||||
'uploaded_at' => time()
|
||||
];
|
||||
|
||||
// 4. Dispatch events if needed
|
||||
if ($events) {
|
||||
$events->dispatch('file-uploaded', [
|
||||
'filename' => $file->getClientFilename(),
|
||||
'path' => $savedPath
|
||||
]);
|
||||
}
|
||||
|
||||
// 5. Return updated component data
|
||||
return ComponentData::fromArray([
|
||||
'uploaded_files' => $this->uploadedFiles
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate uploaded file
|
||||
*/
|
||||
public function validateUpload(UploadedFile $file): array
|
||||
{
|
||||
$errors = [];
|
||||
|
||||
// File type validation
|
||||
$allowedTypes = $this->getAllowedMimeTypes();
|
||||
if (!empty($allowedTypes) && !in_array($file->getClientMediaType(), $allowedTypes)) {
|
||||
$errors[] = "File type {$file->getClientMediaType()} is not allowed";
|
||||
}
|
||||
|
||||
// File size validation
|
||||
$maxSize = $this->getMaxFileSize();
|
||||
if ($file->getSize() > $maxSize) {
|
||||
$errors[] = "File size exceeds maximum allowed size";
|
||||
}
|
||||
|
||||
// Custom validation
|
||||
if (!$this->isValidDocument($file)) {
|
||||
$errors[] = "Invalid document format";
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get allowed MIME types
|
||||
*/
|
||||
public function getAllowedMimeTypes(): array
|
||||
{
|
||||
return [
|
||||
'application/pdf',
|
||||
'application/msword',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'image/jpeg',
|
||||
'image/png'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get maximum file size in bytes
|
||||
*/
|
||||
public function getMaxFileSize(): int
|
||||
{
|
||||
return 10 * 1024 * 1024; // 10MB
|
||||
}
|
||||
|
||||
private function isValidDocument(UploadedFile $file): bool
|
||||
{
|
||||
// Custom validation logic
|
||||
return true;
|
||||
}
|
||||
|
||||
private function saveFile(UploadedFile $file): string
|
||||
{
|
||||
// Save file to storage
|
||||
$uploadDir = '/var/www/storage/uploads';
|
||||
$filename = uniqid() . '_' . $file->getClientFilename();
|
||||
$file->moveTo($uploadDir . '/' . $filename);
|
||||
return $filename;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Upload Endpoint
|
||||
|
||||
**Route**: `POST /live-component/{id}/upload`
|
||||
|
||||
**Request Format**:
|
||||
```
|
||||
Content-Type: multipart/form-data
|
||||
|
||||
file: <binary file data>
|
||||
state: <JSON string of current component state>
|
||||
params: <JSON string of additional parameters>
|
||||
_csrf_token: <CSRF token>
|
||||
```
|
||||
|
||||
**Response Format**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"html": "<updated component HTML>",
|
||||
"state": {
|
||||
"id": "document-upload:abc123",
|
||||
"component": "DocumentUploadComponent",
|
||||
"data": {
|
||||
"uploaded_files": [...]
|
||||
}
|
||||
},
|
||||
"events": [...],
|
||||
"file": {
|
||||
"name": "document.pdf",
|
||||
"size": 1048576,
|
||||
"type": "application/pdf"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Error Response**:
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "File validation failed",
|
||||
"errors": [
|
||||
"File type application/octet-stream is not allowed"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Frontend Implementation
|
||||
|
||||
### Quick Start: FileUploadWidget
|
||||
|
||||
Der einfachste Weg, File Uploads zu implementieren, ist die Verwendung des `FileUploadWidget`:
|
||||
|
||||
```html
|
||||
<!-- In your LiveComponent template -->
|
||||
<div data-live-component="document-upload:abc123">
|
||||
<!-- Upload Widget Container -->
|
||||
<div id="upload-widget"></div>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import { FileUploadWidget } from '/resources/js/modules/livecomponent/FileUploadWidget.js';
|
||||
|
||||
// Initialize widget
|
||||
const widget = new FileUploadWidget(
|
||||
document.getElementById('upload-widget'),
|
||||
{
|
||||
maxFileSize: 10 * 1024 * 1024, // 10MB
|
||||
allowedMimeTypes: ['application/pdf', 'image/jpeg', 'image/png'],
|
||||
allowedExtensions: ['pdf', 'jpg', 'jpeg', 'png'],
|
||||
maxFiles: 5,
|
||||
multiple: true,
|
||||
autoUpload: true,
|
||||
showPreviews: true,
|
||||
showProgress: true
|
||||
}
|
||||
);
|
||||
</script>
|
||||
```
|
||||
|
||||
### Advanced: ComponentFileUploader
|
||||
|
||||
Für vollständige Kontrolle verwenden Sie direkt `ComponentFileUploader`:
|
||||
|
||||
```javascript
|
||||
import { ComponentFileUploader } from '/resources/js/modules/livecomponent/ComponentFileUploader.js';
|
||||
|
||||
const componentElement = document.querySelector('[data-live-id="document-upload:abc123"]');
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const fileInput = document.getElementById('file-input');
|
||||
|
||||
const uploader = new ComponentFileUploader(componentElement, {
|
||||
// Configuration
|
||||
maxFileSize: 10 * 1024 * 1024,
|
||||
allowedMimeTypes: ['application/pdf', 'image/jpeg'],
|
||||
maxFiles: 10,
|
||||
autoUpload: true,
|
||||
multiple: true,
|
||||
maxConcurrentUploads: 2,
|
||||
|
||||
// UI Elements
|
||||
dropZone: dropZone,
|
||||
fileInput: fileInput,
|
||||
|
||||
// Callbacks
|
||||
onFileAdded: ({ fileId, file, progress }) => {
|
||||
console.log('File added:', file.name);
|
||||
// Update UI
|
||||
},
|
||||
|
||||
onUploadStart: ({ fileId, file }) => {
|
||||
console.log('Upload started:', file.name);
|
||||
},
|
||||
|
||||
onUploadProgress: ({ fileId, percentage, uploadSpeed, remainingTime }) => {
|
||||
console.log(`Upload progress: ${percentage}%`);
|
||||
// Update progress bar
|
||||
},
|
||||
|
||||
onUploadComplete: ({ fileId, file, response }) => {
|
||||
console.log('Upload complete:', file.name);
|
||||
// Handle success
|
||||
},
|
||||
|
||||
onUploadError: ({ fileId, file, error }) => {
|
||||
console.error('Upload failed:', error);
|
||||
// Handle error
|
||||
},
|
||||
|
||||
onAllUploadsComplete: ({ totalFiles, successCount, errorCount }) => {
|
||||
console.log(`All uploads complete: ${successCount}/${totalFiles} succeeded`);
|
||||
}
|
||||
});
|
||||
|
||||
// Programmatically add files
|
||||
uploader.addFiles([file1, file2, file3]);
|
||||
|
||||
// Start uploads (if autoUpload is false)
|
||||
uploader.uploadAll();
|
||||
|
||||
// Cancel all uploads
|
||||
uploader.cancelAll();
|
||||
|
||||
// Get statistics
|
||||
const stats = uploader.getStats();
|
||||
console.log(`Progress: ${stats.overallProgress}%`);
|
||||
```
|
||||
|
||||
### Custom Drag & Drop
|
||||
|
||||
```javascript
|
||||
import { DragDropZone } from '/resources/js/modules/livecomponent/ComponentFileUploader.js';
|
||||
|
||||
const dropZone = new DragDropZone(document.getElementById('drop-area'), {
|
||||
onFilesDropped: (files) => {
|
||||
console.log('Files dropped:', files);
|
||||
uploader.addFiles(files);
|
||||
},
|
||||
onDragEnter: () => {
|
||||
console.log('Drag enter');
|
||||
},
|
||||
onDragLeave: () => {
|
||||
console.log('Drag leave');
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Client-Side Validation
|
||||
|
||||
```javascript
|
||||
import { FileValidator } from '/resources/js/modules/livecomponent/ComponentFileUploader.js';
|
||||
|
||||
const validator = new FileValidator({
|
||||
maxFileSize: 5 * 1024 * 1024, // 5MB
|
||||
allowedMimeTypes: ['image/jpeg', 'image/png'],
|
||||
allowedExtensions: ['jpg', 'jpeg', 'png'],
|
||||
minFileSize: 1024 // 1KB minimum
|
||||
});
|
||||
|
||||
// Validate single file
|
||||
const errors = validator.validate(file);
|
||||
if (errors.length > 0) {
|
||||
console.error('Validation errors:', errors);
|
||||
}
|
||||
|
||||
// Quick validation
|
||||
if (!validator.isValid(file)) {
|
||||
console.error('File is not valid');
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### ComponentFileUploader Options
|
||||
|
||||
```javascript
|
||||
{
|
||||
// File Constraints
|
||||
maxFileSize: 10 * 1024 * 1024, // Maximum file size in bytes (default: 10MB)
|
||||
allowedMimeTypes: [], // Array of allowed MIME types (empty = allow all)
|
||||
allowedExtensions: [], // Array of allowed file extensions (empty = allow all)
|
||||
maxFiles: 10, // Maximum number of files (default: 10)
|
||||
|
||||
// Upload Behavior
|
||||
autoUpload: true, // Auto-upload on file add (default: true)
|
||||
multiple: true, // Allow multiple files (default: true)
|
||||
maxConcurrentUploads: 2, // Max concurrent uploads (default: 2)
|
||||
|
||||
// Endpoints
|
||||
endpoint: '/live-component/{id}/upload', // Upload endpoint (default: auto-detected)
|
||||
|
||||
// UI Elements (optional)
|
||||
dropZone: HTMLElement, // Drop zone element
|
||||
fileInput: HTMLElement, // File input element
|
||||
|
||||
// Callbacks
|
||||
onFileAdded: (data) => {}, // Called when file is added
|
||||
onFileRemoved: (data) => {}, // Called when file is removed
|
||||
onUploadStart: (data) => {}, // Called when upload starts
|
||||
onUploadProgress: (data) => {}, // Called during upload
|
||||
onUploadComplete: (data) => {}, // Called on upload success
|
||||
onUploadError: (data) => {}, // Called on upload error
|
||||
onAllUploadsComplete: (data) => {} // Called when all uploads are done
|
||||
}
|
||||
```
|
||||
|
||||
### FileUploadWidget Options
|
||||
|
||||
```javascript
|
||||
{
|
||||
// Inherits all ComponentFileUploader options, plus:
|
||||
|
||||
// UI Configuration
|
||||
showPreviews: true, // Show image previews (default: true)
|
||||
showProgress: true, // Show progress bars (default: true)
|
||||
showFileList: true, // Show file list (default: true)
|
||||
|
||||
// Text Configuration
|
||||
dropZoneText: 'Drag & drop files here or click to browse',
|
||||
browseButtonText: 'Browse Files',
|
||||
uploadButtonText: 'Upload All'
|
||||
}
|
||||
```
|
||||
|
||||
## Callback Data Structures
|
||||
|
||||
### onFileAdded
|
||||
|
||||
```javascript
|
||||
{
|
||||
fileId: 'unique-file-id',
|
||||
file: File, // Native File object
|
||||
progress: {
|
||||
fileId: 'unique-file-id',
|
||||
fileName: 'document.pdf',
|
||||
fileSize: 1048576,
|
||||
uploadedBytes: 0,
|
||||
percentage: 0,
|
||||
status: 'pending',
|
||||
error: null,
|
||||
uploadSpeed: 0,
|
||||
remainingTime: 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### onUploadProgress
|
||||
|
||||
```javascript
|
||||
{
|
||||
fileId: 'unique-file-id',
|
||||
fileName: 'document.pdf',
|
||||
fileSize: 1048576,
|
||||
uploadedBytes: 524288, // Bytes uploaded so far
|
||||
percentage: 50, // Upload percentage (0-100)
|
||||
status: 'uploading',
|
||||
uploadSpeed: 1048576, // Bytes per second
|
||||
remainingTime: 0.5 // Seconds remaining
|
||||
}
|
||||
```
|
||||
|
||||
### onUploadComplete
|
||||
|
||||
```javascript
|
||||
{
|
||||
fileId: 'unique-file-id',
|
||||
file: File,
|
||||
response: {
|
||||
success: true,
|
||||
html: '<updated component HTML>',
|
||||
state: {...},
|
||||
events: [...],
|
||||
file: {
|
||||
name: 'document.pdf',
|
||||
size: 1048576,
|
||||
type: 'application/pdf'
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### onUploadError
|
||||
|
||||
```javascript
|
||||
{
|
||||
fileId: 'unique-file-id',
|
||||
file: File,
|
||||
error: 'File validation failed'
|
||||
}
|
||||
```
|
||||
|
||||
## Use Cases & Examples
|
||||
|
||||
### Basic Image Upload
|
||||
|
||||
```php
|
||||
final class ProfileImageUpload extends AbstractLiveComponent implements SupportsFileUpload
|
||||
{
|
||||
private ?string $profileImage = null;
|
||||
|
||||
public function handleUpload(UploadedFile $file, ?ComponentEventDispatcher $events = null): ComponentData
|
||||
{
|
||||
// Save image
|
||||
$filename = $this->imageService->save($file, 'profiles');
|
||||
|
||||
// Update state
|
||||
$this->profileImage = $filename;
|
||||
|
||||
return ComponentData::fromArray([
|
||||
'profile_image' => $this->profileImage
|
||||
]);
|
||||
}
|
||||
|
||||
public function validateUpload(UploadedFile $file): array
|
||||
{
|
||||
$errors = [];
|
||||
|
||||
// Only allow images
|
||||
if (!str_starts_with($file->getClientMediaType(), 'image/')) {
|
||||
$errors[] = 'Only images are allowed';
|
||||
}
|
||||
|
||||
// Max 2MB
|
||||
if ($file->getSize() > 2 * 1024 * 1024) {
|
||||
$errors[] = 'Image must be smaller than 2MB';
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
public function getAllowedMimeTypes(): array
|
||||
{
|
||||
return ['image/jpeg', 'image/png', 'image/webp'];
|
||||
}
|
||||
|
||||
public function getMaxFileSize(): int
|
||||
{
|
||||
return 2 * 1024 * 1024;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Multi-Document Upload with Progress
|
||||
|
||||
```javascript
|
||||
import { ComponentFileUploader } from './ComponentFileUploader.js';
|
||||
|
||||
const uploader = new ComponentFileUploader(componentElement, {
|
||||
maxFiles: 20,
|
||||
maxFileSize: 50 * 1024 * 1024, // 50MB
|
||||
allowedMimeTypes: [
|
||||
'application/pdf',
|
||||
'application/msword',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
||||
],
|
||||
|
||||
onUploadProgress: ({ fileId, percentage, uploadSpeed, remainingTime }) => {
|
||||
// Update progress UI
|
||||
const progressBar = document.querySelector(`[data-file-id="${fileId}"] .progress-bar`);
|
||||
progressBar.style.width = `${percentage}%`;
|
||||
|
||||
const progressText = document.querySelector(`[data-file-id="${fileId}"] .progress-text`);
|
||||
progressText.textContent = `${percentage}% - ${formatSpeed(uploadSpeed)} - ${formatTime(remainingTime)} remaining`;
|
||||
},
|
||||
|
||||
onAllUploadsComplete: ({ successCount, errorCount }) => {
|
||||
if (errorCount === 0) {
|
||||
alert(`All ${successCount} files uploaded successfully!`);
|
||||
} else {
|
||||
alert(`${successCount} files uploaded, ${errorCount} failed`);
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Custom Validation Messages
|
||||
|
||||
```javascript
|
||||
const validator = new FileValidator({
|
||||
maxFileSize: 10 * 1024 * 1024,
|
||||
allowedMimeTypes: ['application/pdf']
|
||||
});
|
||||
|
||||
const errors = validator.validate(file);
|
||||
|
||||
// Translate errors
|
||||
const translatedErrors = errors.map(error => {
|
||||
if (error.includes('File size')) {
|
||||
return 'Dateigröße überschreitet das Maximum';
|
||||
} else if (error.includes('File type')) {
|
||||
return 'Nur PDF-Dateien sind erlaubt';
|
||||
}
|
||||
return error;
|
||||
});
|
||||
|
||||
if (translatedErrors.length > 0) {
|
||||
showErrorMessages(translatedErrors);
|
||||
}
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### CSRF Protection
|
||||
|
||||
Das System verwendet automatisch CSRF-Tokens:
|
||||
|
||||
```javascript
|
||||
// CSRF token wird automatisch aus Component-Element gelesen
|
||||
const csrfToken = componentElement.dataset.csrfToken;
|
||||
|
||||
// Und in jedem Upload-Request gesendet
|
||||
xhr.setRequestHeader('X-CSRF-Form-ID', csrfTokens.form_id);
|
||||
xhr.setRequestHeader('X-CSRF-Token', csrfTokens.token);
|
||||
```
|
||||
|
||||
### File Validation
|
||||
|
||||
**Backend Validation ist PFLICHT**:
|
||||
|
||||
```php
|
||||
public function validateUpload(UploadedFile $file): array
|
||||
{
|
||||
$errors = [];
|
||||
|
||||
// 1. MIME type validation
|
||||
$allowedTypes = $this->getAllowedMimeTypes();
|
||||
if (!in_array($file->getClientMediaType(), $allowedTypes)) {
|
||||
$errors[] = 'Invalid file type';
|
||||
}
|
||||
|
||||
// 2. File extension validation
|
||||
$extension = pathinfo($file->getClientFilename(), PATHINFO_EXTENSION);
|
||||
if (!in_array(strtolower($extension), ['pdf', 'jpg', 'png'])) {
|
||||
$errors[] = 'Invalid file extension';
|
||||
}
|
||||
|
||||
// 3. File size validation
|
||||
if ($file->getSize() > $this->getMaxFileSize()) {
|
||||
$errors[] = 'File too large';
|
||||
}
|
||||
|
||||
// 4. File content validation (check magic bytes)
|
||||
$finfo = new \finfo(FILEINFO_MIME_TYPE);
|
||||
$actualMimeType = $finfo->file($file->getStream()->getMetadata('uri'));
|
||||
if ($actualMimeType !== $file->getClientMediaType()) {
|
||||
$errors[] = 'File content does not match declared type';
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
```
|
||||
|
||||
### Secure File Storage
|
||||
|
||||
```php
|
||||
private function saveFile(UploadedFile $file): string
|
||||
{
|
||||
// 1. Generate secure filename (no user input)
|
||||
$filename = bin2hex(random_bytes(16)) . '.' . $this->getSecureExtension($file);
|
||||
|
||||
// 2. Store outside web root
|
||||
$uploadDir = '/var/www/storage/uploads';
|
||||
|
||||
// 3. Set restrictive permissions
|
||||
$file->moveTo($uploadDir . '/' . $filename);
|
||||
chmod($uploadDir . '/' . $filename, 0600);
|
||||
|
||||
return $filename;
|
||||
}
|
||||
|
||||
private function getSecureExtension(UploadedFile $file): string
|
||||
{
|
||||
// Use MIME type to determine extension, not user-provided extension
|
||||
return match ($file->getClientMediaType()) {
|
||||
'application/pdf' => 'pdf',
|
||||
'image/jpeg' => 'jpg',
|
||||
'image/png' => 'png',
|
||||
default => 'bin'
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Concurrent Uploads
|
||||
|
||||
```javascript
|
||||
const uploader = new ComponentFileUploader(componentElement, {
|
||||
maxConcurrentUploads: 3, // Upload 3 files simultaneously
|
||||
autoUpload: true
|
||||
});
|
||||
```
|
||||
|
||||
### Chunked Uploads (Large Files)
|
||||
|
||||
Für große Dateien sollten Chunked Uploads implementiert werden:
|
||||
|
||||
```javascript
|
||||
// TODO: Chunked upload support (geplant für v2.0)
|
||||
// Aktuell empfohlen: maxFileSize Limit für große Dateien
|
||||
```
|
||||
|
||||
### Progress Throttling
|
||||
|
||||
```javascript
|
||||
let lastProgressUpdate = 0;
|
||||
|
||||
onUploadProgress: ({ fileId, percentage }) => {
|
||||
const now = Date.now();
|
||||
|
||||
// Only update UI every 100ms
|
||||
if (now - lastProgressUpdate > 100) {
|
||||
updateProgressBar(fileId, percentage);
|
||||
lastProgressUpdate = now;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Problem: Upload schlägt fehl mit 413 (Request Entity Too Large)
|
||||
|
||||
**Lösung**: Erhöhe PHP Upload Limits:
|
||||
|
||||
```ini
|
||||
; php.ini
|
||||
upload_max_filesize = 50M
|
||||
post_max_size = 50M
|
||||
```
|
||||
|
||||
### Problem: CSRF Token fehlt
|
||||
|
||||
**Lösung**: Stelle sicher, dass Component CSRF Token hat:
|
||||
|
||||
```html
|
||||
<div data-live-component="upload:123" data-csrf-token="<?= $csrfToken ?>">
|
||||
...
|
||||
</div>
|
||||
```
|
||||
|
||||
### Problem: Uploads sind langsam
|
||||
|
||||
**Lösungen**:
|
||||
1. Erhöhe `maxConcurrentUploads`
|
||||
2. Implementiere Chunked Uploads
|
||||
3. Komprimiere Dateien vor Upload (z.B. Bilder)
|
||||
4. Verwende CDN für statische Assets
|
||||
|
||||
### Problem: Browser hängt bei vielen Dateien
|
||||
|
||||
**Lösung**: Limitiere `maxFiles` und zeige Queue-Status:
|
||||
|
||||
```javascript
|
||||
const uploader = new ComponentFileUploader(componentElement, {
|
||||
maxFiles: 20, // Limit gleichzeitig ausgewählte Dateien
|
||||
maxConcurrentUploads: 2 // Aber nur 2 gleichzeitig hochladen
|
||||
});
|
||||
```
|
||||
|
||||
### Problem: Drag & Drop funktioniert nicht
|
||||
|
||||
**Lösung**: Prüfe Event Listener Setup:
|
||||
|
||||
```javascript
|
||||
// Stelle sicher, dass Drop Zone Element existiert
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
if (!dropZone) {
|
||||
console.error('Drop zone element not found');
|
||||
}
|
||||
|
||||
// Prüfe CSS cursor
|
||||
dropZone.style.cursor = 'pointer';
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Immer Backend-Validierung
|
||||
|
||||
```php
|
||||
// ❌ Niemals nur Frontend-Validierung verlassen
|
||||
// ✅ Immer Backend validateUpload() implementieren
|
||||
```
|
||||
|
||||
### 2. Sichere Dateinamen
|
||||
|
||||
```php
|
||||
// ❌ User-provided filenames verwenden
|
||||
$file->moveTo('/uploads/' . $file->getClientFilename());
|
||||
|
||||
// ✅ Sichere, generierte Dateinamen
|
||||
$file->moveTo('/uploads/' . bin2hex(random_bytes(16)) . '.pdf');
|
||||
```
|
||||
|
||||
### 3. Progress Feedback
|
||||
|
||||
```javascript
|
||||
// ✅ Immer Progress anzeigen für bessere UX
|
||||
onUploadProgress: ({ percentage }) => {
|
||||
updateProgressBar(percentage);
|
||||
updateStatusText(`Uploading: ${percentage}%`);
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Error Handling
|
||||
|
||||
```javascript
|
||||
// ✅ User-freundliche Fehlermeldungen
|
||||
onUploadError: ({ error }) => {
|
||||
const userMessage = translateError(error);
|
||||
showNotification(userMessage, 'error');
|
||||
logError(error); // Log für Debugging
|
||||
}
|
||||
```
|
||||
|
||||
### 5. File Size Limits
|
||||
|
||||
```php
|
||||
// ✅ Realistische Limits setzen
|
||||
public function getMaxFileSize(): int
|
||||
{
|
||||
// 10MB für Dokumente
|
||||
return 10 * 1024 * 1024;
|
||||
|
||||
// 5MB für Bilder
|
||||
// return 5 * 1024 * 1024;
|
||||
}
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Unit Tests (Frontend)
|
||||
|
||||
```javascript
|
||||
import { FileValidator } from './ComponentFileUploader.js';
|
||||
|
||||
describe('FileValidator', () => {
|
||||
it('validates file size', () => {
|
||||
const validator = new FileValidator({
|
||||
maxFileSize: 1024 * 1024 // 1MB
|
||||
});
|
||||
|
||||
const file = new File(['x'.repeat(2 * 1024 * 1024)], 'large.pdf');
|
||||
const errors = validator.validate(file);
|
||||
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
expect(errors[0]).toContain('File size');
|
||||
});
|
||||
|
||||
it('validates MIME type', () => {
|
||||
const validator = new FileValidator({
|
||||
allowedMimeTypes: ['application/pdf']
|
||||
});
|
||||
|
||||
const file = new File(['test'], 'test.jpg', { type: 'image/jpeg' });
|
||||
const errors = validator.validate(file);
|
||||
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
expect(errors[0]).toContain('not allowed');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Integration Tests (Backend)
|
||||
|
||||
```php
|
||||
it('handles file upload successfully', function () {
|
||||
$component = new DocumentUploadComponent();
|
||||
|
||||
$file = createUploadedFile('test.pdf', 'application/pdf', 1024);
|
||||
|
||||
$result = $component->handleUpload($file);
|
||||
|
||||
expect($result->toArray())->toHaveKey('uploaded_files');
|
||||
});
|
||||
|
||||
it('validates file type', function () {
|
||||
$component = new DocumentUploadComponent();
|
||||
|
||||
$file = createUploadedFile('test.exe', 'application/octet-stream', 1024);
|
||||
|
||||
$errors = $component->validateUpload($file);
|
||||
|
||||
expect($errors)->not->toBeEmpty();
|
||||
});
|
||||
```
|
||||
|
||||
## Zusammenfassung
|
||||
|
||||
Das File Upload System bietet:
|
||||
|
||||
- ✅ **Einfache Integration** - FileUploadWidget für schnellen Start
|
||||
- ✅ **Flexible API** - ComponentFileUploader für vollständige Kontrolle
|
||||
- ✅ **Drag & Drop** - Intuitive Dateiauswahl
|
||||
- ✅ **Multi-File Support** - Mehrere Dateien gleichzeitig
|
||||
- ✅ **Progress Tracking** - Echtzeit-Fortschrittsanzeige
|
||||
- ✅ **Validation** - Client & Server-Side
|
||||
- ✅ **Security** - CSRF Protection, sichere Dateinamen
|
||||
- ✅ **Performance** - Concurrent Uploads, Queue Management
|
||||
- ✅ **Responsive Design** - Mobile-optimiert
|
||||
- ✅ **Dark Mode** - Automatische Theme-Unterstützung
|
||||
|
||||
**Framework Integration**:
|
||||
- Value Objects für Type Safety
|
||||
- Event System Integration
|
||||
- Component State Management
|
||||
- CSRF Token Handling
|
||||
- Automatic HTML Refresh
|
||||
@@ -1,458 +0,0 @@
|
||||
# LiveComponent FormBuilder Integration
|
||||
|
||||
Elegante Integration zwischen LiveComponent System und bestehendem FormBuilder.
|
||||
|
||||
## Architektur
|
||||
|
||||
```
|
||||
┌─────────────────────────┐
|
||||
│ MultiStepFormDefinition │ ← Value Object mit Steps
|
||||
└────────────┬────────────┘
|
||||
│
|
||||
├─► FormStepDefinition ← Step-Info + Fields
|
||||
│
|
||||
└─► FormFieldDefinition ← Field-Config (text, email, etc.)
|
||||
│
|
||||
├─► FieldType (enum)
|
||||
├─► FieldCondition (conditional rendering)
|
||||
└─► StepValidator (validation logic)
|
||||
|
||||
┌─────────────────────────┐
|
||||
│ MultiStepFormComponent │ ← Generic LiveComponent
|
||||
└────────────┬────────────┘
|
||||
│
|
||||
└─► LiveFormBuilder ← Erweitert bestehenden FormBuilder
|
||||
│
|
||||
└─► FormBuilder (bestehend, wiederverwendet!)
|
||||
```
|
||||
|
||||
## Verwendungsbeispiel
|
||||
|
||||
### 1. Form Definition erstellen (Deklarativ, Type-Safe)
|
||||
|
||||
```php
|
||||
use App\Framework\LiveComponents\FormBuilder\MultiStepFormDefinition;
|
||||
use App\Framework\LiveComponents\FormBuilder\FormStepDefinition;
|
||||
use App\Framework\LiveComponents\FormBuilder\FormFieldDefinition;
|
||||
use App\Framework\LiveComponents\FormBuilder\FieldCondition;
|
||||
|
||||
// Deklarative Form-Definition - kein Code-Duplikat!
|
||||
$userRegistrationForm = new MultiStepFormDefinition(
|
||||
steps: [
|
||||
// Step 1: Personal Information
|
||||
new FormStepDefinition(
|
||||
title: 'Persönliche Informationen',
|
||||
description: 'Bitte geben Sie Ihre persönlichen Daten ein',
|
||||
fields: [
|
||||
FormFieldDefinition::text(
|
||||
name: 'first_name',
|
||||
label: 'Vorname',
|
||||
required: true
|
||||
),
|
||||
FormFieldDefinition::text(
|
||||
name: 'last_name',
|
||||
label: 'Nachname',
|
||||
required: true
|
||||
),
|
||||
FormFieldDefinition::email(
|
||||
name: 'email',
|
||||
label: 'E-Mail Adresse',
|
||||
required: true
|
||||
)
|
||||
],
|
||||
validator: new PersonalInfoValidator()
|
||||
),
|
||||
|
||||
// Step 2: Account Type
|
||||
new FormStepDefinition(
|
||||
title: 'Konto-Typ',
|
||||
description: 'Wählen Sie Ihren Konto-Typ',
|
||||
fields: [
|
||||
FormFieldDefinition::radio(
|
||||
name: 'account_type',
|
||||
label: 'Account Type',
|
||||
options: [
|
||||
'personal' => 'Privatkonto',
|
||||
'business' => 'Geschäftskonto'
|
||||
],
|
||||
required: true
|
||||
),
|
||||
// Conditional Field - nur bei Business
|
||||
FormFieldDefinition::text(
|
||||
name: 'company_name',
|
||||
label: 'Firmenname',
|
||||
required: true
|
||||
)->showWhen(
|
||||
FieldCondition::equals('account_type', 'business')
|
||||
),
|
||||
FormFieldDefinition::text(
|
||||
name: 'vat_number',
|
||||
label: 'USt-IdNr.',
|
||||
placeholder: 'DE123456789'
|
||||
)->showWhen(
|
||||
FieldCondition::equals('account_type', 'business')
|
||||
)
|
||||
],
|
||||
validator: new AccountTypeValidator()
|
||||
),
|
||||
|
||||
// Step 3: Preferences
|
||||
new FormStepDefinition(
|
||||
title: 'Präferenzen',
|
||||
description: 'Passen Sie Ihre Einstellungen an',
|
||||
fields: [
|
||||
FormFieldDefinition::checkbox(
|
||||
name: 'newsletter',
|
||||
label: 'Newsletter abonnieren'
|
||||
),
|
||||
FormFieldDefinition::select(
|
||||
name: 'language',
|
||||
label: 'Bevorzugte Sprache',
|
||||
options: [
|
||||
'en' => 'English',
|
||||
'de' => 'Deutsch',
|
||||
'fr' => 'Français'
|
||||
],
|
||||
defaultValue: 'de'
|
||||
)
|
||||
]
|
||||
)
|
||||
],
|
||||
submitHandler: new UserRegistrationSubmitHandler()
|
||||
);
|
||||
```
|
||||
|
||||
### 2. Validator implementieren
|
||||
|
||||
```php
|
||||
use App\Framework\LiveComponents\FormBuilder\StepValidator;
|
||||
|
||||
final readonly class PersonalInfoValidator implements StepValidator
|
||||
{
|
||||
public function validate(array $formData): array
|
||||
{
|
||||
$errors = [];
|
||||
|
||||
if (empty($formData['first_name'] ?? '')) {
|
||||
$errors['first_name'] = 'Vorname ist erforderlich';
|
||||
}
|
||||
|
||||
if (empty($formData['last_name'] ?? '')) {
|
||||
$errors['last_name'] = 'Nachname ist erforderlich';
|
||||
}
|
||||
|
||||
if (empty($formData['email'] ?? '')) {
|
||||
$errors['email'] = 'E-Mail ist erforderlich';
|
||||
} elseif (!filter_var($formData['email'], FILTER_VALIDATE_EMAIL)) {
|
||||
$errors['email'] = 'Ungültige E-Mail Adresse';
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
}
|
||||
|
||||
final readonly class AccountTypeValidator implements StepValidator
|
||||
{
|
||||
public function validate(array $formData): array
|
||||
{
|
||||
$errors = [];
|
||||
|
||||
if (empty($formData['account_type'] ?? '')) {
|
||||
$errors['account_type'] = 'Bitte wählen Sie einen Konto-Typ';
|
||||
}
|
||||
|
||||
// Conditional validation für Business
|
||||
if (($formData['account_type'] ?? '') === 'business') {
|
||||
if (empty($formData['company_name'] ?? '')) {
|
||||
$errors['company_name'] = 'Firmenname ist erforderlich';
|
||||
}
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Submit Handler implementieren
|
||||
|
||||
```php
|
||||
use App\Framework\LiveComponents\FormBuilder\FormSubmitHandler;
|
||||
use App\Framework\LiveComponents\FormBuilder\SubmitResult;
|
||||
|
||||
final readonly class UserRegistrationSubmitHandler implements FormSubmitHandler
|
||||
{
|
||||
public function __construct(
|
||||
private UserService $userService
|
||||
) {}
|
||||
|
||||
public function handle(array $formData): SubmitResult
|
||||
{
|
||||
try {
|
||||
$user = $this->userService->registerUser(
|
||||
firstName: $formData['first_name'],
|
||||
lastName: $formData['last_name'],
|
||||
email: $formData['email'],
|
||||
accountType: $formData['account_type'],
|
||||
companyName: $formData['company_name'] ?? null,
|
||||
newsletter: ($formData['newsletter'] ?? 'no') === 'yes',
|
||||
language: $formData['language'] ?? 'de'
|
||||
);
|
||||
|
||||
return SubmitResult::success(
|
||||
message: 'Registrierung erfolgreich!',
|
||||
redirectUrl: '/dashboard',
|
||||
data: ['user_id' => $user->id]
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
return SubmitResult::failure(
|
||||
message: 'Registrierung fehlgeschlagen: ' . $e->getMessage()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Controller Setup
|
||||
|
||||
```php
|
||||
use App\Framework\Http\Attributes\Route;
|
||||
use App\Framework\Http\Method;
|
||||
use App\Framework\Http\ViewResult;
|
||||
use App\Framework\LiveComponents\FormBuilder\MultiStepFormComponent;
|
||||
use App\Framework\LiveComponents\ValueObjects\ComponentId;
|
||||
|
||||
final readonly class UserRegistrationController
|
||||
{
|
||||
#[Route('/register', method: Method::GET)]
|
||||
public function showRegistrationForm(): ViewResult
|
||||
{
|
||||
// Form Definition (könnte auch aus Container kommen)
|
||||
$formDefinition = $this->createUserRegistrationForm();
|
||||
|
||||
// Component erstellen
|
||||
$component = new MultiStepFormComponent(
|
||||
id: ComponentId::generate('user-registration'),
|
||||
formDefinition: $formDefinition
|
||||
);
|
||||
|
||||
return new ViewResult(
|
||||
template: 'pages/register',
|
||||
data: [
|
||||
'registration_form' => $component
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
private function createUserRegistrationForm(): MultiStepFormDefinition
|
||||
{
|
||||
return new MultiStepFormDefinition(
|
||||
steps: [
|
||||
// ... (wie oben)
|
||||
],
|
||||
submitHandler: new UserRegistrationSubmitHandler($this->userService)
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Template Usage
|
||||
|
||||
```html
|
||||
<!-- pages/register.view.php -->
|
||||
<div class="registration-page">
|
||||
<h1>Benutzerregistrierung</h1>
|
||||
|
||||
<!-- LiveComponent einbinden -->
|
||||
<livecomponent name="multi-step-form" data="registration_form" />
|
||||
</div>
|
||||
```
|
||||
|
||||
## Vorteile dieser Lösung
|
||||
|
||||
### ✅ Kein Code-Duplikat
|
||||
- Nutzt bestehenden `FormBuilder` aus View-Modul
|
||||
- `LiveFormBuilder` erweitert nur mit LiveComponent-Features
|
||||
- Keine doppelte Field-Rendering-Logik
|
||||
|
||||
### ✅ Type-Safe & Framework-Compliant
|
||||
- Alle Value Objects: `FormFieldDefinition`, `FormStepDefinition`, `MultiStepFormDefinition`
|
||||
- Readonly Classes überall
|
||||
- Enums für `FieldType`
|
||||
|
||||
### ✅ Deklarativ statt Imperativ
|
||||
- Form-Definition rein deklarativ (kein Code für Rendering)
|
||||
- Klare Trennung: Definition vs. Rendering vs. Validation vs. Submission
|
||||
|
||||
### ✅ Conditional Fields eingebaut
|
||||
```php
|
||||
FormFieldDefinition::text('company_name', 'Firma', required: true)
|
||||
->showWhen(FieldCondition::equals('account_type', 'business'))
|
||||
```
|
||||
|
||||
### ✅ Wiederverwendbare Validators
|
||||
```php
|
||||
final readonly class EmailValidator implements StepValidator
|
||||
{
|
||||
public function validate(array $formData): array
|
||||
{
|
||||
// Wiederverwendbare Validation-Logic
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ✅ Testbar
|
||||
```php
|
||||
// Unit Test für Validator
|
||||
it('validates email format', function () {
|
||||
$validator = new PersonalInfoValidator();
|
||||
|
||||
$errors = $validator->validate(['email' => 'invalid']);
|
||||
|
||||
expect($errors)->toHaveKey('email');
|
||||
});
|
||||
|
||||
// Integration Test für Component
|
||||
it('moves to next step after validation', function () {
|
||||
$component = new MultiStepFormComponent(
|
||||
id: ComponentId::generate('test'),
|
||||
formDefinition: $this->testFormDef
|
||||
);
|
||||
|
||||
$result = $component->nextStep([
|
||||
'first_name' => 'John',
|
||||
'last_name' => 'Doe',
|
||||
'email' => 'john@example.com'
|
||||
]);
|
||||
|
||||
expect($result->get('current_step'))->toBe(2);
|
||||
});
|
||||
```
|
||||
|
||||
## Erweiterungsmöglichkeiten
|
||||
|
||||
### Custom Field Types
|
||||
|
||||
```php
|
||||
// Neuen FieldType hinzufügen
|
||||
enum FieldType: string
|
||||
{
|
||||
case TEXT = 'text';
|
||||
case EMAIL = 'email';
|
||||
// ... existing types
|
||||
case DATE = 'date';
|
||||
case PHONE = 'phone';
|
||||
case CURRENCY = 'currency';
|
||||
}
|
||||
|
||||
// LiveFormBuilder erweitern
|
||||
final readonly class LiveFormBuilder
|
||||
{
|
||||
public function addLiveDateInput(
|
||||
string $name,
|
||||
string $label,
|
||||
?string $value = null
|
||||
): self {
|
||||
// Implementation
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Multi-Field Conditions
|
||||
|
||||
```php
|
||||
final readonly class AndCondition implements FieldConditionContract
|
||||
{
|
||||
public function __construct(
|
||||
private FieldCondition $left,
|
||||
private FieldCondition $right
|
||||
) {}
|
||||
|
||||
public function matches(array $formData): bool
|
||||
{
|
||||
return $this->left->matches($formData)
|
||||
&& $this->right->matches($formData);
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
FormFieldDefinition::text('special_field', 'Special')
|
||||
->showWhen(
|
||||
new AndCondition(
|
||||
FieldCondition::equals('account_type', 'business'),
|
||||
FieldCondition::equals('country', 'DE')
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
### Custom Renderers
|
||||
|
||||
```php
|
||||
interface FieldRenderer
|
||||
{
|
||||
public function render(FormFieldDefinition $field, mixed $value): string;
|
||||
}
|
||||
|
||||
final readonly class CustomTextRenderer implements FieldRenderer
|
||||
{
|
||||
public function render(FormFieldDefinition $field, mixed $value): string
|
||||
{
|
||||
// Custom rendering logic
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Vergleich: Vorher vs. Nachher
|
||||
|
||||
### ❌ Vorher (Code-Duplikat)
|
||||
|
||||
```php
|
||||
// Hardcoded Form in Component
|
||||
public function getRenderData(): ComponentRenderData
|
||||
{
|
||||
return new ComponentRenderData(
|
||||
templatePath: 'livecomponent-dynamic-form',
|
||||
data: [
|
||||
'form_first_name' => $formData['first_name'] ?? '',
|
||||
'form_last_name' => $formData['last_name'] ?? '',
|
||||
// ... 50 weitere Zeilen hardcoded mappings
|
||||
]
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### ✅ Nachher (Wiederverwendbar)
|
||||
|
||||
```php
|
||||
// Deklarative Definition
|
||||
$formDef = new MultiStepFormDefinition(
|
||||
steps: [
|
||||
new FormStepDefinition(
|
||||
title: 'Personal Info',
|
||||
fields: [
|
||||
FormFieldDefinition::text('first_name', 'First Name'),
|
||||
FormFieldDefinition::text('last_name', 'Last Name')
|
||||
]
|
||||
)
|
||||
]
|
||||
);
|
||||
|
||||
// Generic Component - keine Anpassungen nötig!
|
||||
$component = new MultiStepFormComponent(
|
||||
id: ComponentId::generate('my-form'),
|
||||
formDefinition: $formDef
|
||||
);
|
||||
```
|
||||
|
||||
## Zusammenfassung
|
||||
|
||||
Diese Integration:
|
||||
- ✅ Nutzt bestehenden `FormBuilder` (keine Code-Duplizierung)
|
||||
- ✅ Erweitert ihn minimal für LiveComponent-Features
|
||||
- ✅ Vollständig type-safe mit Value Objects
|
||||
- ✅ Framework-compliant (readonly, final, composition)
|
||||
- ✅ Deklarative Form-Definitionen
|
||||
- ✅ Conditional Fields eingebaut
|
||||
- ✅ Wiederverwendbare Validators
|
||||
- ✅ Generische Component (kein Custom-Code pro Form)
|
||||
- ✅ Einfach testbar
|
||||
- ✅ Leicht erweiterbar
|
||||
@@ -1,681 +0,0 @@
|
||||
# LiveComponent Lazy Loading
|
||||
|
||||
**Performance-Optimization durch Viewport-basiertes Component Loading**
|
||||
|
||||
## Übersicht
|
||||
|
||||
Das Lazy Loading System lädt LiveComponents erst, wenn sie im Browser-Viewport sichtbar werden. Dies reduziert die initiale Ladezeit erheblich, besonders bei Seiten mit vielen Components.
|
||||
|
||||
**Key Features:**
|
||||
- ⚡ **Viewport-Detection** - Intersection Observer API
|
||||
- 🎯 **Priority-Based Loading** - High/Normal/Low Prioritäten
|
||||
- 🔄 **Progressive Loading** - Sequentielle Queue-Verarbeitung
|
||||
- 📊 **Loading States** - Placeholder → Loading → Loaded
|
||||
- 🧹 **Automatic Cleanup** - Memory-efficient observer management
|
||||
|
||||
## Performance Benefits
|
||||
|
||||
| Metric | Without Lazy Loading | With Lazy Loading | Improvement |
|
||||
|--------|---------------------|-------------------|-------------|
|
||||
| Initial Page Load | 2500ms | 800ms | **68% faster** |
|
||||
| Time to Interactive | 3200ms | 1100ms | **66% faster** |
|
||||
| Initial JavaScript | 450KB | 120KB | **73% smaller** |
|
||||
| Components Loaded | All (20) | Visible (3-5) | **75% fewer** |
|
||||
| Memory Usage | 120MB | 35MB | **71% less** |
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Basic Lazy Component
|
||||
|
||||
```html
|
||||
<!-- Regular Component (loads immediately) -->
|
||||
<div data-live-component="notification-center:user-123"
|
||||
data-state='{"notifications": []}'
|
||||
data-csrf-token="...">
|
||||
<!-- Component HTML -->
|
||||
</div>
|
||||
|
||||
<!-- Lazy Component (loads on viewport entry) -->
|
||||
<div data-live-component-lazy="notification-center:user-123"
|
||||
data-lazy-threshold="0.1"
|
||||
data-lazy-priority="normal"
|
||||
data-lazy-placeholder="Loading notifications...">
|
||||
</div>
|
||||
```
|
||||
|
||||
**Key Differences:**
|
||||
- Use `data-live-component-lazy` instead of `data-live-component`
|
||||
- No initial HTML needed (placeholder will be shown)
|
||||
- No `data-state` needed initially (server will provide it)
|
||||
|
||||
### 2. Priority Levels
|
||||
|
||||
```html
|
||||
<!-- High Priority - Loads first -->
|
||||
<div data-live-component-lazy="user-stats:123"
|
||||
data-lazy-priority="high"
|
||||
data-lazy-placeholder="Loading user stats...">
|
||||
</div>
|
||||
|
||||
<!-- Normal Priority - Standard loading (default) -->
|
||||
<div data-live-component-lazy="activity-feed:user-123"
|
||||
data-lazy-priority="normal"
|
||||
data-lazy-placeholder="Loading activities...">
|
||||
</div>
|
||||
|
||||
<!-- Low Priority - Loads last -->
|
||||
<div data-live-component-lazy="recommendations:user-123"
|
||||
data-lazy-priority="low"
|
||||
data-lazy-placeholder="Loading recommendations...">
|
||||
</div>
|
||||
```
|
||||
|
||||
**Priority Weights:**
|
||||
- `high`: 3 (Critical components, always load first)
|
||||
- `normal`: 2 (Standard components, default)
|
||||
- `low`: 1 (Non-critical components, load last)
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### Viewport Threshold
|
||||
|
||||
Controls when loading triggers (0.0 = top edge, 1.0 = fully visible):
|
||||
|
||||
```html
|
||||
<!-- Load when 10% visible (default) -->
|
||||
<div data-live-component-lazy="counter:demo"
|
||||
data-lazy-threshold="0.1">
|
||||
</div>
|
||||
|
||||
<!-- Load when 50% visible -->
|
||||
<div data-live-component-lazy="chart:demo"
|
||||
data-lazy-threshold="0.5">
|
||||
</div>
|
||||
|
||||
<!-- Load when fully visible -->
|
||||
<div data-live-component-lazy="video-player:demo"
|
||||
data-lazy-threshold="1.0">
|
||||
</div>
|
||||
```
|
||||
|
||||
### Root Margin (Pre-loading)
|
||||
|
||||
Global configuration in LazyComponentLoader:
|
||||
|
||||
```javascript
|
||||
this.defaultOptions = {
|
||||
rootMargin: '50px', // Load 50px before entering viewport
|
||||
threshold: 0.1 // Default threshold
|
||||
};
|
||||
```
|
||||
|
||||
**Effect:** Components start loading before user scrolls to them, creating seamless experience.
|
||||
|
||||
### Custom Placeholder
|
||||
|
||||
```html
|
||||
<!-- Simple text placeholder -->
|
||||
<div data-live-component-lazy="notifications:123"
|
||||
data-lazy-placeholder="Loading your notifications...">
|
||||
</div>
|
||||
|
||||
<!-- No placeholder (just loading indicator) -->
|
||||
<div data-live-component-lazy="stats:123">
|
||||
</div>
|
||||
```
|
||||
|
||||
## Loading States
|
||||
|
||||
The system automatically manages 3 loading states:
|
||||
|
||||
### 1. Placeholder State
|
||||
|
||||
Shown immediately when component is registered:
|
||||
|
||||
```html
|
||||
<div class="livecomponent-lazy-placeholder">
|
||||
<div>⏳</div>
|
||||
<div>Loading notifications...</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 2. Loading State
|
||||
|
||||
Shown when component enters viewport and server request starts:
|
||||
|
||||
```html
|
||||
<div class="livecomponent-lazy-loading">
|
||||
<div class="spinner"></div>
|
||||
<div>Loading component...</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 3. Loaded State
|
||||
|
||||
Component HTML from server replaces placeholder:
|
||||
|
||||
```html
|
||||
<div data-live-component="notification-center:user-123" ...>
|
||||
<!-- Full component HTML with actions, state, etc. -->
|
||||
</div>
|
||||
```
|
||||
|
||||
## Backend Implementation
|
||||
|
||||
### Controller Route
|
||||
|
||||
```php
|
||||
#[Route('/live-component/{id}/lazy-load', method: Method::GET)]
|
||||
public function handleLazyLoad(string $id, HttpRequest $request): JsonResult
|
||||
{
|
||||
try {
|
||||
$componentId = ComponentId::fromString($id);
|
||||
|
||||
// Resolve component with initial state
|
||||
$component = $this->componentRegistry->resolve(
|
||||
$componentId,
|
||||
initialData: null
|
||||
);
|
||||
|
||||
// Render component HTML with wrapper
|
||||
$html = $this->componentRegistry->renderWithWrapper($component);
|
||||
|
||||
// Get component state
|
||||
$componentData = $component->getData();
|
||||
|
||||
// Generate CSRF token for component
|
||||
$csrfToken = $this->componentRegistry->generateCsrfToken($componentId);
|
||||
|
||||
return new JsonResult([
|
||||
'success' => true,
|
||||
'html' => $html,
|
||||
'state' => $componentData->toArray(),
|
||||
'csrf_token' => $csrfToken,
|
||||
'component_id' => $componentId->toString()
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return new JsonResult([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage(),
|
||||
'error_code' => 'LAZY_LOAD_FAILED'
|
||||
], Status::INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Response Format
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"html": "<div data-live-component=\"notification-center:user-123\" ...>...</div>",
|
||||
"state": {
|
||||
"notifications": [...]
|
||||
},
|
||||
"csrf_token": "abc123...",
|
||||
"component_id": "notification-center:user-123"
|
||||
}
|
||||
```
|
||||
|
||||
## JavaScript API
|
||||
|
||||
### LazyComponentLoader Class
|
||||
|
||||
```javascript
|
||||
import { LazyComponentLoader } from './LazyComponentLoader.js';
|
||||
|
||||
// Create loader instance
|
||||
const lazyLoader = new LazyComponentLoader(liveComponentManager);
|
||||
|
||||
// Initialize system (scans DOM for lazy components)
|
||||
lazyLoader.init();
|
||||
|
||||
// Register new lazy component dynamically
|
||||
lazyLoader.registerLazyComponent(element);
|
||||
|
||||
// Unregister lazy component
|
||||
lazyLoader.unregister(element);
|
||||
|
||||
// Get loading statistics
|
||||
const stats = lazyLoader.getStats();
|
||||
console.log(stats);
|
||||
// {
|
||||
// total: 10,
|
||||
// loaded: 3,
|
||||
// loading: 1,
|
||||
// pending: 6,
|
||||
// queued: 2
|
||||
// }
|
||||
|
||||
// Cleanup and destroy
|
||||
lazyLoader.destroy();
|
||||
```
|
||||
|
||||
### Events
|
||||
|
||||
Listen for lazy loading events:
|
||||
|
||||
```javascript
|
||||
// Component successfully loaded
|
||||
document.addEventListener('livecomponent:lazy:loaded', (e) => {
|
||||
console.log('Loaded:', e.detail.componentId);
|
||||
// Custom post-load logic
|
||||
});
|
||||
|
||||
// Component load failed
|
||||
document.addEventListener('livecomponent:lazy:error', (e) => {
|
||||
console.error('Error:', e.detail.componentId, e.detail.error);
|
||||
// Error handling, retry logic, etc.
|
||||
});
|
||||
```
|
||||
|
||||
### Manual Loading
|
||||
|
||||
```javascript
|
||||
// Force load a specific component
|
||||
const config = lazyLoader.lazyComponents.get(element);
|
||||
if (config && !config.loaded) {
|
||||
await lazyLoader.loadComponent(config);
|
||||
}
|
||||
|
||||
// Process loading queue immediately (bypasses delays)
|
||||
await lazyLoader.processLoadingQueue();
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Loading Flow
|
||||
|
||||
```
|
||||
┌─────────────────┐
|
||||
│ DOM Scan │
|
||||
│ (init) │
|
||||
└────────┬────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Register Lazy │
|
||||
│ Components │
|
||||
└────────┬────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Show Placeholder│
|
||||
└────────┬────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Start Observing │
|
||||
│ (Intersection │
|
||||
│ Observer) │
|
||||
└────────┬────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐ No
|
||||
│ Intersecting? ├─────────► Continue Observing
|
||||
└────────┬────────┘
|
||||
│ Yes
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Queue Component │
|
||||
│ (Priority-based)│
|
||||
└────────┬────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Process Queue │
|
||||
│ (Sequential) │
|
||||
└────────┬────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Show Loading │
|
||||
└────────┬────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Fetch from │
|
||||
│ Server │
|
||||
│ GET /lazy-load │
|
||||
└────────┬────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Replace HTML │
|
||||
│ Initialize │
|
||||
│ LiveComponent │
|
||||
└────────┬────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Stop Observing │
|
||||
│ Mark as Loaded │
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
### Priority Queue System
|
||||
|
||||
```javascript
|
||||
// When component enters viewport
|
||||
queueComponentLoad(config) {
|
||||
const priorityWeight = this.getPriorityWeight(config.priority);
|
||||
|
||||
this.loadingQueue.push({
|
||||
config,
|
||||
priority: priorityWeight,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
// Sort by priority (high to low), then timestamp (early to late)
|
||||
this.loadingQueue.sort((a, b) => {
|
||||
if (b.priority !== a.priority) {
|
||||
return b.priority - a.priority;
|
||||
}
|
||||
return a.timestamp - b.timestamp;
|
||||
});
|
||||
|
||||
this.processLoadingQueue();
|
||||
}
|
||||
```
|
||||
|
||||
**Queue Order Example:**
|
||||
```
|
||||
Queue: [
|
||||
{ priority: 3, timestamp: 1000 }, // High, entered first
|
||||
{ priority: 3, timestamp: 1100 }, // High, entered second
|
||||
{ priority: 2, timestamp: 900 }, // Normal, entered early
|
||||
{ priority: 1, timestamp: 1200 } // Low, entered last
|
||||
]
|
||||
```
|
||||
|
||||
## Use Cases
|
||||
|
||||
### 1. Long Landing Pages
|
||||
|
||||
```html
|
||||
<!-- Above the fold - immediate -->
|
||||
<div data-live-component="hero:banner">...</div>
|
||||
<div data-live-component="features:overview">...</div>
|
||||
|
||||
<!-- Below the fold - lazy -->
|
||||
<div data-live-component-lazy="testimonials:featured"
|
||||
data-lazy-priority="normal">
|
||||
</div>
|
||||
|
||||
<div data-live-component-lazy="pricing:table"
|
||||
data-lazy-priority="low">
|
||||
</div>
|
||||
|
||||
<div data-live-component-lazy="faq:list"
|
||||
data-lazy-priority="low">
|
||||
</div>
|
||||
```
|
||||
|
||||
### 2. Dashboard with Widgets
|
||||
|
||||
```html
|
||||
<!-- Critical widgets - high priority -->
|
||||
<div data-live-component-lazy="user-stats:123"
|
||||
data-lazy-priority="high"
|
||||
data-lazy-threshold="0.1">
|
||||
</div>
|
||||
|
||||
<div data-live-component-lazy="notifications:123"
|
||||
data-lazy-priority="high"
|
||||
data-lazy-threshold="0.1">
|
||||
</div>
|
||||
|
||||
<!-- Secondary widgets - normal priority -->
|
||||
<div data-live-component-lazy="activity-feed:123"
|
||||
data-lazy-priority="normal">
|
||||
</div>
|
||||
|
||||
<!-- Tertiary widgets - low priority -->
|
||||
<div data-live-component-lazy="recommendations:123"
|
||||
data-lazy-priority="low">
|
||||
</div>
|
||||
```
|
||||
|
||||
### 3. Infinite Scroll
|
||||
|
||||
```html
|
||||
<!-- Initial visible items -->
|
||||
<div data-live-component="product-card:1">...</div>
|
||||
<div data-live-component="product-card:2">...</div>
|
||||
<div data-live-component="product-card:3">...</div>
|
||||
|
||||
<!-- Lazy load next batch -->
|
||||
<div data-live-component-lazy="product-card:4"
|
||||
data-lazy-threshold="0.5">
|
||||
</div>
|
||||
<div data-live-component-lazy="product-card:5"
|
||||
data-lazy-threshold="0.5">
|
||||
</div>
|
||||
```
|
||||
|
||||
### 4. Tab Panels
|
||||
|
||||
```html
|
||||
<div class="tabs">
|
||||
<!-- Active tab - immediate -->
|
||||
<div class="tab-panel active">
|
||||
<div data-live-component="profile:user-123">...</div>
|
||||
</div>
|
||||
|
||||
<!-- Hidden tabs - lazy load when shown -->
|
||||
<div class="tab-panel">
|
||||
<div data-live-component-lazy="settings:user-123"
|
||||
data-lazy-priority="normal">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-panel">
|
||||
<div data-live-component-lazy="activity-history:user-123"
|
||||
data-lazy-priority="low">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### ✅ DO
|
||||
|
||||
**Use lazy loading for:**
|
||||
- Below-the-fold components
|
||||
- Secondary/tertiary content
|
||||
- Heavy components (charts, tables, media)
|
||||
- Non-critical features
|
||||
- Infrequently accessed sections
|
||||
|
||||
**Priority Guidelines:**
|
||||
- `high`: User-specific data, real-time updates
|
||||
- `normal`: Standard content, common features
|
||||
- `low`: Recommendations, suggestions, ads
|
||||
|
||||
**Threshold Guidelines:**
|
||||
- `0.1`: Standard (load early for smooth UX)
|
||||
- `0.5`: Conservative (load when half-visible)
|
||||
- `1.0`: Aggressive (only when fully visible)
|
||||
|
||||
### ❌ DON'T
|
||||
|
||||
**Avoid lazy loading for:**
|
||||
- Above-the-fold content
|
||||
- Critical user interactions
|
||||
- SEO-important content
|
||||
- Small/lightweight components
|
||||
|
||||
**Anti-patterns:**
|
||||
- Setting all components to `high` priority
|
||||
- Using threshold `1.0` for everything
|
||||
- Lazy loading components user immediately needs
|
||||
- Over-complicating with too many priority levels
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Server-Side
|
||||
|
||||
```php
|
||||
// Cache component HTML for repeated lazy loads
|
||||
public function handleLazyLoad(string $id): JsonResult
|
||||
{
|
||||
$cacheKey = "lazy_component_{$id}";
|
||||
|
||||
return $this->cache->remember($cacheKey, function() use ($id) {
|
||||
$component = $this->componentRegistry->resolve(
|
||||
ComponentId::fromString($id)
|
||||
);
|
||||
|
||||
return new JsonResult([
|
||||
'success' => true,
|
||||
'html' => $this->componentRegistry->renderWithWrapper($component),
|
||||
// ... other data
|
||||
]);
|
||||
}, Duration::fromMinutes(5));
|
||||
}
|
||||
```
|
||||
|
||||
### Client-Side
|
||||
|
||||
```javascript
|
||||
// Adjust root margin for faster pre-loading
|
||||
this.defaultOptions = {
|
||||
rootMargin: '200px', // Start loading 200px early
|
||||
threshold: 0.1
|
||||
};
|
||||
|
||||
// Batch multiple components entering viewport simultaneously
|
||||
// (Already implemented in processLoadingQueue)
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Component doesn't load
|
||||
|
||||
**Check:**
|
||||
1. `data-live-component-lazy` attribute present?
|
||||
2. LazyLoader initialized? (`window.LiveComponent.lazyLoader`)
|
||||
3. Element visible in viewport?
|
||||
4. Network request succeeding? (check DevTools)
|
||||
5. Server route `/live-component/{id}/lazy-load` working?
|
||||
|
||||
**Debug:**
|
||||
```javascript
|
||||
const stats = window.LiveComponent.lazyLoader.getStats();
|
||||
console.log(stats); // Check pending/loading/loaded counts
|
||||
|
||||
// Check if component is registered
|
||||
const config = window.LiveComponent.lazyLoader.lazyComponents.get(element);
|
||||
console.log(config);
|
||||
```
|
||||
|
||||
### Component loads too late/early
|
||||
|
||||
**Adjust threshold:**
|
||||
```html
|
||||
<!-- Load earlier (when 10% visible instead of 50%) -->
|
||||
<div data-live-component-lazy="..."
|
||||
data-lazy-threshold="0.1">
|
||||
</div>
|
||||
```
|
||||
|
||||
**Adjust root margin:**
|
||||
```javascript
|
||||
// Global configuration
|
||||
this.defaultOptions.rootMargin = '100px'; // Load 100px before viewport
|
||||
```
|
||||
|
||||
### Priority not working
|
||||
|
||||
**Check queue:**
|
||||
```javascript
|
||||
console.log(window.LiveComponent.lazyLoader.loadingQueue);
|
||||
// Should be sorted by priority (high to low)
|
||||
```
|
||||
|
||||
**Verify priority value:**
|
||||
```html
|
||||
<div data-lazy-priority="high"> ✅ Correct
|
||||
<div data-lazy-priority="High"> ❌ Case-sensitive!
|
||||
<div data-lazy-priority="urgent"> ❌ Only high/normal/low
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Manual Testing
|
||||
|
||||
```bash
|
||||
# Open test page in browser
|
||||
php tests/debug/test-lazy-loading.php
|
||||
# Navigate to http://localhost/tests/debug/test-lazy-loading.php
|
||||
```
|
||||
|
||||
**Test Checklist:**
|
||||
- [ ] Placeholders shown immediately
|
||||
- [ ] Components load when scrolling into view
|
||||
- [ ] High priority loads before low priority
|
||||
- [ ] Loading spinner appears briefly
|
||||
- [ ] Stats panel updates correctly
|
||||
- [ ] Console logs show loading sequence
|
||||
- [ ] No console errors
|
||||
|
||||
### Automated Testing
|
||||
|
||||
```javascript
|
||||
describe('LazyComponentLoader', () => {
|
||||
it('registers lazy components', () => {
|
||||
const loader = new LazyComponentLoader(mockManager);
|
||||
loader.init();
|
||||
|
||||
const lazyElements = document.querySelectorAll('[data-live-component-lazy]');
|
||||
expect(loader.lazyComponents.size).toBe(lazyElements.length);
|
||||
});
|
||||
|
||||
it('loads component on intersection', async () => {
|
||||
const loader = new LazyComponentLoader(mockManager);
|
||||
loader.init();
|
||||
|
||||
// Simulate intersection
|
||||
const entry = { isIntersecting: true, target: lazyElement };
|
||||
loader.handleIntersection([entry]);
|
||||
|
||||
// Wait for load
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
const config = loader.lazyComponents.get(lazyElement);
|
||||
expect(config.loaded).toBe(true);
|
||||
});
|
||||
|
||||
it('respects priority ordering', () => {
|
||||
const loader = new LazyComponentLoader(mockManager);
|
||||
|
||||
loader.queueComponentLoad({ priority: 'low', ...lowConfig });
|
||||
loader.queueComponentLoad({ priority: 'high', ...highConfig });
|
||||
loader.queueComponentLoad({ priority: 'normal', ...normalConfig });
|
||||
|
||||
expect(loader.loadingQueue[0].config).toBe(highConfig);
|
||||
expect(loader.loadingQueue[1].config).toBe(normalConfig);
|
||||
expect(loader.loadingQueue[2].config).toBe(lowConfig);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
**Lazy Loading provides:**
|
||||
- ✅ **68% faster** initial page load
|
||||
- ✅ **73% smaller** initial JavaScript bundle
|
||||
- ✅ **75% fewer** components loaded initially
|
||||
- ✅ **71% less** memory usage
|
||||
- ✅ **Seamless UX** with priority-based loading
|
||||
- ✅ **Easy integration** with minimal code changes
|
||||
|
||||
**When to use:**
|
||||
- Pages with 10+ components
|
||||
- Below-the-fold content
|
||||
- Heavy components (charts, tables, media)
|
||||
- Secondary/tertiary features
|
||||
|
||||
**Implementation effort:** ⚡ **Low** - Just change `data-live-component` to `data-live-component-lazy`
|
||||
@@ -1,572 +0,0 @@
|
||||
# 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
|
||||
@@ -1,717 +0,0 @@
|
||||
# LiveComponents Nested Components System
|
||||
|
||||
Comprehensive guide for building nested component hierarchies with parent-child relationships, event bubbling, and state synchronization.
|
||||
|
||||
## Overview
|
||||
|
||||
The Nested Components System enables complex UI compositions through parent-child component relationships. Parents can manage global state while children handle localized behavior, with events bubbling up for coordination.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Parent Component │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ Global State Management │ │
|
||||
│ │ • Manages list of items │ │
|
||||
│ │ • Provides data to children │ │
|
||||
│ │ • Handles child events │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌────────────┴────────────┐ │
|
||||
│ │ │ │
|
||||
│ ┌─────────▼────────┐ ┌──────────▼────────┐ │
|
||||
│ │ Child Component │ │ Child Component │ │
|
||||
│ │ • Local State │ │ • Local State │ │
|
||||
│ │ • Dispatch Events│ │ • Dispatch Events│ │
|
||||
│ └──────────────────┘ └───────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### 1. Component Hierarchy
|
||||
|
||||
**ComponentHierarchy Value Object** - Represents parent-child relationships:
|
||||
|
||||
```php
|
||||
use App\Framework\LiveComponents\ValueObjects\ComponentHierarchy;
|
||||
use App\Framework\LiveComponents\ValueObjects\ComponentId;
|
||||
|
||||
// Root component (no parent)
|
||||
$rootHierarchy = ComponentHierarchy::root();
|
||||
// depth=0, path=[]
|
||||
|
||||
// First-level child
|
||||
$childHierarchy = ComponentHierarchy::fromParent(
|
||||
parentId: ComponentId::fromString('parent:main'),
|
||||
childId: ComponentId::fromString('child:1')
|
||||
);
|
||||
// depth=1, path=['parent:main', 'child:1']
|
||||
|
||||
// Add another level
|
||||
$grandchildHierarchy = $childHierarchy->withChild(
|
||||
ComponentId::fromString('grandchild:1')
|
||||
);
|
||||
// depth=2, path=['parent:main', 'child:1', 'grandchild:1']
|
||||
```
|
||||
|
||||
**Hierarchy Queries:**
|
||||
```php
|
||||
$hierarchy->isRoot(); // true if no parent
|
||||
$hierarchy->isChild(); // true if has parent
|
||||
$hierarchy->getLevel(); // nesting depth (0, 1, 2, ...)
|
||||
$hierarchy->isDescendantOf($componentId); // check ancestry
|
||||
```
|
||||
|
||||
### 2. NestedComponentManager
|
||||
|
||||
**Server-Side Hierarchy Management:**
|
||||
|
||||
```php
|
||||
use App\Framework\LiveComponents\NestedComponentManager;
|
||||
|
||||
$manager = new NestedComponentManager();
|
||||
|
||||
// Register root component
|
||||
$parentId = ComponentId::fromString('todo-list:main');
|
||||
$manager->registerHierarchy($parentId, ComponentHierarchy::root());
|
||||
|
||||
// Register child
|
||||
$childId = ComponentId::fromString('todo-item:1');
|
||||
$childHierarchy = ComponentHierarchy::fromParent($parentId, $childId);
|
||||
$manager->registerHierarchy($childId, $childHierarchy);
|
||||
|
||||
// Query hierarchy
|
||||
$manager->hasChildren($parentId); // true
|
||||
$manager->getChildIds($parentId); // [ComponentId('todo-item:1')]
|
||||
$manager->getParentId($childId); // ComponentId('todo-list:main')
|
||||
$manager->isRoot($parentId); // true
|
||||
$manager->getDepth($childId); // 1
|
||||
|
||||
// Get all ancestors/descendants
|
||||
$ancestors = $manager->getAncestors($childId); // [parentId]
|
||||
$descendants = $manager->getDescendants($parentId); // [childId]
|
||||
|
||||
// Statistics
|
||||
$stats = $manager->getStats();
|
||||
// ['total_components' => 2, 'root_components' => 1, ...]
|
||||
```
|
||||
|
||||
### 3. SupportsNesting Interface
|
||||
|
||||
**Parent components must implement this interface:**
|
||||
|
||||
```php
|
||||
use App\Framework\LiveComponents\Contracts\SupportsNesting;
|
||||
|
||||
interface SupportsNesting
|
||||
{
|
||||
/**
|
||||
* Get list of child component IDs
|
||||
*/
|
||||
public function getChildComponents(): array;
|
||||
|
||||
/**
|
||||
* Handle event from child component
|
||||
*
|
||||
* @return bool Return false to stop event bubbling, true to continue
|
||||
*/
|
||||
public function onChildEvent(ComponentId $childId, string $eventName, array $payload): bool;
|
||||
|
||||
/**
|
||||
* Validate child component compatibility
|
||||
*/
|
||||
public function canHaveChild(ComponentId $childId): bool;
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Event Bubbling
|
||||
|
||||
**Events flow from child to parent:**
|
||||
|
||||
```
|
||||
┌─────────────────────────┐
|
||||
│ Child Component │
|
||||
│ • User clicks button │
|
||||
│ • Dispatches event │
|
||||
└───────────┬─────────────┘
|
||||
│ Event Bubbles Up
|
||||
▼
|
||||
┌─────────────────────────┐
|
||||
│ Parent Component │
|
||||
│ • Receives event │
|
||||
│ • Updates state │
|
||||
│ • Re-renders children │
|
||||
└─────────────────────────┘
|
||||
```
|
||||
|
||||
**Event Dispatcher:**
|
||||
```php
|
||||
use App\Framework\LiveComponents\NestedComponentEventDispatcher;
|
||||
|
||||
$dispatcher = new NestedComponentEventDispatcher();
|
||||
|
||||
// Child dispatches event
|
||||
$dispatcher->dispatch(
|
||||
componentId: ComponentId::fromString('todo-item:1'),
|
||||
eventName: 'todo-completed',
|
||||
payload: [
|
||||
'todo_id' => '1',
|
||||
'completed' => true
|
||||
]
|
||||
);
|
||||
|
||||
// Check dispatched events
|
||||
$dispatcher->hasEvents(); // true
|
||||
$dispatcher->count(); // 1
|
||||
$events = $dispatcher->getEvents();
|
||||
```
|
||||
|
||||
## Implementation Guide
|
||||
|
||||
### Creating a Parent Component
|
||||
|
||||
**1. Implement SupportsNesting:**
|
||||
|
||||
```php
|
||||
use App\Framework\LiveComponents\Contracts\LiveComponentContract;
|
||||
use App\Framework\LiveComponents\Contracts\SupportsNesting;
|
||||
use App\Framework\LiveComponents\Attributes\LiveComponent;
|
||||
|
||||
#[LiveComponent('todo-list')]
|
||||
final readonly class TodoListComponent implements LiveComponentContract, SupportsNesting
|
||||
{
|
||||
private ComponentId $id;
|
||||
private TodoListState $state;
|
||||
|
||||
public function __construct(
|
||||
ComponentId $id,
|
||||
?ComponentData $initialData = null,
|
||||
array $todos = []
|
||||
) {
|
||||
$this->id = $id;
|
||||
$this->state = $initialData
|
||||
? TodoListState::fromComponentData($initialData)
|
||||
: new TodoListState(todos: $todos);
|
||||
}
|
||||
|
||||
// LiveComponentContract methods
|
||||
public function getId(): ComponentId { return $this->id; }
|
||||
public function getData(): ComponentData { return $this->state->toComponentData(); }
|
||||
public function getRenderData(): ComponentRenderData { /* ... */ }
|
||||
|
||||
// SupportsNesting methods
|
||||
|
||||
public function getChildComponents(): array
|
||||
{
|
||||
// Return array of child component IDs
|
||||
$childIds = [];
|
||||
foreach ($this->state->todos as $todo) {
|
||||
$childIds[] = "todo-item:{$todo['id']}";
|
||||
}
|
||||
return $childIds;
|
||||
}
|
||||
|
||||
public function onChildEvent(ComponentId $childId, string $eventName, array $payload): bool
|
||||
{
|
||||
// Handle events from children
|
||||
match ($eventName) {
|
||||
'todo-completed' => $this->handleTodoCompleted($payload),
|
||||
'todo-deleted' => $this->handleTodoDeleted($payload),
|
||||
default => null
|
||||
};
|
||||
|
||||
return true; // Continue bubbling
|
||||
}
|
||||
|
||||
public function canHaveChild(ComponentId $childId): bool
|
||||
{
|
||||
// Only accept TodoItem components
|
||||
return str_starts_with($childId->name, 'todo-item');
|
||||
}
|
||||
|
||||
private function handleTodoCompleted(array $payload): void
|
||||
{
|
||||
$todoId = $payload['todo_id'];
|
||||
$completed = $payload['completed'];
|
||||
|
||||
// Log or trigger side effects
|
||||
error_log("Todo {$todoId} marked as " . ($completed ? 'completed' : 'active'));
|
||||
|
||||
// Note: State updates happen through Actions, not event handlers
|
||||
// Event handlers are for logging, analytics, side effects
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**2. Create Parent State:**
|
||||
|
||||
```php
|
||||
final readonly class TodoListState
|
||||
{
|
||||
public function __construct(
|
||||
public array $todos = [],
|
||||
public string $filter = 'all'
|
||||
) {}
|
||||
|
||||
public static function fromComponentData(ComponentData $data): self
|
||||
{
|
||||
$array = $data->toArray();
|
||||
return new self(
|
||||
todos: $array['todos'] ?? [],
|
||||
filter: $array['filter'] ?? 'all'
|
||||
);
|
||||
}
|
||||
|
||||
public function toComponentData(): ComponentData
|
||||
{
|
||||
return ComponentData::fromArray([
|
||||
'todos' => $this->todos,
|
||||
'filter' => $this->filter
|
||||
]);
|
||||
}
|
||||
|
||||
public function withTodoAdded(array $todo): self
|
||||
{
|
||||
return new self(
|
||||
todos: [...$this->todos, $todo],
|
||||
filter: $this->filter
|
||||
);
|
||||
}
|
||||
|
||||
// More transformation methods...
|
||||
}
|
||||
```
|
||||
|
||||
**3. Create Parent Template:**
|
||||
|
||||
```html
|
||||
<!-- todo-list.view.php -->
|
||||
<div class="todo-list">
|
||||
<!-- Parent UI -->
|
||||
<h2>My Todos ({total_count})</h2>
|
||||
|
||||
<!-- Child Components -->
|
||||
<for items="todos" as="todo">
|
||||
<div
|
||||
data-live-component="todo-item:{todo.id}"
|
||||
data-parent-component="{component_id}"
|
||||
data-nesting-depth="1"
|
||||
>
|
||||
<!-- TodoItemComponent renders here -->
|
||||
</div>
|
||||
</for>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Creating a Child Component
|
||||
|
||||
**1. Implement Component with Event Dispatcher:**
|
||||
|
||||
```php
|
||||
use App\Framework\LiveComponents\NestedComponentEventDispatcher;
|
||||
|
||||
#[LiveComponent('todo-item')]
|
||||
final readonly class TodoItemComponent implements LiveComponentContract
|
||||
{
|
||||
private ComponentId $id;
|
||||
private TodoItemState $state;
|
||||
|
||||
public function __construct(
|
||||
ComponentId $id,
|
||||
private NestedComponentEventDispatcher $eventDispatcher,
|
||||
?ComponentData $initialData = null,
|
||||
?array $todoData = null
|
||||
) {
|
||||
$this->id = $id;
|
||||
$this->state = $initialData
|
||||
? TodoItemState::fromComponentData($initialData)
|
||||
: TodoItemState::fromTodoArray($todoData ?? []);
|
||||
}
|
||||
|
||||
#[Action]
|
||||
public function toggle(): ComponentData
|
||||
{
|
||||
$newState = $this->state->withToggled();
|
||||
|
||||
// Dispatch event to parent
|
||||
$this->eventDispatcher->dispatch(
|
||||
componentId: $this->id,
|
||||
eventName: 'todo-completed',
|
||||
payload: [
|
||||
'todo_id' => $this->state->id,
|
||||
'completed' => $newState->completed
|
||||
]
|
||||
);
|
||||
|
||||
return $newState->toComponentData();
|
||||
}
|
||||
|
||||
#[Action]
|
||||
public function delete(): ComponentData
|
||||
{
|
||||
// Dispatch delete event to parent
|
||||
$this->eventDispatcher->dispatch(
|
||||
componentId: $this->id,
|
||||
eventName: 'todo-deleted',
|
||||
payload: ['todo_id' => $this->state->id]
|
||||
);
|
||||
|
||||
return $this->state->toComponentData();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**2. Create Child State:**
|
||||
|
||||
```php
|
||||
final readonly class TodoItemState
|
||||
{
|
||||
public function __construct(
|
||||
public string $id,
|
||||
public string $title,
|
||||
public bool $completed = false,
|
||||
public int $createdAt = 0
|
||||
) {}
|
||||
|
||||
public static function fromTodoArray(array $todo): self
|
||||
{
|
||||
return new self(
|
||||
id: $todo['id'] ?? '',
|
||||
title: $todo['title'] ?? '',
|
||||
completed: $todo['completed'] ?? false,
|
||||
createdAt: $todo['created_at'] ?? time()
|
||||
);
|
||||
}
|
||||
|
||||
public function withToggled(): self
|
||||
{
|
||||
return new self(
|
||||
id: $this->id,
|
||||
title: $this->title,
|
||||
completed: !$this->completed,
|
||||
createdAt: $this->createdAt
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**3. Create Child Template:**
|
||||
|
||||
```html
|
||||
<!-- todo-item.view.php -->
|
||||
<div class="todo-item {completed|then:todo-item--completed}">
|
||||
<button data-livecomponent-action="toggle">
|
||||
<if condition="completed">✓</if>
|
||||
</button>
|
||||
|
||||
<div class="todo-item__title">{title}</div>
|
||||
|
||||
<button data-livecomponent-action="delete">✕</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Client-Side Integration
|
||||
|
||||
**Automatic Initialization:**
|
||||
|
||||
```javascript
|
||||
// NestedComponentHandler automatically initializes with LiveComponentManager
|
||||
import { LiveComponentManager } from './livecomponent/index.js';
|
||||
|
||||
// Scans DOM for nested components
|
||||
const nestedHandler = liveComponentManager.nestedHandler;
|
||||
|
||||
// Get hierarchy info
|
||||
const parentId = nestedHandler.getParentId('todo-item:1'); // 'todo-list:main'
|
||||
const childIds = nestedHandler.getChildIds('todo-list:main'); // ['todo-item:1', ...]
|
||||
|
||||
// Event bubbling
|
||||
nestedHandler.bubbleEvent('todo-item:1', 'todo-completed', {
|
||||
todo_id: '1',
|
||||
completed: true
|
||||
});
|
||||
|
||||
// Statistics
|
||||
const stats = nestedHandler.getStats();
|
||||
// { total_components: 5, root_components: 1, max_nesting_depth: 2, ... }
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. State Management
|
||||
|
||||
**✅ Parent owns the data:**
|
||||
```php
|
||||
// Parent manages list
|
||||
private TodoListState $state; // Contains all todos
|
||||
|
||||
// Child manages display state only
|
||||
private TodoItemState $state; // id, title, completed, isEditing
|
||||
```
|
||||
|
||||
**❌ Don't duplicate state:**
|
||||
```php
|
||||
// Bad: Both parent and child store todo data
|
||||
// This leads to synchronization issues
|
||||
```
|
||||
|
||||
### 2. Event Handling
|
||||
|
||||
**✅ Use event handlers for side effects:**
|
||||
```php
|
||||
public function onChildEvent(ComponentId $childId, string $eventName, array $payload): bool
|
||||
{
|
||||
// ✅ Logging
|
||||
error_log("Child event: {$eventName}");
|
||||
|
||||
// ✅ Analytics
|
||||
$this->analytics->track($eventName, $payload);
|
||||
|
||||
// ✅ External system updates
|
||||
$this->cache->invalidate($payload['todo_id']);
|
||||
|
||||
return true; // Continue bubbling
|
||||
}
|
||||
```
|
||||
|
||||
**❌ Don't modify state in event handlers:**
|
||||
```php
|
||||
// ❌ Bad: Event handlers shouldn't modify component state
|
||||
// State changes happen through Actions that return new ComponentData
|
||||
```
|
||||
|
||||
### 3. Child Compatibility
|
||||
|
||||
**✅ Validate child types:**
|
||||
```php
|
||||
public function canHaveChild(ComponentId $childId): bool
|
||||
{
|
||||
// Only accept specific component types
|
||||
return str_starts_with($childId->name, 'todo-item');
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Circular Dependencies
|
||||
|
||||
**✅ Framework automatically prevents:**
|
||||
```php
|
||||
// This will throw InvalidArgumentException:
|
||||
$manager->registerHierarchy($componentId, $hierarchy);
|
||||
// "Circular dependency detected: Component cannot be its own ancestor"
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Hierarchy Depth
|
||||
|
||||
- **Recommended:** Max 3-4 levels deep
|
||||
- **Reason:** Each level adds overhead for event bubbling
|
||||
- **Alternative:** Flatten hierarchy when possible
|
||||
|
||||
### Event Bubbling
|
||||
|
||||
- **Cost:** O(depth) for each event
|
||||
- **Optimization:** Stop bubbling early when not needed
|
||||
- **Pattern:** Return `false` from `onChildEvent()` to stop
|
||||
|
||||
```php
|
||||
public function onChildEvent(ComponentId $childId, string $eventName, array $payload): bool
|
||||
{
|
||||
if ($eventName === 'internal-event') {
|
||||
// Handle locally, don't bubble further
|
||||
return false;
|
||||
}
|
||||
|
||||
// Let other events bubble
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
### State Synchronization
|
||||
|
||||
- **Pattern:** Parent as single source of truth
|
||||
- **Benefit:** Avoids synchronization bugs
|
||||
- **Trade-off:** More re-renders, but simpler logic
|
||||
|
||||
## Testing
|
||||
|
||||
### Unit Tests
|
||||
|
||||
```php
|
||||
describe('NestedComponentManager', function () {
|
||||
it('tracks parent-child relationships', function () {
|
||||
$manager = new NestedComponentManager();
|
||||
|
||||
$parentId = ComponentId::fromString('parent:1');
|
||||
$childId = ComponentId::fromString('child:1');
|
||||
|
||||
$manager->registerHierarchy($parentId, ComponentHierarchy::root());
|
||||
$manager->registerHierarchy(
|
||||
$childId,
|
||||
ComponentHierarchy::fromParent($parentId, $childId)
|
||||
);
|
||||
|
||||
expect($manager->hasChildren($parentId))->toBeTrue();
|
||||
expect($manager->getParentId($childId))->toEqual($parentId);
|
||||
});
|
||||
|
||||
it('prevents circular dependencies', function () {
|
||||
$manager = new NestedComponentManager();
|
||||
$id = ComponentId::fromString('self:1');
|
||||
|
||||
expect(fn() => $manager->registerHierarchy(
|
||||
$id,
|
||||
ComponentHierarchy::fromParent($id, $id)
|
||||
))->toThrow(InvalidArgumentException::class);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Integration Tests
|
||||
|
||||
```php
|
||||
describe('TodoList with nested TodoItems', function () {
|
||||
it('handles child events', function () {
|
||||
$todoList = new TodoListComponent(
|
||||
id: ComponentId::fromString('todo-list:test'),
|
||||
todos: [
|
||||
['id' => '1', 'title' => 'Test', 'completed' => false]
|
||||
]
|
||||
);
|
||||
|
||||
$childId = ComponentId::fromString('todo-item:1');
|
||||
|
||||
// Simulate child event
|
||||
$result = $todoList->onChildEvent(
|
||||
$childId,
|
||||
'todo-completed',
|
||||
['todo_id' => '1', 'completed' => true]
|
||||
);
|
||||
|
||||
expect($result)->toBeTrue(); // Event bubbled successfully
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Problem: Children not rendering
|
||||
|
||||
**Cause:** Missing `data-parent-component` attribute
|
||||
|
||||
**Solution:**
|
||||
```html
|
||||
<!-- ✅ Correct -->
|
||||
<div
|
||||
data-live-component="child:1"
|
||||
data-parent-component="parent:main"
|
||||
data-nesting-depth="1"
|
||||
>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Problem: Events not bubbling
|
||||
|
||||
**Cause:** Wrong ComponentId or event name
|
||||
|
||||
**Solution:**
|
||||
```php
|
||||
// ✅ Use exact component ID
|
||||
$this->eventDispatcher->dispatch(
|
||||
componentId: $this->id, // ✅ Correct: use component's own ID
|
||||
eventName: 'todo-completed',
|
||||
payload: [...]
|
||||
);
|
||||
```
|
||||
|
||||
### Problem: Circular dependency error
|
||||
|
||||
**Cause:** Component trying to be its own ancestor
|
||||
|
||||
**Solution:**
|
||||
```php
|
||||
// ❌ Wrong: Same component as parent and child
|
||||
$hierarchy = ComponentHierarchy::fromParent($sameId, $sameId);
|
||||
|
||||
// ✅ Correct: Different components
|
||||
$hierarchy = ComponentHierarchy::fromParent($parentId, $childId);
|
||||
```
|
||||
|
||||
## Advanced Patterns
|
||||
|
||||
### Multi-Level Nesting
|
||||
|
||||
```php
|
||||
// Grandparent → Parent → Child
|
||||
$grandparent = ComponentHierarchy::root();
|
||||
|
||||
$parent = ComponentHierarchy::fromParent(
|
||||
ComponentId::fromString('grandparent:1'),
|
||||
ComponentId::fromString('parent:1')
|
||||
);
|
||||
|
||||
$child = $parent->withChild(
|
||||
ComponentId::fromString('child:1')
|
||||
);
|
||||
// depth=2, path=['grandparent:1', 'parent:1', 'child:1']
|
||||
```
|
||||
|
||||
### Conditional Children
|
||||
|
||||
```php
|
||||
public function getChildComponents(): array
|
||||
{
|
||||
// Only show children if filter matches
|
||||
$filteredTodos = $this->state->getFilteredTodos();
|
||||
|
||||
return array_map(
|
||||
fn($todo) => "todo-item:{$todo['id']}",
|
||||
$filteredTodos
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Dynamic Child Addition
|
||||
|
||||
```php
|
||||
#[Action]
|
||||
public function addTodo(string $title): ComponentData
|
||||
{
|
||||
$newTodo = [
|
||||
'id' => uniqid('todo_', true),
|
||||
'title' => $title,
|
||||
'completed' => false
|
||||
];
|
||||
|
||||
// State includes new todo
|
||||
$newState = $this->state->withTodoAdded($newTodo);
|
||||
|
||||
// Framework automatically creates child component
|
||||
// based on getChildComponents() result
|
||||
|
||||
return $newState->toComponentData();
|
||||
}
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
**Nested Components enable:**
|
||||
- ✅ Complex UI compositions
|
||||
- ✅ Parent-child communication via events
|
||||
- ✅ Hierarchical state management
|
||||
- ✅ Reusable component patterns
|
||||
- ✅ Type-safe relationships
|
||||
|
||||
**Key Classes:**
|
||||
- `ComponentHierarchy` - Relationship value object
|
||||
- `NestedComponentManager` - Server-side hierarchy
|
||||
- `NestedComponentHandler` - Client-side hierarchy
|
||||
- `NestedComponentEventDispatcher` - Event bubbling
|
||||
- `SupportsNesting` - Parent component interface
|
||||
|
||||
**Next Steps:**
|
||||
- Implement Slot System for flexible composition
|
||||
- Add SSE integration for real-time updates
|
||||
- Explore advanced caching strategies
|
||||
@@ -1,223 +0,0 @@
|
||||
# LiveComponent Security Model
|
||||
|
||||
## CSRF-Schutz für LiveComponents
|
||||
|
||||
### Frage: Sollten wir CSRF grundsätzlich für LiveComponents deaktivieren?
|
||||
|
||||
**Antwort: Ja, aber mit alternativen Sicherheitsmaßnahmen.**
|
||||
|
||||
### Warum CSRF-Deaktivierung bei LiveComponents sinnvoll ist
|
||||
|
||||
#### 1. **Technische Inkompatibilität**
|
||||
- LiveComponents senden **State per JSON**, nicht als Form-Data
|
||||
- Traditionelle CSRF-Tokens in `<form>`-Elementen funktionieren nicht
|
||||
- AJAX-Requests benötigen andere Token-Delivery-Mechanismen
|
||||
- Token-Rotation würde LiveComponent-State invalidieren
|
||||
|
||||
#### 2. **Architekturelle Gründe**
|
||||
- **Stateless Component Model**: Jeder Request enthält vollständigen State
|
||||
- **Component-ID als Identifier**: Komponenten sind durch eindeutige IDs identifiziert
|
||||
- **Action-basierte Security**: Actions werden explizit auf Component-Ebene validiert
|
||||
- **Version Tracking**: Concurrent Update Detection durch Version-Nummern
|
||||
|
||||
#### 3. **Alternative Sicherheitsmaßnahmen**
|
||||
LiveComponents haben ein **eigenes Sicherheitsmodell**:
|
||||
|
||||
```php
|
||||
// ComponentAction mit Validierung
|
||||
final readonly class ComponentAction
|
||||
{
|
||||
public function __construct(
|
||||
public string $componentId, // Eindeutige Component-ID
|
||||
public string $method, // Explizite Action-Methode
|
||||
public array $params, // Validierte Parameter
|
||||
public int $version // Concurrent Update Detection
|
||||
) {}
|
||||
}
|
||||
```
|
||||
|
||||
### Implementierte Security-Layer für LiveComponents
|
||||
|
||||
#### 1. **Origin Validation**
|
||||
```php
|
||||
// SameSite Cookies + Origin Header Check
|
||||
if ($request->headers->get('Origin') !== $expectedOrigin) {
|
||||
throw new SecurityException('Invalid origin');
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. **X-Requested-With Header**
|
||||
```php
|
||||
// AJAX-Request Verification
|
||||
if ($request->headers->get('X-Requested-With') !== 'XMLHttpRequest') {
|
||||
throw new SecurityException('Invalid request type');
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. **Component State Integrity**
|
||||
```php
|
||||
// State Tampering Detection
|
||||
$hash = hash_hmac('sha256', json_encode($state), $secretKey);
|
||||
if (!hash_equals($hash, $providedHash)) {
|
||||
throw new SecurityException('State tampering detected');
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. **Version-based Concurrency Control**
|
||||
```php
|
||||
// Prevent Concurrent Update Issues
|
||||
if ($currentVersion !== $expectedVersion) {
|
||||
throw new ConcurrentUpdateException('State has changed');
|
||||
}
|
||||
```
|
||||
|
||||
### Middleware-Konfiguration
|
||||
|
||||
#### CSRF-Middleware Skip
|
||||
```php
|
||||
// src/Framework/Http/Middlewares/CsrfMiddleware.php
|
||||
|
||||
// Skip CSRF validation for API routes and LiveComponent AJAX endpoints
|
||||
// LiveComponents use stateless, component-scoped security model instead
|
||||
if (str_starts_with($request->path, '/api/') ||
|
||||
str_starts_with($request->path, '/live-component/') ||
|
||||
str_starts_with($request->path, '/livecomponent/')) {
|
||||
return $next($context);
|
||||
}
|
||||
```
|
||||
|
||||
#### Honeypot-Middleware Skip
|
||||
```php
|
||||
// src/Framework/Http/Middlewares/HoneypotMiddleware.php
|
||||
|
||||
// Skip honeypot validation for API routes and LiveComponent AJAX endpoints
|
||||
if (str_starts_with($request->path, '/api/') ||
|
||||
str_starts_with($request->path, '/live-component/') ||
|
||||
str_starts_with($request->path, '/livecomponent/')) {
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
### LiveComponent Routes
|
||||
|
||||
```php
|
||||
// Framework Route
|
||||
#[Route('/live-component/{id}', method: Method::POST)]
|
||||
public function handleAction(string $id, HttpRequest $request): JsonResult
|
||||
|
||||
// Upload Route
|
||||
#[Route('/live-component/{id}/upload', method: Method::POST)]
|
||||
public function handleUpload(string $id, HttpRequest $request): JsonResult
|
||||
```
|
||||
|
||||
### Sicherheitsempfehlungen
|
||||
|
||||
#### ✅ DO (Implementiert):
|
||||
1. **Origin Validation**: Same-Origin-Policy durchsetzen
|
||||
2. **X-Requested-With Header**: AJAX-Requests validieren
|
||||
3. **Component State Integrity**: State-Hashing implementieren
|
||||
4. **Version Control**: Concurrent Updates erkennen
|
||||
5. **Rate Limiting**: API-Rate-Limits für LiveComponent-Endpoints
|
||||
6. **Session Validation**: Authentifizierte User-Sessions prüfen
|
||||
|
||||
#### ❌ DON'T:
|
||||
1. **Keine traditionellen CSRF-Tokens** in LiveComponent-Requests
|
||||
2. **Keine Honeypot-Felder** in JSON-Payloads
|
||||
3. **Keine Token-Rotation** während LiveComponent-Sessions
|
||||
4. **Keine Form-basierte Validierung** für AJAX-Endpoints
|
||||
|
||||
### Security Threat Model
|
||||
|
||||
#### Bedrohungen die WEITERHIN abgewehrt werden:
|
||||
- ✅ **Session Hijacking**: Session-Cookie mit HttpOnly + Secure Flags
|
||||
- ✅ **XSS Attacks**: Content Security Policy + Output Escaping
|
||||
- ✅ **Man-in-the-Middle**: HTTPS-Only Communication
|
||||
- ✅ **Replay Attacks**: Version-based Concurrency Detection
|
||||
- ✅ **State Tampering**: HMAC State Integrity Validation
|
||||
|
||||
#### Bedrohungen die durch CSRF-Skip entstehen könnten:
|
||||
- ⚠️ **Cross-Site Request Forgery**: Durch Origin Validation abgedeckt
|
||||
- ⚠️ **Clickjacking**: Durch X-Frame-Options Header abgedeckt
|
||||
- ⚠️ **JSON Hijacking**: Durch X-Requested-With Header abgedeckt
|
||||
|
||||
### Alternative Security Implementation
|
||||
|
||||
Für **kritische Actions** (z.B. Zahlungen, Account-Löschung):
|
||||
|
||||
```php
|
||||
// Zusätzliche Action-Level Security
|
||||
final class CriticalAction extends LiveComponent
|
||||
{
|
||||
public function deleteAccount(array $params): ComponentUpdate
|
||||
{
|
||||
// 1. Re-Authentication Check
|
||||
if (!$this->session->recentlyAuthenticated()) {
|
||||
throw new ReAuthenticationRequired();
|
||||
}
|
||||
|
||||
// 2. Action-Specific Token
|
||||
$actionToken = $params['action_token'] ?? null;
|
||||
if (!$this->validateActionToken($actionToken)) {
|
||||
throw new InvalidActionToken();
|
||||
}
|
||||
|
||||
// 3. Rate Limiting
|
||||
if ($this->rateLimiter->tooManyAttempts($this->userId)) {
|
||||
throw new TooManyAttemptsException();
|
||||
}
|
||||
|
||||
// 4. Execute Critical Action
|
||||
$this->accountService->delete($this->userId);
|
||||
|
||||
return ComponentUpdate::withMessage('Account deleted');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Testing Security
|
||||
|
||||
```php
|
||||
// Security Test Cases
|
||||
describe('LiveComponent Security', function () {
|
||||
it('rejects requests without X-Requested-With header', function () {
|
||||
$response = $this->post('/live-component/datatable:demo', [
|
||||
'action' => 'sort'
|
||||
]);
|
||||
|
||||
expect($response->status)->toBe(403);
|
||||
});
|
||||
|
||||
it('validates component state integrity', function () {
|
||||
$tamperedState = ['malicious' => 'data'];
|
||||
|
||||
$response = $this->post('/live-component/datatable:demo', [
|
||||
'action' => 'sort',
|
||||
'state' => $tamperedState
|
||||
], [
|
||||
'X-Requested-With' => 'XMLHttpRequest'
|
||||
]);
|
||||
|
||||
expect($response->status)->toBe(400);
|
||||
expect($response->json()['error'])->toContain('State tampering');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Zusammenfassung
|
||||
|
||||
**CSRF und Honeypot sind für LiveComponents deaktiviert**, weil:
|
||||
|
||||
1. ✅ **Technisch inkompatibel** mit JSON-basiertem State Management
|
||||
2. ✅ **Architektonisch unnötig** durch Component-scoped Security Model
|
||||
3. ✅ **Durch alternative Maßnahmen ersetzt**: Origin Validation, State Integrity, Version Control
|
||||
4. ✅ **Best Practice** in modernen JavaScript-Frameworks (React, Vue, Angular)
|
||||
|
||||
**Die Sicherheit wird gewährleistet durch:**
|
||||
- Origin Validation (Same-Origin-Policy)
|
||||
- X-Requested-With Header Validation
|
||||
- Component State Integrity (HMAC)
|
||||
- Version-based Concurrency Control
|
||||
- Session Validation für authentifizierte Actions
|
||||
- Optional: Action-Level Tokens für kritische Operations
|
||||
|
||||
Dies entspricht dem **Security-Model moderner Single-Page Applications** und ist die empfohlene Vorgehensweise für AJAX-basierte Component-Systeme.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,461 +0,0 @@
|
||||
# LiveComponents Best Practices
|
||||
|
||||
## Template-System Philosophie
|
||||
|
||||
**Grundprinzip**: Templates sollten **nur für die Darstellung** zuständig sein, nicht für Logik.
|
||||
|
||||
### ✅ Was Templates können
|
||||
|
||||
- **Variable Substitution**: `{{variableName}}`
|
||||
- **Conditional Rendering**: `{{#if condition}}...{{/if}}`
|
||||
- **Loops**: `{{#each items}}...{{/each}}`
|
||||
- **Nested Properties**: `{{user.name}}`, `{{item.value}}`
|
||||
|
||||
### ❌ Was Templates NICHT können
|
||||
|
||||
- Komplexe Expressions: `{{user.role === 'admin' && user.active}}`
|
||||
- Berechnungen: `{{count * 2}}`, `{{items.length}}`
|
||||
- Method Calls: `{{formatDate(created)}}`, `{{user.getName()}}`
|
||||
- Vergleichsoperatoren in Platzhaltern: `{{price > 100}}`
|
||||
|
||||
## Best Practice: Daten im Component vorbereiten
|
||||
|
||||
### Anti-Pattern ❌
|
||||
|
||||
```php
|
||||
// Component
|
||||
public function getRenderData(): ComponentRenderData
|
||||
{
|
||||
return new ComponentRenderData('user-card', [
|
||||
'user' => $this->user,
|
||||
'permissions' => $this->permissions
|
||||
]);
|
||||
}
|
||||
```
|
||||
|
||||
```html
|
||||
<!-- Template mit komplexer Logik -->
|
||||
{{#if user.role}}
|
||||
{{#if user.role === 'admin'}}
|
||||
{{#if user.isActive}}
|
||||
<span class="badge">{{permissions.length}} Permissions</span>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
```
|
||||
|
||||
**Probleme:**
|
||||
- ❌ Logik im Template schwer testbar
|
||||
- ❌ Template-Syntax unterstützt keine Vergleichsoperatoren
|
||||
- ❌ Keine Type Safety
|
||||
- ❌ Schwer zu debuggen
|
||||
|
||||
### Best Practice ✅
|
||||
|
||||
```php
|
||||
// Component - Daten vollständig vorbereiten
|
||||
public function getRenderData(): ComponentRenderData
|
||||
{
|
||||
return new ComponentRenderData('user-card', [
|
||||
'user' => $this->user,
|
||||
'showAdminBadge' => $this->shouldShowAdminBadge(),
|
||||
'permissionCount' => $this->getPermissionCount(),
|
||||
'badgeClass' => $this->getBadgeClass(),
|
||||
'badgeText' => $this->getBadgeText()
|
||||
]);
|
||||
}
|
||||
|
||||
private function shouldShowAdminBadge(): bool
|
||||
{
|
||||
return $this->user->role === 'admin' && $this->user->isActive;
|
||||
}
|
||||
|
||||
private function getPermissionCount(): int
|
||||
{
|
||||
return count($this->permissions);
|
||||
}
|
||||
|
||||
private function getBadgeClass(): string
|
||||
{
|
||||
return $this->user->isActive ? 'badge-success' : 'badge-secondary';
|
||||
}
|
||||
|
||||
private function getBadgeText(): string
|
||||
{
|
||||
$count = $this->getPermissionCount();
|
||||
return "{$count} Permission" . ($count !== 1 ? 's' : '');
|
||||
}
|
||||
```
|
||||
|
||||
```html
|
||||
<!-- Template - nur Darstellung -->
|
||||
{{#if showAdminBadge}}
|
||||
<span class="badge {{badgeClass}}">{{badgeText}}</span>
|
||||
{{/if}}
|
||||
```
|
||||
|
||||
**Vorteile:**
|
||||
- ✅ Business-Logik testbar in Component
|
||||
- ✅ Template einfach und lesbar
|
||||
- ✅ Type Safety durch PHP
|
||||
- ✅ Einfach zu debuggen
|
||||
- ✅ Wiederverwendbare Component-Methoden
|
||||
|
||||
## Praktische Beispiele
|
||||
|
||||
### Beispiel 1: Conditional Rendering
|
||||
|
||||
**❌ Anti-Pattern:**
|
||||
```html
|
||||
{{#if user.orders.length > 0}}
|
||||
<div>User has {{user.orders.length}} orders</div>
|
||||
{{/if}}
|
||||
```
|
||||
|
||||
**✅ Best Practice:**
|
||||
```php
|
||||
// Component
|
||||
public function getRenderData(): ComponentRenderData
|
||||
{
|
||||
return new ComponentRenderData('user-summary', [
|
||||
'hasOrders' => $this->hasOrders(),
|
||||
'orderCount' => $this->getOrderCount(),
|
||||
'orderText' => $this->getOrderText()
|
||||
]);
|
||||
}
|
||||
|
||||
private function hasOrders(): bool
|
||||
{
|
||||
return count($this->user->orders) > 0;
|
||||
}
|
||||
|
||||
private function getOrderCount(): int
|
||||
{
|
||||
return count($this->user->orders);
|
||||
}
|
||||
|
||||
private function getOrderText(): string
|
||||
{
|
||||
$count = $this->getOrderCount();
|
||||
return "User has {$count} order" . ($count !== 1 ? 's' : '');
|
||||
}
|
||||
```
|
||||
|
||||
```html
|
||||
<!-- Template -->
|
||||
{{#if hasOrders}}
|
||||
<div>{{orderText}}</div>
|
||||
{{/if}}
|
||||
```
|
||||
|
||||
### Beispiel 2: Formatierung & Berechnungen
|
||||
|
||||
**❌ Anti-Pattern:**
|
||||
```html
|
||||
<div class="price">€ {{price * 1.19}}</div>
|
||||
<div class="date">{{created.format('d.m.Y')}}</div>
|
||||
```
|
||||
|
||||
**✅ Best Practice:**
|
||||
```php
|
||||
// Component
|
||||
public function getRenderData(): ComponentRenderData
|
||||
{
|
||||
return new ComponentRenderData('product-card', [
|
||||
'priceWithTax' => $this->getPriceWithTax(),
|
||||
'formattedDate' => $this->getFormattedDate(),
|
||||
'priceDisplay' => $this->getPriceDisplay()
|
||||
]);
|
||||
}
|
||||
|
||||
private function getPriceWithTax(): float
|
||||
{
|
||||
return $this->price * 1.19;
|
||||
}
|
||||
|
||||
private function getFormattedDate(): string
|
||||
{
|
||||
return $this->created->format('d.m.Y');
|
||||
}
|
||||
|
||||
private function getPriceDisplay(): string
|
||||
{
|
||||
return '€ ' . number_format($this->getPriceWithTax(), 2, ',', '.');
|
||||
}
|
||||
```
|
||||
|
||||
```html
|
||||
<!-- Template -->
|
||||
<div class="price">{{priceDisplay}}</div>
|
||||
<div class="date">{{formattedDate}}</div>
|
||||
```
|
||||
|
||||
### Beispiel 3: CSS-Klassen basierend auf Status
|
||||
|
||||
**❌ Anti-Pattern:**
|
||||
```html
|
||||
<div class="status {{status === 'active' ? 'status-active' : 'status-inactive'}}">
|
||||
{{status}}
|
||||
</div>
|
||||
```
|
||||
|
||||
**✅ Best Practice:**
|
||||
```php
|
||||
// Component
|
||||
public function getRenderData(): ComponentRenderData
|
||||
{
|
||||
return new ComponentRenderData('status-badge', [
|
||||
'statusClass' => $this->getStatusClass(),
|
||||
'statusText' => $this->getStatusText(),
|
||||
'statusIcon' => $this->getStatusIcon()
|
||||
]);
|
||||
}
|
||||
|
||||
private function getStatusClass(): string
|
||||
{
|
||||
return match($this->status) {
|
||||
'active' => 'status-active',
|
||||
'pending' => 'status-pending',
|
||||
'inactive' => 'status-inactive',
|
||||
default => 'status-unknown'
|
||||
};
|
||||
}
|
||||
|
||||
private function getStatusText(): string
|
||||
{
|
||||
return ucfirst($this->status);
|
||||
}
|
||||
|
||||
private function getStatusIcon(): string
|
||||
{
|
||||
return match($this->status) {
|
||||
'active' => '✓',
|
||||
'pending' => '⏳',
|
||||
'inactive' => '✗',
|
||||
default => '?'
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
```html
|
||||
<!-- Template -->
|
||||
<div class="status {{statusClass}}">
|
||||
<span class="icon">{{statusIcon}}</span>
|
||||
{{statusText}}
|
||||
</div>
|
||||
```
|
||||
|
||||
### Beispiel 4: Listen mit berechneten Werten
|
||||
|
||||
**❌ Anti-Pattern:**
|
||||
```html
|
||||
{{#each items}}
|
||||
<div class="item">
|
||||
{{name}} - {{price * quantity}} €
|
||||
{{#if inStock && quantity > 0}}
|
||||
<span class="available">Available</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/each}}
|
||||
```
|
||||
|
||||
**✅ Best Practice:**
|
||||
```php
|
||||
// Component
|
||||
public function getRenderData(): ComponentRenderData
|
||||
{
|
||||
return new ComponentRenderData('order-items', [
|
||||
'items' => $this->prepareItems()
|
||||
]);
|
||||
}
|
||||
|
||||
private function prepareItems(): array
|
||||
{
|
||||
return array_map(function($item) {
|
||||
return [
|
||||
'name' => $item->name,
|
||||
'totalPrice' => $this->formatPrice($item->price * $item->quantity),
|
||||
'showAvailable' => $item->inStock && $item->quantity > 0,
|
||||
'itemClass' => $item->inStock ? 'item-available' : 'item-unavailable'
|
||||
];
|
||||
}, $this->items);
|
||||
}
|
||||
|
||||
private function formatPrice(float $price): string
|
||||
{
|
||||
return number_format($price, 2, ',', '.') . ' €';
|
||||
}
|
||||
```
|
||||
|
||||
```html
|
||||
<!-- Template -->
|
||||
{{#each items}}
|
||||
<div class="item {{itemClass}}">
|
||||
{{name}} - {{totalPrice}}
|
||||
{{#if showAvailable}}
|
||||
<span class="available">Available</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/each}}
|
||||
```
|
||||
|
||||
## LiveComponent-Spezifische Patterns
|
||||
|
||||
### Pattern 1: Event-Daten vorbereiten
|
||||
|
||||
```php
|
||||
// Component
|
||||
public function increment(): ComponentUpdate
|
||||
{
|
||||
$oldValue = $this->initialData['count'];
|
||||
$newValue = $oldValue + 1;
|
||||
|
||||
return new ComponentUpdate(
|
||||
newState: ['count' => $newValue],
|
||||
events: [
|
||||
new ComponentEvent(
|
||||
name: 'counter:changed',
|
||||
data: [
|
||||
'old_value' => $oldValue,
|
||||
'new_value' => $newValue,
|
||||
'change' => '+1',
|
||||
'isEven' => $newValue % 2 === 0,
|
||||
'isMilestone' => $newValue % 10 === 0
|
||||
]
|
||||
)
|
||||
]
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 2: Conditional Actions basierend auf State
|
||||
|
||||
```php
|
||||
// Component
|
||||
public function getRenderData(): ComponentRenderData
|
||||
{
|
||||
$count = $this->initialData['count'];
|
||||
|
||||
return new ComponentRenderData('counter', [
|
||||
'count' => $count,
|
||||
'canDecrement' => $count > 0,
|
||||
'canIncrement' => $count < 100,
|
||||
'showReset' => $count !== 0,
|
||||
'decrementClass' => $count > 0 ? 'btn-danger' : 'btn-disabled',
|
||||
'incrementClass' => $count < 100 ? 'btn-success' : 'btn-disabled'
|
||||
]);
|
||||
}
|
||||
```
|
||||
|
||||
```html
|
||||
<!-- Template -->
|
||||
<div class="counter">
|
||||
<h2>Count: {{count}}</h2>
|
||||
|
||||
{{#if canDecrement}}
|
||||
<button data-live-action="decrement" class="{{decrementClass}}">
|
||||
- Decrement
|
||||
</button>
|
||||
{{/if}}
|
||||
|
||||
{{#if canIncrement}}
|
||||
<button data-live-action="increment" class="{{incrementClass}}">
|
||||
+ Increment
|
||||
</button>
|
||||
{{/if}}
|
||||
|
||||
{{#if showReset}}
|
||||
<button data-live-action="reset" class="btn-secondary">
|
||||
Reset
|
||||
</button>
|
||||
{{/if}}
|
||||
</div>
|
||||
```
|
||||
|
||||
### Pattern 3: Cache-optimierte Datenvorbereitung
|
||||
|
||||
```php
|
||||
// Component mit Caching
|
||||
final readonly class StatsComponent implements LiveComponentContract, Cacheable
|
||||
{
|
||||
public function getRenderData(): ComponentRenderData
|
||||
{
|
||||
// Teure Berechnung einmal durchführen
|
||||
$stats = $this->computeExpensiveStats();
|
||||
|
||||
// Alle Darstellungs-Daten vorbereiten
|
||||
return new ComponentRenderData('stats', [
|
||||
'stats' => $stats,
|
||||
'totalUsers' => number_format($stats['total_users'], 0, ',', '.'),
|
||||
'activeSessionsText' => $this->getActiveSessionsText($stats['active_sessions']),
|
||||
'revenueFormatted' => $this->formatRevenue($stats['revenue']),
|
||||
'showAlert' => $stats['total_users'] > 5000,
|
||||
'alertClass' => $stats['total_users'] > 5000 ? 'alert-warning' : 'alert-info'
|
||||
]);
|
||||
}
|
||||
|
||||
private function getActiveSessionsText(int $count): string
|
||||
{
|
||||
return "{$count} active session" . ($count !== 1 ? 's' : '');
|
||||
}
|
||||
|
||||
private function formatRevenue(int $revenue): string
|
||||
{
|
||||
return '€ ' . number_format($revenue, 2, ',', '.');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Template-System Referenz
|
||||
|
||||
### Unterstützte Syntax
|
||||
|
||||
**Variable Substitution:**
|
||||
```html
|
||||
{{variableName}}
|
||||
{{object.property}}
|
||||
{{array.0.name}}
|
||||
```
|
||||
|
||||
**Conditionals:**
|
||||
```html
|
||||
{{#if condition}}
|
||||
Content when true
|
||||
{{/if}}
|
||||
|
||||
{{#if condition}}
|
||||
True content
|
||||
{{else}}
|
||||
False content
|
||||
{{/if}}
|
||||
```
|
||||
|
||||
**Loops:**
|
||||
```html
|
||||
{{#each items}}
|
||||
{{name}} - {{value}}
|
||||
{{/each}}
|
||||
```
|
||||
|
||||
**Nested Templates:**
|
||||
```html
|
||||
{{#if user}}
|
||||
{{#each user.orders}}
|
||||
<div>Order {{id}}: {{total}}</div>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
```
|
||||
|
||||
## Zusammenfassung
|
||||
|
||||
**Goldene Regeln:**
|
||||
|
||||
1. ✅ **Bereite alle Daten im Component vor** - keine Logik im Template
|
||||
2. ✅ **Verwende aussagekräftige Property-Namen** - `showAdminBadge` statt `isAdminAndActive`
|
||||
3. ✅ **Formatiere Daten in PHP** - `priceFormatted` statt Berechnung im Template
|
||||
4. ✅ **CSS-Klassen vorbereiten** - `statusClass` statt Conditional im Template
|
||||
5. ✅ **Boolean Flags für Conditionals** - `hasOrders` statt `orders.length > 0`
|
||||
6. ✅ **Listen vorverarbeiten** - Arrays mit allen Display-Daten vorbereiten
|
||||
7. ✅ **Teste Component-Logik** - nicht Template-Rendering
|
||||
|
||||
**Das Template-System ist bewusst einfach gehalten, um saubere Separation of Concerns zu fördern.**
|
||||
@@ -1,683 +0,0 @@
|
||||
# LiveComponents Caching System
|
||||
|
||||
Umfassende Dokumentation des LiveComponents Caching-Systems mit Performance Metrics.
|
||||
|
||||
## Übersicht
|
||||
|
||||
Das LiveComponents Caching-System bietet **mehrschichtige Caching-Strategien** für optimale Performance:
|
||||
|
||||
- **Component State Cache**: ~70% schnellere Initialisierung
|
||||
- **Slot Content Cache**: ~60% schnellere Slot-Resolution
|
||||
- **Template Fragment Cache**: ~80% schnellere Template-Rendering
|
||||
|
||||
Alle Caches unterstützen **automatische Performance-Metriken** über Decorator Pattern.
|
||||
|
||||
## Architektur
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ LiveComponent Handler │
|
||||
│ (Orchestriert Component Lifecycle) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Metrics-Aware Cache Decorators │
|
||||
│ (Transparente Performance-Metriken-Sammlung) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ ComponentStateCache │ SlotContentCache │ TemplateFragmentCache│
|
||||
│ (Core Cache Implementations) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Framework Cache Layer (SmartCache) │
|
||||
│ (File/Redis/Database Driver) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Komponenten
|
||||
|
||||
### 1. Component State Cache
|
||||
|
||||
**Zweck**: Cached Component State zwischen Requests für schnellere Rehydration.
|
||||
|
||||
**Performance**: ~70% schnellere Component-Initialisierung
|
||||
|
||||
**Verwendung**:
|
||||
|
||||
```php
|
||||
use App\Framework\LiveComponents\Cache\ComponentStateCache;
|
||||
use App\Framework\Core\ValueObjects\Duration;
|
||||
|
||||
$cache = $container->get(ComponentStateCache::class);
|
||||
|
||||
// State speichern
|
||||
$cache->store(
|
||||
componentId: $componentId,
|
||||
state: $componentState,
|
||||
ttl: Duration::fromHours(1)
|
||||
);
|
||||
|
||||
// State abrufen
|
||||
$cachedState = $cache->retrieve($componentId, $currentState);
|
||||
|
||||
if ($cachedState !== null) {
|
||||
// Cached state verwenden - ~70% schneller
|
||||
$component->hydrateFromCache($cachedState);
|
||||
} else {
|
||||
// Fresh initialization
|
||||
$component->initialize($freshState);
|
||||
}
|
||||
```
|
||||
|
||||
**Auto-TTL basierend auf Component-Typ**:
|
||||
```php
|
||||
// Auto-optimierte TTL
|
||||
$cache->storeWithAutoTTL(
|
||||
componentId: $componentId,
|
||||
state: $state,
|
||||
componentType: 'counter' // 5 Minuten TTL
|
||||
);
|
||||
|
||||
/*
|
||||
TTL-Strategien:
|
||||
- counter, timer: 5 Minuten (frequent updates)
|
||||
- chart, datatable: 30 Minuten (moderate updates)
|
||||
- card, modal, layout: 2 Stunden (static-ish)
|
||||
*/
|
||||
```
|
||||
|
||||
### 2. Slot Content Cache
|
||||
|
||||
**Zweck**: Cached resolved Slot-Content für wiederverwendbare Komponenten.
|
||||
|
||||
**Performance**: ~60% schnellere Slot-Resolution
|
||||
|
||||
**Verwendung**:
|
||||
```php
|
||||
use App\Framework\LiveComponents\Cache\SlotContentCache;
|
||||
|
||||
$cache = $container->get(SlotContentCache::class);
|
||||
|
||||
// Single Slot speichern
|
||||
$cache->storeResolvedContent(
|
||||
componentId: $componentId,
|
||||
slotName: 'header',
|
||||
resolvedContent: $renderedHeaderHtml,
|
||||
ttl: Duration::fromMinutes(30)
|
||||
);
|
||||
|
||||
// Slot abrufen
|
||||
$cached = $cache->getResolvedContent($componentId, 'header');
|
||||
|
||||
if ($cached !== null) {
|
||||
return $cached; // ~60% schneller
|
||||
}
|
||||
```
|
||||
|
||||
**Batch Operations für Multiple Slots**:
|
||||
```php
|
||||
// Batch store - alle Slots in einem Call
|
||||
$cache->storeBatch(
|
||||
componentId: $componentId,
|
||||
slots: [
|
||||
'header' => $headerHtml,
|
||||
'footer' => $footerHtml,
|
||||
'sidebar' => $sidebarHtml
|
||||
],
|
||||
ttl: Duration::fromHours(1)
|
||||
);
|
||||
|
||||
// Batch retrieve
|
||||
$cachedSlots = $cache->getBatch($componentId, ['header', 'footer', 'sidebar']);
|
||||
|
||||
if (isset($cachedSlots['header'])) {
|
||||
// Header aus Cache
|
||||
}
|
||||
```
|
||||
|
||||
**Content-Hash Based Invalidation**:
|
||||
```php
|
||||
// Automatische Invalidierung bei Content-Änderung
|
||||
$cache->storeWithContentHash(
|
||||
componentId: $componentId,
|
||||
slotName: 'dynamic-content',
|
||||
resolvedContent: $dynamicHtml,
|
||||
ttl: Duration::fromHours(2)
|
||||
);
|
||||
|
||||
// Wenn Content sich ändert, ändert sich Hash → alter Cache ungültig
|
||||
```
|
||||
|
||||
### 3. Template Fragment Cache
|
||||
|
||||
**Zweck**: Cached gerenderte Template-Fragmente für wiederverwendbare UI-Komponenten.
|
||||
|
||||
**Performance**: ~80% schnellere Template-Rendering
|
||||
|
||||
**Verwendung**:
|
||||
```php
|
||||
use App\Framework\LiveComponents\Cache\TemplateFragmentCache;
|
||||
|
||||
$cache = $container->get(TemplateFragmentCache::class);
|
||||
|
||||
// Template Fragment speichern
|
||||
$cache->store(
|
||||
componentType: 'card',
|
||||
renderedHtml: $renderedCardHtml,
|
||||
data: ['title' => 'User Profile', 'userId' => 123],
|
||||
variant: 'default',
|
||||
ttl: Duration::fromHours(2)
|
||||
);
|
||||
|
||||
// Template abrufen
|
||||
$cached = $cache->get(
|
||||
componentType: 'card',
|
||||
data: ['title' => 'User Profile', 'userId' => 123],
|
||||
variant: 'default'
|
||||
);
|
||||
```
|
||||
|
||||
**Remember Pattern**:
|
||||
```php
|
||||
// Eleganter: get from cache or execute callback
|
||||
$renderedHtml = $cache->remember(
|
||||
componentType: 'card',
|
||||
data: $templateData,
|
||||
callback: fn() => $this->templateRenderer->render('card.view.php', $templateData),
|
||||
variant: 'compact',
|
||||
ttl: Duration::fromHours(1)
|
||||
);
|
||||
```
|
||||
|
||||
**Static Templates** (keine Data-Variationen):
|
||||
```php
|
||||
// Für Layouts, Header, Footer - komplett statisch
|
||||
$cache->storeStatic(
|
||||
componentType: 'layout',
|
||||
renderedHtml: $layoutShellHtml,
|
||||
variant: 'default',
|
||||
ttl: Duration::fromHours(24) // Lange TTL
|
||||
);
|
||||
|
||||
$layoutHtml = $cache->getStatic('layout', 'default');
|
||||
```
|
||||
|
||||
**Auto-TTL basierend auf Component-Typ**:
|
||||
```php
|
||||
$cache->storeWithAutoTTL(
|
||||
componentType: 'header', // 24h TTL (static)
|
||||
renderedHtml: $headerHtml,
|
||||
data: $headerData,
|
||||
variant: 'default'
|
||||
);
|
||||
```
|
||||
|
||||
### 4. Cache Invalidation Strategy
|
||||
|
||||
**Zweck**: Koordinierte Cache-Invalidierung über alle Cache-Layer.
|
||||
|
||||
**Verwendung**:
|
||||
```php
|
||||
use App\Framework\LiveComponents\Cache\CacheInvalidationStrategy;
|
||||
|
||||
$strategy = $container->get(CacheInvalidationStrategy::class);
|
||||
|
||||
// Komplettes Component invalidieren (State + Slots)
|
||||
$result = $strategy->invalidateComponent($componentId);
|
||||
|
||||
if ($result->success) {
|
||||
// ['state', 'slots'] invalidiert
|
||||
}
|
||||
|
||||
// Nur Slots invalidieren
|
||||
$result = $strategy->invalidateComponentSlots($componentId);
|
||||
|
||||
// Einzelner Slot
|
||||
$result = $strategy->invalidateSlot($componentId, 'header');
|
||||
|
||||
// Template Type (alle Templates von Typ)
|
||||
$result = $strategy->invalidateComponentType('card');
|
||||
|
||||
// Template Variant
|
||||
$result = $strategy->invalidateVariant('card', 'compact');
|
||||
```
|
||||
|
||||
**Smart State-Change Invalidation**:
|
||||
```php
|
||||
// Invalidiert nur betroffene Caches
|
||||
$result = $strategy->invalidateOnStateChange(
|
||||
componentId: $componentId,
|
||||
oldState: $oldComponentState,
|
||||
newState: $newComponentState
|
||||
);
|
||||
|
||||
/*
|
||||
Prüft State Keys die Slot-Rendering betreffen:
|
||||
- sidebarWidth
|
||||
- sidebarCollapsed
|
||||
- isOpen
|
||||
- padding
|
||||
- theme
|
||||
- variant
|
||||
|
||||
Nur wenn diese sich ändern → Slot Cache auch invalidieren
|
||||
*/
|
||||
```
|
||||
|
||||
**Bulk Invalidation**:
|
||||
```php
|
||||
// Viele Components auf einmal
|
||||
$componentIds = [$id1, $id2, $id3, ...];
|
||||
$result = $strategy->invalidateBulk($componentIds);
|
||||
|
||||
// Result enthält Success-Count
|
||||
// reason: "bulk_invalidation:150/200" (150 von 200 erfolgreich)
|
||||
```
|
||||
|
||||
**Nuclear Option** (mit Vorsicht!):
|
||||
```php
|
||||
// ALLE LiveComponent Caches löschen
|
||||
$result = $strategy->clearAll();
|
||||
|
||||
// Use Cases:
|
||||
// - Deployment
|
||||
// - Major Framework Updates
|
||||
// - Debug/Development
|
||||
```
|
||||
|
||||
## Performance Metrics System
|
||||
|
||||
### Metrics Collector Setup
|
||||
|
||||
```php
|
||||
use App\Framework\LiveComponents\Cache\CacheMetricsCollector;
|
||||
use App\Framework\LiveComponents\Cache\MetricsAwareComponentStateCache;
|
||||
|
||||
// DI Container Setup
|
||||
$metricsCollector = new CacheMetricsCollector();
|
||||
$container->singleton(CacheMetricsCollector::class, $metricsCollector);
|
||||
|
||||
// Metrics-Aware Caches registrieren
|
||||
$stateCache = new ComponentStateCache($frameworkCache);
|
||||
$metricAwareStateCache = new MetricsAwareComponentStateCache(
|
||||
$stateCache,
|
||||
$metricsCollector
|
||||
);
|
||||
|
||||
$container->singleton(ComponentStateCache::class, $metricAwareStateCache);
|
||||
```
|
||||
|
||||
### Metrics Abrufen
|
||||
|
||||
```php
|
||||
// Metrics für spezifischen Cache-Typ
|
||||
$stateMetrics = $metricsCollector->getMetrics(CacheType::STATE);
|
||||
|
||||
echo "State Cache Hit Rate: " . $stateMetrics->hitRate->format(2); // "85.50%"
|
||||
echo "Average Lookup Time: " . $stateMetrics->averageLookupTimeMs . "ms";
|
||||
echo "Performance Grade: " . $stateMetrics->getPerformanceGrade(); // "A"
|
||||
```
|
||||
|
||||
### Performance Summary
|
||||
|
||||
```php
|
||||
$summary = $metricsCollector->getSummary();
|
||||
|
||||
/*
|
||||
[
|
||||
'overall' => [
|
||||
'cache_type' => 'merged',
|
||||
'hits' => 1500,
|
||||
'misses' => 200,
|
||||
'hit_rate' => '88.24%',
|
||||
'miss_rate' => '11.76%',
|
||||
'average_lookup_time_ms' => 0.523,
|
||||
'total_size' => 450,
|
||||
'invalidations' => 15,
|
||||
'performance_grade' => 'A'
|
||||
],
|
||||
'by_type' => [
|
||||
'state' => [...],
|
||||
'slot' => [...],
|
||||
'template' => [...]
|
||||
],
|
||||
'performance_assessment' => [
|
||||
'state_cache' => [
|
||||
'target' => '70.0%',
|
||||
'actual' => '85.50%',
|
||||
'meets_target' => true,
|
||||
'grade' => 'A'
|
||||
],
|
||||
'slot_cache' => [
|
||||
'target' => '60.0%',
|
||||
'actual' => '72.30%',
|
||||
'meets_target' => true,
|
||||
'grade' => 'C'
|
||||
],
|
||||
'template_cache' => [
|
||||
'target' => '80.0%',
|
||||
'actual' => '91.20%',
|
||||
'meets_target' => true,
|
||||
'grade' => 'A'
|
||||
],
|
||||
'overall_grade' => 'A'
|
||||
]
|
||||
]
|
||||
*/
|
||||
```
|
||||
|
||||
### Performance Monitoring
|
||||
|
||||
```php
|
||||
// Check ob Caches underperforming sind
|
||||
if ($metricsCollector->hasPerformanceIssues()) {
|
||||
$warnings = $metricsCollector->getPerformanceWarnings();
|
||||
|
||||
foreach ($warnings as $warning) {
|
||||
$logger->warning($warning);
|
||||
// "State cache hit rate (65.00%) below target (70.0%)"
|
||||
}
|
||||
|
||||
// Alert Operations Team oder Auto-Tuning triggern
|
||||
}
|
||||
```
|
||||
|
||||
### Metrics Export für Monitoring Tools
|
||||
|
||||
```php
|
||||
// Export für Prometheus, Grafana, etc.
|
||||
$export = $metricsCollector->export();
|
||||
|
||||
/*
|
||||
[
|
||||
'timestamp' => 1678901234,
|
||||
'metrics' => [
|
||||
'overall' => [...],
|
||||
'by_type' => [...],
|
||||
'performance_assessment' => [...]
|
||||
]
|
||||
]
|
||||
*/
|
||||
|
||||
// An Monitoring-Service senden
|
||||
$monitoringService->sendMetrics($export);
|
||||
```
|
||||
|
||||
## Integration Beispiele
|
||||
|
||||
### Beispiel 1: Card Component mit Caching
|
||||
|
||||
```php
|
||||
final readonly class CardComponent
|
||||
{
|
||||
public function __construct(
|
||||
private ComponentStateCache $stateCache,
|
||||
private TemplateFragmentCache $templateCache,
|
||||
private TemplateRenderer $renderer
|
||||
) {}
|
||||
|
||||
public function render(ComponentId $componentId, ComponentState $state): string
|
||||
{
|
||||
// 1. Try State Cache
|
||||
$cachedState = $this->stateCache->retrieve($componentId, $state);
|
||||
|
||||
if ($cachedState !== null) {
|
||||
$state = $cachedState; // ~70% schneller
|
||||
} else {
|
||||
$this->stateCache->storeWithAutoTTL($componentId, $state, 'card');
|
||||
}
|
||||
|
||||
// 2. Try Template Cache mit Remember Pattern
|
||||
return $this->templateCache->remember(
|
||||
componentType: 'card',
|
||||
data: $state->toArray(),
|
||||
callback: fn() => $this->renderer->render('card.view.php', $state->toArray()),
|
||||
variant: $state->get('variant', 'default'),
|
||||
ttl: Duration::fromHours(2)
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Beispiel 2: Nested Component mit Slot Caching
|
||||
|
||||
```php
|
||||
final readonly class LayoutComponent
|
||||
{
|
||||
public function __construct(
|
||||
private SlotContentCache $slotCache,
|
||||
private SlotManager $slotManager
|
||||
) {}
|
||||
|
||||
public function render(ComponentId $componentId, array $slots): string
|
||||
{
|
||||
// 1. Check Slot Cache - Batch Operation
|
||||
$cachedSlots = $this->slotCache->getBatch(
|
||||
$componentId,
|
||||
array_keys($slots)
|
||||
);
|
||||
|
||||
$resolvedSlots = [];
|
||||
|
||||
foreach ($slots as $slotName => $slotContent) {
|
||||
// 2. Use cached oder resolve fresh
|
||||
if (isset($cachedSlots[$slotName])) {
|
||||
$resolvedSlots[$slotName] = $cachedSlots[$slotName];
|
||||
} else {
|
||||
$resolved = $this->slotManager->resolveSlot($slotName, $slotContent);
|
||||
$resolvedSlots[$slotName] = $resolved;
|
||||
|
||||
// 3. Cache for next time
|
||||
$this->slotCache->storeResolvedContent(
|
||||
$componentId,
|
||||
$slotName,
|
||||
$resolved,
|
||||
Duration::fromHours(1)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->renderLayout($resolvedSlots);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Beispiel 3: Dynamic Component mit Smart Invalidation
|
||||
|
||||
```php
|
||||
final readonly class DynamicFormComponent
|
||||
{
|
||||
public function __construct(
|
||||
private ComponentStateCache $stateCache,
|
||||
private CacheInvalidationStrategy $invalidationStrategy
|
||||
) {}
|
||||
|
||||
public function updateState(
|
||||
ComponentId $componentId,
|
||||
ComponentState $oldState,
|
||||
ComponentState $newState
|
||||
): void {
|
||||
// 1. Smart invalidation - nur betroffene Caches
|
||||
$result = $this->invalidationStrategy->invalidateOnStateChange(
|
||||
$componentId,
|
||||
$oldState,
|
||||
$newState
|
||||
);
|
||||
|
||||
// 2. Store new state
|
||||
$this->stateCache->storeWithAutoTTL(
|
||||
$componentId,
|
||||
$newState,
|
||||
'dynamic-form'
|
||||
);
|
||||
|
||||
// Log invalidation result
|
||||
$this->logger->info('Cache invalidated', [
|
||||
'component_id' => $componentId->toString(),
|
||||
'invalidated' => $result->invalidated,
|
||||
'reason' => $result->reason
|
||||
]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Best Practices
|
||||
|
||||
### 1. Use Auto-TTL Methods
|
||||
|
||||
```php
|
||||
// ✅ Good - Auto-optimierte TTL
|
||||
$cache->storeWithAutoTTL($componentId, $state, 'counter');
|
||||
|
||||
// ❌ Avoid - Manual TTL kann suboptimal sein
|
||||
$cache->store($componentId, $state, Duration::fromHours(24)); // Zu lange für counter
|
||||
```
|
||||
|
||||
### 2. Batch Operations für Multiple Slots
|
||||
|
||||
```php
|
||||
// ✅ Good - Batch Operation
|
||||
$cache->storeBatch($componentId, [
|
||||
'header' => $headerHtml,
|
||||
'footer' => $footerHtml,
|
||||
'sidebar' => $sidebarHtml
|
||||
]);
|
||||
|
||||
// ❌ Avoid - Einzelne Calls
|
||||
$cache->storeResolvedContent($componentId, 'header', $headerHtml);
|
||||
$cache->storeResolvedContent($componentId, 'footer', $footerHtml);
|
||||
$cache->storeResolvedContent($componentId, 'sidebar', $sidebarHtml);
|
||||
```
|
||||
|
||||
### 3. Remember Pattern für Templates
|
||||
|
||||
```php
|
||||
// ✅ Good - Remember Pattern
|
||||
$html = $cache->remember($type, $data, fn() => $this->render($data));
|
||||
|
||||
// ❌ Avoid - Manual get/store
|
||||
$html = $cache->get($type, $data);
|
||||
if ($html === null) {
|
||||
$html = $this->render($data);
|
||||
$cache->store($type, $html, $data);
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Smart Invalidation
|
||||
|
||||
```php
|
||||
// ✅ Good - Smart invalidation
|
||||
$strategy->invalidateOnStateChange($id, $old, $new);
|
||||
|
||||
// ❌ Avoid - Always invalidate all
|
||||
$strategy->invalidateComponent($id); // Invalidiert auch wenn nicht nötig
|
||||
```
|
||||
|
||||
### 5. Content-Hash Based Caching
|
||||
|
||||
```php
|
||||
// ✅ Good - Auto-invalidation bei Content-Änderung
|
||||
$cache->storeWithContentHash($id, $slotName, $content);
|
||||
|
||||
// ❌ Avoid - Manual invalidation tracking
|
||||
$cache->storeResolvedContent($id, $slotName, $content);
|
||||
// ... später manuell invalidieren müssen
|
||||
```
|
||||
|
||||
## Monitoring Dashboard Beispiel
|
||||
|
||||
```php
|
||||
final readonly class CacheMonitoringController
|
||||
{
|
||||
public function dashboard(Request $request): ViewResult
|
||||
{
|
||||
$summary = $this->metricsCollector->getSummary();
|
||||
|
||||
return new ViewResult('cache-dashboard', [
|
||||
'overall_stats' => $summary['overall'],
|
||||
'state_cache' => $summary['by_type']['state'],
|
||||
'slot_cache' => $summary['by_type']['slot'],
|
||||
'template_cache' => $summary['by_type']['template'],
|
||||
'assessment' => $summary['performance_assessment'],
|
||||
'warnings' => $this->metricsCollector->getPerformanceWarnings(),
|
||||
'has_issues' => $this->metricsCollector->hasPerformanceIssues()
|
||||
]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Problem: Niedrige Hit Rate
|
||||
|
||||
**Symptom**: Hit Rate < Target (70%, 60%, 80%)
|
||||
|
||||
**Lösungen**:
|
||||
1. **TTL zu kurz**: Erhöhe TTL mit Auto-TTL Methods
|
||||
2. **Zu viele Invalidations**: Prüfe Smart Invalidation Logik
|
||||
3. **Cache Size zu klein**: Erhöhe Cache Capacity
|
||||
4. **Variant Explosion**: Reduziere Template Variants
|
||||
|
||||
### Problem: Hohe Average Lookup Time
|
||||
|
||||
**Symptom**: Lookup Time > 1ms
|
||||
|
||||
**Lösungen**:
|
||||
1. **Cache Driver langsam**: Wechsle zu Redis statt File Cache
|
||||
2. **Große Payloads**: Komprimiere cached Data
|
||||
3. **Netzwerk Latency**: Use lokalen Cache statt remote
|
||||
|
||||
### Problem: Memory Issues
|
||||
|
||||
**Symptom**: Out of Memory Errors
|
||||
|
||||
**Lösungen**:
|
||||
1. **Cache Size explodiert**: Implementiere LRU Eviction
|
||||
2. **TTL zu lange**: Reduziere TTL für rarely-used Components
|
||||
3. **Zu viele Variants**: Consolidate Template Variants
|
||||
|
||||
## Performance Benchmarks
|
||||
|
||||
Typische Performance-Werte im Production Environment:
|
||||
|
||||
| Operation | Without Cache | With Cache | Improvement |
|
||||
|-----------|---------------|------------|-------------|
|
||||
| Component Init | 5.2ms | 1.5ms | **71% faster** |
|
||||
| Slot Resolution | 3.8ms | 1.5ms | **61% faster** |
|
||||
| Template Render | 12.4ms | 2.1ms | **83% faster** |
|
||||
| Full Component | 21.4ms | 5.1ms | **76% faster** |
|
||||
|
||||
**Cache Hit Rates** (Production):
|
||||
- State Cache: 85-90%
|
||||
- Slot Cache: 70-80%
|
||||
- Template Cache: 90-95%
|
||||
|
||||
**Memory Usage**:
|
||||
- Per Cached State: ~2KB
|
||||
- Per Cached Slot: ~1KB
|
||||
- Per Cached Template: ~5KB
|
||||
- Total (10k components): ~80MB
|
||||
|
||||
## Zusammenfassung
|
||||
|
||||
Das LiveComponents Caching-System bietet:
|
||||
|
||||
✅ **3-Layer Caching** (State, Slot, Template)
|
||||
✅ **~70-80% Performance-Steigerung**
|
||||
✅ **Automatische Metrics** via Decorator Pattern
|
||||
✅ **Smart Invalidation** basierend auf State Changes
|
||||
✅ **Flexible TTL-Strategien**
|
||||
✅ **Content-Hash Based Auto-Invalidation**
|
||||
✅ **Batch Operations** für Efficiency
|
||||
✅ **Remember Pattern** für einfache Nutzung
|
||||
✅ **Performance Monitoring** out-of-the-box
|
||||
✅ **Production-Ready** mit Type Safety
|
||||
|
||||
Framework-konform:
|
||||
- Value Objects (CacheType, CacheMetrics, Percentage, Hash)
|
||||
- Readonly Classes
|
||||
- Immutable State
|
||||
- Decorator Pattern
|
||||
- Type Safety everywhere
|
||||
@@ -1,236 +0,0 @@
|
||||
# LiveComponents DOM Badges System
|
||||
|
||||
**Visual component overlays for real-time monitoring and debugging.**
|
||||
|
||||
## Overview
|
||||
|
||||
The DOM Badges system provides visual overlays on LiveComponent elements, displaying component IDs, names, and activity counters directly on the page. This enables developers to quickly identify and debug components without opening the DevTools panel.
|
||||
|
||||
## Features
|
||||
|
||||
### Visual Component Identification
|
||||
- **Component Name**: Shows the component's display name
|
||||
- **Component ID**: Truncated ID (first 8 characters) for identification
|
||||
- **Icon Indicator**: ⚡ icon for visual consistency
|
||||
|
||||
### Real-Time Activity Tracking
|
||||
- **Action Counter**: Tracks number of actions executed on the component
|
||||
- **Activity Animation**: Badge pulses with green highlight when actions are triggered
|
||||
- **Live Updates**: Counter updates in real-time as actions execute
|
||||
|
||||
### Interactive Features
|
||||
- **Click to Focus**: Click badge to open DevTools and focus on that component
|
||||
- **Hover Highlight**: Hover over badge to highlight the component element with blue outline
|
||||
- **DevTools Integration**: Clicking badge scrolls to component in tree and expands details
|
||||
|
||||
### Badge Management
|
||||
- **Toggle Visibility**: Badge button in DevTools header to show/hide all badges
|
||||
- **Auto-Positioning**: Badges automatically position at top-left of component elements
|
||||
- **Dynamic Updates**: Badges track DOM changes and update positions accordingly
|
||||
|
||||
## Architecture
|
||||
|
||||
### Badge Data Structure
|
||||
```javascript
|
||||
{
|
||||
badge: HTMLElement, // Badge DOM element
|
||||
element: HTMLElement, // Component element
|
||||
actionCount: number // Number of actions executed
|
||||
}
|
||||
```
|
||||
|
||||
### Badge Lifecycle
|
||||
1. **Initialization**: `initializeDomBadges()` sets up MutationObserver
|
||||
2. **Creation**: `createDomBadge()` creates badge for each component
|
||||
3. **Positioning**: `positionBadge()` calculates fixed position
|
||||
4. **Updates**: `updateBadgeActivity()` increments counter and triggers animation
|
||||
5. **Cleanup**: `cleanupRemovedBadges()` removes badges for destroyed components
|
||||
|
||||
## Usage
|
||||
|
||||
### Automatic Badge Creation
|
||||
Badges are automatically created when:
|
||||
- DevTools is opened and components exist in DOM
|
||||
- New components are added to the page (via MutationObserver)
|
||||
- Badge visibility is toggled on
|
||||
|
||||
### Toggle Badge Visibility
|
||||
```javascript
|
||||
// Via DevTools UI: Click "⚡ Badges" button in header
|
||||
|
||||
// Programmatically
|
||||
devTools.toggleBadges();
|
||||
```
|
||||
|
||||
### Badge Interactions
|
||||
|
||||
**Click Badge**:
|
||||
- Opens DevTools if closed
|
||||
- Switches to Components tab
|
||||
- Scrolls to component in tree
|
||||
- Expands component details
|
||||
|
||||
**Hover Badge**:
|
||||
- Shows blue outline around component element
|
||||
- Highlights component boundaries for easy identification
|
||||
|
||||
## Styling
|
||||
|
||||
### Badge Appearance
|
||||
- **Background**: Dark semi-transparent (#1e1e1e with 95% opacity)
|
||||
- **Border**: Blue (#007acc) that changes to green (#4ec9b0) on hover
|
||||
- **Backdrop Filter**: 4px blur for modern glass-morphism effect
|
||||
- **Box Shadow**: Subtle shadow for depth
|
||||
|
||||
### Active Animation
|
||||
```css
|
||||
@keyframes badge-pulse {
|
||||
0% { transform: scale(1); box-shadow: default }
|
||||
50% { transform: scale(1.1); box-shadow: green glow }
|
||||
100% { transform: scale(1); box-shadow: default }
|
||||
}
|
||||
```
|
||||
|
||||
### Color Coding
|
||||
- **Component Name**: Blue (#569cd6) - VS Code variable color
|
||||
- **Component ID**: Gray (#858585) - subdued identifier
|
||||
- **Action Count**: Yellow (#dcdcaa) - VS Code function color
|
||||
- **Active Border**: Green (#4ec9b0) - VS Code string color
|
||||
|
||||
## Technical Implementation
|
||||
|
||||
### MutationObserver Integration
|
||||
```javascript
|
||||
const observer = new MutationObserver(() => {
|
||||
this.updateDomBadges();
|
||||
});
|
||||
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
attributeFilter: ['data-component-id']
|
||||
});
|
||||
```
|
||||
|
||||
### Position Calculation
|
||||
Badges use `fixed` positioning with `getBoundingClientRect()`:
|
||||
```javascript
|
||||
const rect = element.getBoundingClientRect();
|
||||
badge.style.position = 'fixed';
|
||||
badge.style.top = `${rect.top + window.scrollY}px`;
|
||||
badge.style.left = `${rect.left + window.scrollX}px`;
|
||||
```
|
||||
|
||||
### Activity Tracking Integration
|
||||
```javascript
|
||||
// In logAction() method
|
||||
logAction(componentId, ...) {
|
||||
// ... log action ...
|
||||
|
||||
// Update badge
|
||||
this.updateBadgeActivity(componentId);
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Efficient Updates
|
||||
- **Deduplication**: Checks if badge already exists before creating
|
||||
- **Position Updates**: Only updates position for existing badges
|
||||
- **Cleanup**: Removes badges for destroyed components to prevent memory leaks
|
||||
|
||||
### Throttled DOM Operations
|
||||
- **MutationObserver**: Batches DOM changes
|
||||
- **Single Reflow**: Badge creation/positioning minimizes layout thrashing
|
||||
- **Display None**: Hidden badges use `display: none` instead of removal
|
||||
|
||||
## Best Practices
|
||||
|
||||
### When to Use Badges
|
||||
✅ **Good Use Cases**:
|
||||
- Debugging component layout and positioning
|
||||
- Identifying which components are active on a page
|
||||
- Tracking component action frequency
|
||||
- Visual confirmation of component presence
|
||||
|
||||
❌ **Avoid**:
|
||||
- Production environments (only enabled in development)
|
||||
- Pages with 50+ components (visual clutter)
|
||||
- When precise layout debugging is needed (use browser DevTools instead)
|
||||
|
||||
### Badge Visibility Toggle
|
||||
- Keep badges **enabled** when actively debugging components
|
||||
- **Disable** badges when focusing on other DevTools tabs
|
||||
- Badges automatically show when DevTools opens
|
||||
|
||||
## Integration with DevTools Features
|
||||
|
||||
### Component Tree
|
||||
- Clicking badge navigates to component in tree
|
||||
- Badge highlights component row with blue background
|
||||
- Component details automatically expand
|
||||
|
||||
### Action Log
|
||||
- Badge counter matches action log entries
|
||||
- Activity animation syncs with action execution
|
||||
- Both show real-time component behavior
|
||||
|
||||
### Event System
|
||||
- Badges react to component lifecycle events
|
||||
- Auto-created on `component:initialized`
|
||||
- Auto-removed on `component:destroyed`
|
||||
|
||||
## Keyboard Shortcuts
|
||||
|
||||
No direct keyboard shortcuts for badges, but:
|
||||
- **Ctrl+Shift+D**: Toggle DevTools (shows/creates badges)
|
||||
- **Click Badge**: Focus component in DevTools
|
||||
|
||||
## Browser Compatibility
|
||||
|
||||
- **Modern Browsers**: Full support (Chrome, Firefox, Safari, Edge)
|
||||
- **MutationObserver**: Required (IE11+ supported)
|
||||
- **CSS Animations**: Fallback to no animation on older browsers
|
||||
- **Backdrop Filter**: Graceful degradation without blur effect
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Badges Not Appearing
|
||||
1. Check if DevTools is enabled (`data-env="development"`)
|
||||
2. Verify components have `data-component-id` attribute
|
||||
3. Ensure badges are enabled (check button opacity in header)
|
||||
4. Look for JavaScript errors in console
|
||||
|
||||
### Badges in Wrong Position
|
||||
1. Check if component element has proper layout (not `display: none`)
|
||||
2. Verify parent containers don't have `transform` CSS
|
||||
3. Badge position updates on DOM mutations
|
||||
4. Manually refresh badges with toggle button
|
||||
|
||||
### Performance Issues
|
||||
1. Reduce number of active components
|
||||
2. Disable badges when not actively debugging
|
||||
3. Check MutationObserver frequency in console
|
||||
4. Consider using Components tab instead for many components
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential improvements for DOM badges:
|
||||
- **State Preview**: Show current component state in badge tooltip
|
||||
- **Network Indicator**: Visual indicator when component is loading
|
||||
- **Error State**: Red badge when component has errors
|
||||
- **Badge Groups**: Collapsible badges for nested components
|
||||
- **Custom Badge Colors**: User-configurable color schemes
|
||||
- **Badge Size Options**: Small/Medium/Large badge sizes
|
||||
- **Filter by Type**: Show only specific component types
|
||||
|
||||
## Summary
|
||||
|
||||
DOM badges provide a powerful visual debugging tool that complements the DevTools overlay. They enable:
|
||||
- **Instant Component Identification**: See what's on the page without opening DevTools
|
||||
- **Real-Time Activity Monitoring**: Track action execution as it happens
|
||||
- **Quick Navigation**: Click to jump to component in DevTools
|
||||
- **Visual Feedback**: Animations and highlights for enhanced UX
|
||||
|
||||
The badge system is designed to be non-intrusive, performant, and seamlessly integrated with the LiveComponent lifecycle and DevTools features.
|
||||
@@ -1,699 +0,0 @@
|
||||
# LiveComponents Implementation Plan
|
||||
|
||||
**Datum**: 2025-10-07
|
||||
**Status**: Draft für Review
|
||||
|
||||
## Executive Summary
|
||||
|
||||
Dieser Plan strukturiert die vorgeschlagenen Verbesserungen für das LiveComponents-Modul in umsetzbare Phasen mit klaren Prioritäten basierend auf Impact, Aufwand und Framework-Compliance.
|
||||
|
||||
---
|
||||
|
||||
## Priorisierungs-Matrix
|
||||
|
||||
### Scoring-System
|
||||
- **Impact**: 1-5 (1=niedrig, 5=kritisch)
|
||||
- **Effort**: 1-5 (1=wenige Stunden, 5=mehrere Wochen)
|
||||
- **Framework Compliance**: 1-5 (1=neutral, 5=essentiell für Framework-Patterns)
|
||||
- **Priority Score**: (Impact × 2 + Framework Compliance - Effort) / 3
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Critical Foundation (P0 - MUST HAVE)
|
||||
|
||||
### 1.1 State-Validierung & Serialization
|
||||
**Impact**: 5 | **Effort**: 2 | **Framework Compliance**: 5 | **Priority**: 6.0
|
||||
|
||||
**Problem**:
|
||||
- Kein Schema/Validierung für State
|
||||
- Serialization-Fehler bei komplexen Objekten
|
||||
- Sicherheitsrisiken durch unvalidierte State-Änderungen
|
||||
|
||||
**Lösung**:
|
||||
```php
|
||||
// State Schema Interface
|
||||
interface StateSchema
|
||||
{
|
||||
public function validate(array $state): ValidationResult;
|
||||
public function sanitize(array $state): array;
|
||||
public function getSerializableFields(): array;
|
||||
}
|
||||
|
||||
// Component mit Schema
|
||||
final readonly class TodoListComponent implements LiveComponentContract
|
||||
{
|
||||
public function getStateSchema(): StateSchema
|
||||
{
|
||||
return new TodoListStateSchema([
|
||||
'todos' => 'array<TodoItem>',
|
||||
'filter' => 'enum:all|active|completed',
|
||||
'page' => 'int:min=1'
|
||||
]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Files to Create**:
|
||||
- `src/Framework/LiveComponents/Contracts/StateSchema.php`
|
||||
- `src/Framework/LiveComponents/StateValidation/StateValidator.php`
|
||||
- `src/Framework/LiveComponents/StateValidation/ValidationResult.php`
|
||||
- `src/Framework/LiveComponents/StateValidation/Sanitizer.php`
|
||||
|
||||
**Framework Pattern**: Value Objects für State, Validation mit expliziten Contracts
|
||||
|
||||
---
|
||||
|
||||
### 1.2 CSRF-Integration
|
||||
**Impact**: 5 | **Effort**: 1 | **Framework Compliance**: 4 | **Priority**: 5.7
|
||||
|
||||
**Problem**:
|
||||
- Keine CSRF-Protection für Component-Actions
|
||||
- Security Gap
|
||||
|
||||
**Lösung**:
|
||||
```php
|
||||
// In LiveComponentHandler
|
||||
public function handle(
|
||||
LiveComponentContract $component,
|
||||
string $method,
|
||||
ActionParameters $params
|
||||
): ComponentUpdate {
|
||||
// CSRF-Check vor Action-Ausführung
|
||||
if (!$this->csrfValidator->validate($params->getCsrfToken())) {
|
||||
throw new CsrfTokenMismatchException();
|
||||
}
|
||||
|
||||
// ... existing code
|
||||
}
|
||||
```
|
||||
|
||||
**Files to Modify**:
|
||||
- `src/Framework/LiveComponents/LiveComponentHandler.php` (add CSRF check)
|
||||
- `src/Framework/LiveComponents/ValueObjects/ActionParameters.php` (add csrfToken field)
|
||||
- `resources/js/modules/livecomponent/index.js` (auto-include CSRF token)
|
||||
|
||||
**Framework Pattern**: Integration mit bestehendem CsrfMiddleware
|
||||
|
||||
---
|
||||
|
||||
### 1.3 Action Guards & Permissions
|
||||
**Impact**: 5 | **Effort**: 2 | **Framework Compliance**: 4 | **Priority**: 5.3
|
||||
|
||||
**Problem**:
|
||||
- Keine Permission-Checks auf Actions
|
||||
- Jeder kann jede Action aufrufen
|
||||
|
||||
**Lösung**:
|
||||
```php
|
||||
#[Attribute(Attribute::TARGET_METHOD)]
|
||||
final readonly class RequiresPermission
|
||||
{
|
||||
public function __construct(
|
||||
public string $permission
|
||||
) {}
|
||||
}
|
||||
|
||||
// Usage
|
||||
#[RequiresPermission('posts.delete')]
|
||||
public function deletePost(string $postId): ComponentData
|
||||
{
|
||||
// Only executed if user has permission
|
||||
}
|
||||
```
|
||||
|
||||
**Files to Create**:
|
||||
- `src/Framework/LiveComponents/Attributes/RequiresPermission.php`
|
||||
- `src/Framework/LiveComponents/Security/ActionAuthorizationChecker.php`
|
||||
|
||||
**Framework Pattern**: Attribute-basierte Authorization wie bei Routes
|
||||
|
||||
---
|
||||
|
||||
### 1.4 Starke Fehlermeldungen
|
||||
**Impact**: 4 | **Effort**: 2 | **Framework Compliance**: 4 | **Priority**: 4.0
|
||||
|
||||
**Problem**:
|
||||
- Generische Fehlermeldungen
|
||||
- Schwer zu debuggen
|
||||
|
||||
**Lösung**:
|
||||
```php
|
||||
// Custom Exceptions mit Context
|
||||
final class ComponentActionNotFoundException extends FrameworkException
|
||||
{
|
||||
public static function forAction(string $componentName, string $action): self
|
||||
{
|
||||
return self::create(
|
||||
ErrorCode::LIVECOMPONENT_ACTION_NOT_FOUND,
|
||||
"Action '{$action}' not found on component '{$componentName}'"
|
||||
)->withData([
|
||||
'component' => $componentName,
|
||||
'action' => $action,
|
||||
'available_actions' => self::getAvailableActions($componentName),
|
||||
'suggestion' => self::suggestSimilarAction($componentName, $action)
|
||||
]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Files to Create**:
|
||||
- `src/Framework/LiveComponents/Exceptions/ComponentActionNotFoundException.php`
|
||||
- `src/Framework/LiveComponents/Exceptions/InvalidStateException.php`
|
||||
- `src/Framework/LiveComponents/Exceptions/ComponentNotFoundException.php`
|
||||
|
||||
**Framework Pattern**: FrameworkException mit ErrorCode, ExceptionContext
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Architektur-Verbesserungen (P1 - SHOULD HAVE)
|
||||
|
||||
### 2.1 Lifecycle Hooks
|
||||
**Impact**: 4 | **Effort**: 3 | **Framework Compliance**: 4 | **Priority**: 3.7
|
||||
|
||||
**Problem**:
|
||||
- Keine standardisierten Lifecycle-Events
|
||||
- Schwer, Initialization/Cleanup zu machen
|
||||
|
||||
**Lösung**:
|
||||
```php
|
||||
interface ComponentLifecycle
|
||||
{
|
||||
public function beforeMount(ComponentData $initialState): ComponentData;
|
||||
public function mount(): void;
|
||||
public function hydrate(ComponentData $state): ComponentData;
|
||||
public function dehydrate(ComponentData $state): array;
|
||||
public function beforeUpdate(ComponentData $oldState, ComponentData $newState): ComponentData;
|
||||
public function updated(ComponentData $state): void;
|
||||
public function beforeDestroy(): void;
|
||||
}
|
||||
|
||||
// Trait mit Default-Implementations
|
||||
trait ComponentLifecycleTrait
|
||||
{
|
||||
public function beforeMount(ComponentData $initialState): ComponentData
|
||||
{
|
||||
return $initialState;
|
||||
}
|
||||
|
||||
// ... all hooks with empty defaults
|
||||
}
|
||||
```
|
||||
|
||||
**Files to Create**:
|
||||
- `src/Framework/LiveComponents/Contracts/ComponentLifecycle.php`
|
||||
- `src/Framework/LiveComponents/Traits/ComponentLifecycleTrait.php`
|
||||
|
||||
**Files to Modify**:
|
||||
- `src/Framework/LiveComponents/LiveComponentHandler.php` (call hooks)
|
||||
- `src/Framework/LiveComponents/ComponentRegistry.php` (call mount hook)
|
||||
|
||||
**Framework Pattern**: Contract + Trait für optionale Hooks
|
||||
|
||||
---
|
||||
|
||||
### 2.2 Middleware-Pipeline
|
||||
**Impact**: 4 | **Effort**: 3 | **Framework Compliance**: 5 | **Priority**: 4.0
|
||||
|
||||
**Problem**:
|
||||
- Cross-Cutting Concerns (Logging, Rate-Limiting) fest im Handler
|
||||
- Nicht erweiterbar
|
||||
|
||||
**Lösung**:
|
||||
```php
|
||||
interface ComponentMiddleware
|
||||
{
|
||||
public function before(
|
||||
LiveComponentContract $component,
|
||||
string $method,
|
||||
ActionParameters $params
|
||||
): void;
|
||||
|
||||
public function after(
|
||||
LiveComponentContract $component,
|
||||
ComponentUpdate $update
|
||||
): ComponentUpdate;
|
||||
}
|
||||
|
||||
// Built-in Middlewares
|
||||
final readonly class RateLimitMiddleware implements ComponentMiddleware {}
|
||||
final readonly class LoggingMiddleware implements ComponentMiddleware {}
|
||||
final readonly class ValidationMiddleware implements ComponentMiddleware {}
|
||||
final readonly class CsrfMiddleware implements ComponentMiddleware {}
|
||||
```
|
||||
|
||||
**Files to Create**:
|
||||
- `src/Framework/LiveComponents/Middleware/ComponentMiddleware.php`
|
||||
- `src/Framework/LiveComponents/Middleware/MiddlewarePipeline.php`
|
||||
- `src/Framework/LiveComponents/Middleware/RateLimitMiddleware.php`
|
||||
- `src/Framework/LiveComponents/Middleware/LoggingMiddleware.php`
|
||||
- `src/Framework/LiveComponents/Middleware/ValidationMiddleware.php`
|
||||
- `src/Framework/LiveComponents/Middleware/CsrfMiddleware.php`
|
||||
|
||||
**Files to Modify**:
|
||||
- `src/Framework/LiveComponents/LiveComponentHandler.php` (integrate pipeline)
|
||||
|
||||
**Framework Pattern**: Middleware-Pattern wie bei HTTP-Requests
|
||||
|
||||
---
|
||||
|
||||
### 2.3 Partial Rendering & DOM-Diffing
|
||||
**Impact**: 5 | **Effort**: 4 | **Framework Compliance**: 3 | **Priority**: 3.5
|
||||
|
||||
**Problem**:
|
||||
- Ganzer Component wird re-rendert
|
||||
- Ineffizient bei großen Components
|
||||
|
||||
**Lösung**:
|
||||
```php
|
||||
// Server-Side: Fragments
|
||||
interface SupportsFragments
|
||||
{
|
||||
public function getFragments(): array;
|
||||
public function renderFragment(string $name, ComponentData $data): string;
|
||||
}
|
||||
|
||||
// Client-Side: DOM-Diffing
|
||||
class DomPatcher {
|
||||
patch(element, newHtml) {
|
||||
// morphdom-ähnlicher Algorithmus
|
||||
this.diffAndPatch(element, newHtml);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Files to Create**:
|
||||
- `src/Framework/LiveComponents/Contracts/SupportsFragments.php`
|
||||
- `src/Framework/LiveComponents/Rendering/FragmentRenderer.php`
|
||||
- `public/js/modules/livecomponent/dom-patcher.js`
|
||||
|
||||
**Files to Modify**:
|
||||
- `src/Framework/View/LiveComponentRenderer.php` (fragment support)
|
||||
- `resources/js/modules/livecomponent/index.js` (DOM diffing)
|
||||
|
||||
**Framework Pattern**: Contract für Fragment-Support
|
||||
|
||||
---
|
||||
|
||||
### 2.4 Rate-Limiting auf Action-Level
|
||||
**Impact**: 4 | **Effort**: 2 | **Framework Compliance**: 4 | **Priority**: 4.0
|
||||
|
||||
**Problem**:
|
||||
- Keine Rate-Limits auf Actions
|
||||
- Potentielle DOS-Attacken
|
||||
|
||||
**Lösung**:
|
||||
```php
|
||||
#[Attribute(Attribute::TARGET_METHOD)]
|
||||
final readonly class RateLimit
|
||||
{
|
||||
public function __construct(
|
||||
public int $maxAttempts = 60,
|
||||
public int $decayMinutes = 1
|
||||
) {}
|
||||
}
|
||||
|
||||
// Usage
|
||||
#[RateLimit(maxAttempts: 5, decayMinutes: 1)]
|
||||
public function sendMessage(string $message): ComponentData
|
||||
{
|
||||
// Max 5 calls per minute
|
||||
}
|
||||
```
|
||||
|
||||
**Files to Create**:
|
||||
- `src/Framework/LiveComponents/Attributes/RateLimit.php`
|
||||
|
||||
**Files to Modify**:
|
||||
- `src/Framework/LiveComponents/Middleware/RateLimitMiddleware.php` (check attribute)
|
||||
|
||||
**Framework Pattern**: Attribute-basierte Konfiguration wie bei Routes
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Developer Experience (P2 - NICE TO HAVE)
|
||||
|
||||
### 3.1 CLI-Generator
|
||||
**Impact**: 3 | **Effort**: 2 | **Framework Compliance**: 3 | **Priority**: 2.7
|
||||
|
||||
**Problem**:
|
||||
- Boilerplate-Code für neue Components
|
||||
- Inkonsistente Struktur
|
||||
|
||||
**Lösung**:
|
||||
```bash
|
||||
php console.php make:livecomponent TodoList \
|
||||
--pollable \
|
||||
--cacheable \
|
||||
--with-test \
|
||||
--with-template
|
||||
```
|
||||
|
||||
**Files to Create**:
|
||||
- `src/Framework/Console/Commands/MakeLiveComponentCommand.php`
|
||||
- `resources/stubs/livecomponent.stub`
|
||||
- `resources/stubs/livecomponent-template.stub`
|
||||
- `resources/stubs/livecomponent-test.stub`
|
||||
|
||||
**Framework Pattern**: ConsoleCommand mit Stub-Templates
|
||||
|
||||
---
|
||||
|
||||
### 3.2 Test-Harness
|
||||
**Impact**: 3 | **Effort**: 2 | **Framework Compliance**: 4 | **Priority**: 3.0
|
||||
|
||||
**Problem**:
|
||||
- Boilerplate für Component-Tests
|
||||
- Inkonsistente Test-Setups
|
||||
|
||||
**Lösung**:
|
||||
```php
|
||||
it('increments counter', function () {
|
||||
$component = $this->mountComponent(CounterComponent::class, ['count' => 5]);
|
||||
|
||||
$update = $this->callAction($component, 'increment');
|
||||
|
||||
$this->assertComponentState($update, ['count' => 6]);
|
||||
$this->assertEventDispatched($update, 'counter:changed');
|
||||
});
|
||||
```
|
||||
|
||||
**Files to Create**:
|
||||
- `tests/Framework/LiveComponentTestCase.php`
|
||||
- `tests/Framework/Traits/LiveComponentTestHelpers.php`
|
||||
|
||||
**Framework Pattern**: Pest Test Helpers
|
||||
|
||||
---
|
||||
|
||||
### 3.3 Autodiscovery-Optimierung
|
||||
**Impact**: 3 | **Effort**: 2 | **Framework Compliance**: 5 | **Priority**: 3.3
|
||||
|
||||
**Problem**:
|
||||
- Discovery funktioniert, aber könnte klarer sein
|
||||
- Keine Kollisionswarnungen
|
||||
|
||||
**Lösung**:
|
||||
```php
|
||||
// Fallback auf Class-Name → kebab-case wenn kein Attribute
|
||||
final class TodoListComponent implements LiveComponentContract {}
|
||||
// → Auto-Name: "todo-list"
|
||||
|
||||
// Collision Detection
|
||||
if (isset($map[$name])) {
|
||||
throw new ComponentNameCollisionException(
|
||||
"Component name '{$name}' already registered by {$map[$name]}"
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Files to Modify**:
|
||||
- `src/Framework/LiveComponents/ComponentRegistry.php` (fallback + collision check)
|
||||
|
||||
**Framework Pattern**: Convention over Configuration
|
||||
|
||||
---
|
||||
|
||||
### 3.4 Template-Konventionen dokumentieren
|
||||
**Impact**: 2 | **Effort**: 1 | **Framework Compliance**: 3 | **Priority**: 2.0
|
||||
|
||||
**Problem**:
|
||||
- Keine klaren Template-Patterns
|
||||
- Inkonsistente Component-Markup
|
||||
|
||||
**Lösung**:
|
||||
```html
|
||||
<!-- Standard Component Template Structure -->
|
||||
<div data-lc="component-name" data-key="{id}" class="lc-component">
|
||||
<!-- Component-specific markup -->
|
||||
|
||||
<!-- Slot system for extensibility -->
|
||||
<slot name="header"></slot>
|
||||
<slot name="content">Default content</slot>
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Files to Create/Modify**:
|
||||
- `docs/claude/livecomponent-template-conventions.md`
|
||||
- Update existing templates to follow conventions
|
||||
|
||||
**Framework Pattern**: Template-System Integration
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Performance & Polish (P3 - FUTURE)
|
||||
|
||||
### 4.1 Request-Cache/Memoization
|
||||
**Impact**: 3 | **Effort**: 3 | **Framework Compliance**: 4 | **Priority**: 2.7
|
||||
|
||||
**Lösung**:
|
||||
```php
|
||||
interface Cacheable
|
||||
{
|
||||
public function getCacheTtl(): Duration;
|
||||
public function getCacheKey(): CacheKey;
|
||||
public function getCacheTags(): array;
|
||||
public function varyBy(): array; // ['state.userId', 'state.filter']
|
||||
}
|
||||
```
|
||||
|
||||
**Framework Pattern**: SmartCache mit varyBy-Support
|
||||
|
||||
---
|
||||
|
||||
### 4.2 Polling-Coalescing
|
||||
**Impact**: 2 | **Effort**: 3 | **Framework Compliance**: 2 | **Priority**: 1.3
|
||||
|
||||
**Lösung**:
|
||||
```javascript
|
||||
// Batch multiple poll requests
|
||||
class PollCoalescer {
|
||||
batchRequest(components) {
|
||||
return fetch('/live-component/poll-batch', {
|
||||
body: JSON.stringify({
|
||||
components: components.map(c => c.id)
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4.3 Debounce/Throttle im Protokoll
|
||||
**Impact**: 3 | **Effort**: 2 | **Framework Compliance**: 2 | **Priority**: 2.3
|
||||
|
||||
**Lösung**:
|
||||
```html
|
||||
<input
|
||||
data-live-action="search"
|
||||
data-debounce="300"
|
||||
data-throttle="1000"
|
||||
/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4.4 SSE Streaming
|
||||
**Impact**: 3 | **Effort**: 4 | **Framework Compliance**: 3 | **Priority**: 2.0
|
||||
|
||||
**Lösung**:
|
||||
```php
|
||||
interface StreamableComponent
|
||||
{
|
||||
public function streamUpdates(): Generator;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4.5 Optimistic UI
|
||||
**Impact**: 2 | **Effort**: 4 | **Framework Compliance**: 2 | **Priority**: 1.0
|
||||
|
||||
**Lösung**:
|
||||
```javascript
|
||||
// Client setzt State vorläufig, Server bestätigt oder rollt zurück
|
||||
await component.callAction('deleteItem', { id: 123 }, { optimistic: true });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4.6 Background Actions
|
||||
**Impact**: 2 | **Effort**: 3 | **Framework Compliance**: 3 | **Priority**: 1.7
|
||||
|
||||
**Lösung**:
|
||||
```php
|
||||
#[Attribute(Attribute::TARGET_METHOD)]
|
||||
final readonly class Background
|
||||
{
|
||||
public function __construct(
|
||||
public bool $queueable = true,
|
||||
public ?string $queue = null
|
||||
) {}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4.7 Accessibility Hooks
|
||||
**Impact**: 2 | **Effort**: 2 | **Framework Compliance**: 2 | **Priority**: 1.3
|
||||
|
||||
**Lösung**:
|
||||
```html
|
||||
<div
|
||||
data-lc="component"
|
||||
data-lc-keep-focus
|
||||
data-lc-announce-updates
|
||||
role="region"
|
||||
aria-live="polite"
|
||||
>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4.8 DevTools Overlay
|
||||
**Impact**: 2 | **Effort**: 4 | **Framework Compliance**: 1 | **Priority**: 0.7
|
||||
|
||||
**Lösung**:
|
||||
```javascript
|
||||
// Dev-Modus Overlay mit Component-Inspector
|
||||
if (ENV === 'development') {
|
||||
new LiveComponentDevTools().init();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Wins (Immediate Implementation)
|
||||
|
||||
### Top 5 Quick Wins (High Impact / Low Effort)
|
||||
|
||||
1. **CSRF-Integration** (Impact: 5, Effort: 1, Priority: 5.7)
|
||||
- 2-4 Stunden
|
||||
- Sofortiger Security-Benefit
|
||||
|
||||
2. **Starke Fehlermeldungen** (Impact: 4, Effort: 2, Priority: 4.0)
|
||||
- 1 Tag
|
||||
- Massiv verbesserte DX
|
||||
|
||||
3. **CLI-Generator** (Impact: 3, Effort: 2, Priority: 2.7)
|
||||
- 1 Tag
|
||||
- Accelerates development
|
||||
|
||||
4. **Test-Harness** (Impact: 3, Effort: 2, Priority: 3.0)
|
||||
- 1 Tag
|
||||
- Better test coverage
|
||||
|
||||
5. **Template-Konventionen** (Impact: 2, Effort: 1, Priority: 2.0)
|
||||
- 4 Stunden
|
||||
- Consistency across components
|
||||
|
||||
---
|
||||
|
||||
## Implementation Roadmap
|
||||
|
||||
### Sprint 1 (Week 1-2): Security Foundation
|
||||
- ✅ CSRF-Integration
|
||||
- ✅ Action Guards & Permissions
|
||||
- ✅ State-Validierung
|
||||
|
||||
**Deliverables**: Secure LiveComponents-System
|
||||
|
||||
---
|
||||
|
||||
### Sprint 2 (Week 3-4): Architecture Improvements
|
||||
- ✅ Middleware-Pipeline
|
||||
- ✅ Lifecycle Hooks
|
||||
- ✅ Starke Fehlermeldungen
|
||||
|
||||
**Deliverables**: Extensible, Developer-Friendly System
|
||||
|
||||
---
|
||||
|
||||
### Sprint 3 (Week 5-6): Performance & DX
|
||||
- ✅ Partial Rendering
|
||||
- ✅ Rate-Limiting
|
||||
- ✅ CLI-Generator
|
||||
- ✅ Test-Harness
|
||||
|
||||
**Deliverables**: Production-Ready System with Developer Tools
|
||||
|
||||
---
|
||||
|
||||
### Sprint 4+ (Future): Polish & Advanced Features
|
||||
- ☐ Request-Cache/Memoization
|
||||
- ☐ Polling-Coalescing
|
||||
- ☐ SSE Streaming
|
||||
- ☐ Optimistic UI
|
||||
- ☐ Background Actions
|
||||
- ☐ DevTools Overlay
|
||||
|
||||
**Deliverables**: Enterprise-Grade LiveComponents
|
||||
|
||||
---
|
||||
|
||||
## Success Metrics
|
||||
|
||||
### Security
|
||||
- ✅ 100% CSRF-Protection on all Actions
|
||||
- ✅ Permission-Checks auf allen kritischen Actions
|
||||
- ✅ State-Validierung für alle Components
|
||||
|
||||
### Performance
|
||||
- ✅ < 50ms Action-Latency (P95)
|
||||
- ✅ < 100ms Render-Time (P95)
|
||||
- ✅ 80%+ Cache-Hit-Rate
|
||||
|
||||
### Developer Experience
|
||||
- ✅ < 5 Minuten neue Component erstellen
|
||||
- ✅ < 10 Minuten Component-Test schreiben
|
||||
- ✅ Klare Fehlermeldungen mit Lösungsvorschlägen
|
||||
|
||||
### Quality
|
||||
- ✅ 90%+ Test-Coverage
|
||||
- ✅ 100% Framework-Pattern-Compliance
|
||||
- ✅ Zero breaking changes to existing components
|
||||
|
||||
---
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
### High Risk
|
||||
- **Middleware-Pipeline**: Breaking Changes möglich
|
||||
- **Mitigation**: Optional in v1, Mandatory in v2
|
||||
|
||||
- **Lifecycle Hooks**: Performance Impact
|
||||
- **Mitigation**: Profiling, Optional Hooks
|
||||
|
||||
### Medium Risk
|
||||
- **Partial Rendering**: Client-Side Complexity
|
||||
- **Mitigation**: Progressive Enhancement, Fallback zu Full-Render
|
||||
|
||||
- **State-Validierung**: Performance bei großen States
|
||||
- **Mitigation**: Lazy Validation, Schema-Caching
|
||||
|
||||
### Low Risk
|
||||
- **CSRF, Permissions, CLI-Generator**: Isolierte Änderungen
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Review & Approval**: Stakeholder-Review dieses Plans
|
||||
2. **Prototype**: CSRF-Integration als Proof-of-Concept
|
||||
3. **Sprint Planning**: Detaillierte Sprint-1-Tasks
|
||||
4. **Implementation**: Start mit Quick Wins
|
||||
|
||||
---
|
||||
|
||||
## Appendix: Framework-Compliance Checklist
|
||||
|
||||
Alle Implementations müssen folgen:
|
||||
- ✅ Readonly Classes wo möglich
|
||||
- ✅ Value Objects statt Primitives
|
||||
- ✅ Composition over Inheritance
|
||||
- ✅ Attribute-basierte Discovery
|
||||
- ✅ Explicit Dependency Injection
|
||||
- ✅ FrameworkException für Fehler
|
||||
- ✅ Contracts statt Abstraction
|
||||
- ✅ Pest Tests
|
||||
@@ -1,717 +0,0 @@
|
||||
# LiveComponents Lazy Loading
|
||||
|
||||
**Status**: ✅ Implementiert
|
||||
**Date**: 2025-10-09
|
||||
|
||||
Lazy Loading System für LiveComponents mit IntersectionObserver, Priority Queues und Skeleton Loaders.
|
||||
|
||||
---
|
||||
|
||||
## Übersicht
|
||||
|
||||
Das Lazy Loading System ermöglicht es, LiveComponents erst zu laden wenn sie im Viewport sichtbar werden. Dies verbessert die initiale Ladezeit und reduziert unnötige Server-Requests.
|
||||
|
||||
**Key Features**:
|
||||
- IntersectionObserver API für Viewport Detection
|
||||
- Priority-basierte Loading Queue (high, normal, low)
|
||||
- Configurable threshold und root margin
|
||||
- Professional Skeleton Loaders während des Ladens
|
||||
- Automatic Component Initialization nach Load
|
||||
- Error Handling mit Retry Logic
|
||||
- Statistics Tracking
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||
│ Template │───▶│ Placeholder │───▶│ LazyComponent │───▶│ LiveComponent │
|
||||
│ Function │ │ with Skeleton │ │ Loader │ │ Initialization │
|
||||
└─────────────────┘ └─────────────────┘ └─────────────────┘ └─────────────────┘
|
||||
│ │ │ │
|
||||
lazy_component() data-live-component-lazy IntersectionObserver render + init
|
||||
Template Syntax Skeleton Loader CSS Priority Queue Full Component
|
||||
```
|
||||
|
||||
### Workflow
|
||||
|
||||
1. **Template Rendering**: `{{ lazy_component('id', options) }}` generiert Placeholder
|
||||
2. **Initial Page Load**: Skeleton Loader wird angezeigt
|
||||
3. **Viewport Detection**: IntersectionObserver erkennt Sichtbarkeit
|
||||
4. **Priority Queue**: Component wird basierend auf Priority geladen
|
||||
5. **Server Request**: Fetch von `/live-component/{id}/lazy-load`
|
||||
6. **DOM Update**: Placeholder wird durch Component HTML ersetzt
|
||||
7. **Initialization**: LiveComponent wird als normale Component initialisiert
|
||||
|
||||
---
|
||||
|
||||
## Template Usage
|
||||
|
||||
### Basic Lazy Loading
|
||||
|
||||
```php
|
||||
<!-- Einfaches Lazy Loading -->
|
||||
{{ lazy_component('user-stats:123') }}
|
||||
|
||||
<!-- Mit Priority -->
|
||||
{{ lazy_component('notification-bell:user-456', {
|
||||
'priority': 'high'
|
||||
}) }}
|
||||
|
||||
<!-- Mit Custom Placeholder -->
|
||||
{{ lazy_component('activity-feed:latest', {
|
||||
'placeholder': 'Loading your activity feed...',
|
||||
'class': 'skeleton-feed'
|
||||
}) }}
|
||||
|
||||
<!-- Mit allen Optionen -->
|
||||
{{ lazy_component('analytics-chart:dashboard', {
|
||||
'priority': 'normal',
|
||||
'threshold': '0.25',
|
||||
'rootMargin': '100px',
|
||||
'placeholder': 'Loading analytics...',
|
||||
'class': 'skeleton-chart'
|
||||
}) }}
|
||||
```
|
||||
|
||||
### LazyComponentFunction Options
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `priority` | `'high'\|'normal'\|'low'` | `'normal'` | Loading priority in queue |
|
||||
| `threshold` | `string` | `'0.1'` | Visibility threshold (0.0-1.0) |
|
||||
| `placeholder` | `string\|null` | `null` | Custom loading text |
|
||||
| `rootMargin` | `string\|null` | `null` | IntersectionObserver root margin |
|
||||
| `class` | `string` | `''` | CSS class for skeleton loader |
|
||||
|
||||
### Priority Levels
|
||||
|
||||
**High Priority** (`priority: 'high'`):
|
||||
- Laden sobald sichtbar (minimal delay)
|
||||
- Use Cases: Above-the-fold content, kritische UI-Elemente
|
||||
- Beispiele: Navigation, User Profile, Critical Notifications
|
||||
|
||||
**Normal Priority** (`priority: 'normal'`):
|
||||
- Standard Queue Processing
|
||||
- Use Cases: Reguläre Content-Bereiche
|
||||
- Beispiele: Article List, Comment Sections, Product Cards
|
||||
|
||||
**Low Priority** (`priority: 'low'`):
|
||||
- Laden nur wenn Idle Time verfügbar
|
||||
- Use Cases: Below-the-fold content, optional Features
|
||||
- Beispiele: Related Articles, Advertisements, Footer Content
|
||||
|
||||
---
|
||||
|
||||
## Skeleton Loaders
|
||||
|
||||
### Available Skeleton Types
|
||||
|
||||
Das Framework bietet 8 vorgefertigte Skeleton Loader Varianten:
|
||||
|
||||
#### 1. Text Skeleton
|
||||
```html
|
||||
<div class="skeleton skeleton-text skeleton-text--full"></div>
|
||||
<div class="skeleton skeleton-text skeleton-text--80"></div>
|
||||
<div class="skeleton skeleton-text skeleton-text--60"></div>
|
||||
<div class="skeleton skeleton-text skeleton-text--lg"></div>
|
||||
```
|
||||
|
||||
**Use Cases**: Text Placeholders, Titles, Paragraphs
|
||||
|
||||
#### 2. Card Skeleton
|
||||
```html
|
||||
<div class="skeleton-card">
|
||||
<div class="skeleton-card__header">
|
||||
<div class="skeleton skeleton-card__avatar"></div>
|
||||
<div class="skeleton-card__title">
|
||||
<div class="skeleton skeleton-text"></div>
|
||||
<div class="skeleton skeleton-text skeleton-text--60"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="skeleton skeleton-card__image"></div>
|
||||
<div class="skeleton-card__content">
|
||||
<div class="skeleton skeleton-text"></div>
|
||||
<div class="skeleton skeleton-text"></div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Use Cases**: User Cards, Product Cards, Article Cards
|
||||
|
||||
#### 3. List Skeleton
|
||||
```html
|
||||
<div class="skeleton-list">
|
||||
<div class="skeleton-list__item">
|
||||
<div class="skeleton skeleton-list__icon"></div>
|
||||
<div class="skeleton-list__content">
|
||||
<div class="skeleton skeleton-text"></div>
|
||||
<div class="skeleton skeleton-text skeleton-text--60"></div>
|
||||
</div>
|
||||
<div class="skeleton skeleton-list__action"></div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Use Cases**: Navigation Lists, Settings Lists, Item Lists
|
||||
|
||||
#### 4. Table Skeleton
|
||||
```html
|
||||
<div class="skeleton-table">
|
||||
<div class="skeleton-table__row skeleton-table__row--header">
|
||||
<div class="skeleton skeleton-table__cell"></div>
|
||||
<div class="skeleton skeleton-table__cell"></div>
|
||||
<div class="skeleton skeleton-table__cell"></div>
|
||||
</div>
|
||||
<div class="skeleton-table__row">
|
||||
<div class="skeleton skeleton-table__cell"></div>
|
||||
<div class="skeleton skeleton-table__cell"></div>
|
||||
<div class="skeleton skeleton-table__cell"></div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Use Cases**: Data Tables, Reports, Grids
|
||||
|
||||
#### 5. Feed Skeleton
|
||||
```html
|
||||
<div class="skeleton-feed">
|
||||
<div class="skeleton-feed__item">
|
||||
<div class="skeleton-feed__header">
|
||||
<div class="skeleton skeleton-feed__avatar"></div>
|
||||
<div class="skeleton-feed__meta">
|
||||
<div class="skeleton skeleton-text"></div>
|
||||
<div class="skeleton skeleton-text skeleton-text--60"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="skeleton-feed__content">
|
||||
<div class="skeleton skeleton-text"></div>
|
||||
<div class="skeleton skeleton-text"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Use Cases**: Social Feeds, Activity Feeds, Comment Threads
|
||||
|
||||
#### 6. Stats Skeleton
|
||||
```html
|
||||
<div class="skeleton-stats">
|
||||
<div class="skeleton-stats__card">
|
||||
<div class="skeleton skeleton-stats__label"></div>
|
||||
<div class="skeleton skeleton-stats__value"></div>
|
||||
<div class="skeleton skeleton-stats__trend"></div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Use Cases**: Dashboard Stats, Analytics Cards, Metrics Display
|
||||
|
||||
#### 7. Chart Skeleton
|
||||
```html
|
||||
<div class="skeleton-chart">
|
||||
<div class="skeleton skeleton-chart__title"></div>
|
||||
<div class="skeleton-chart__graph">
|
||||
<div class="skeleton skeleton-chart__bar"></div>
|
||||
<div class="skeleton skeleton-chart__bar"></div>
|
||||
<div class="skeleton skeleton-chart__bar"></div>
|
||||
</div>
|
||||
<div class="skeleton-chart__legend">
|
||||
<div class="skeleton skeleton-chart__legend-item"></div>
|
||||
<div class="skeleton skeleton-chart__legend-item"></div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Use Cases**: Charts, Graphs, Data Visualizations
|
||||
|
||||
#### 8. Container Skeleton
|
||||
```html
|
||||
<div class="skeleton-container">
|
||||
<!-- Any skeleton content -->
|
||||
</div>
|
||||
```
|
||||
|
||||
**Use Cases**: Generic Container mit Loading Indicator
|
||||
|
||||
### Skeleton Loader Features
|
||||
|
||||
**Shimmer Animation**:
|
||||
```css
|
||||
.skeleton {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
var(--skeleton-bg) 0%,
|
||||
var(--skeleton-shimmer) 50%,
|
||||
var(--skeleton-bg) 100%
|
||||
);
|
||||
animation: skeleton-shimmer 1.5s infinite ease-in-out;
|
||||
}
|
||||
```
|
||||
|
||||
**Dark Mode Support**:
|
||||
- Automatic color adjustment via `@media (prefers-color-scheme: dark)`
|
||||
- Accessible contrast ratios
|
||||
|
||||
**Reduced Motion Support**:
|
||||
```css
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.skeleton {
|
||||
animation: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Responsive Design**:
|
||||
- Mobile-optimized layouts
|
||||
- Breakpoints at 768px
|
||||
|
||||
---
|
||||
|
||||
## Backend Implementation
|
||||
|
||||
### LazyComponentFunction
|
||||
|
||||
**Location**: `src/Framework/View/Functions/LazyComponentFunction.php`
|
||||
|
||||
```php
|
||||
final readonly class LazyComponentFunction implements TemplateFunction
|
||||
{
|
||||
public function __invoke(string $componentId, array $options = []): string
|
||||
{
|
||||
// Extract and validate options
|
||||
$priority = $options['priority'] ?? 'normal';
|
||||
$threshold = $options['threshold'] ?? '0.1';
|
||||
$placeholder = $options['placeholder'] ?? null;
|
||||
|
||||
// Build HTML attributes
|
||||
$attributes = [
|
||||
'data-live-component-lazy' => htmlspecialchars($componentId),
|
||||
'data-lazy-priority' => htmlspecialchars($priority),
|
||||
'data-lazy-threshold' => htmlspecialchars($threshold)
|
||||
];
|
||||
|
||||
// Generate placeholder HTML
|
||||
return sprintf('<div %s></div>', $attributesHtml);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Registration**: Automatisch in `PlaceholderReplacer` registriert
|
||||
|
||||
### Lazy Load Endpoint
|
||||
|
||||
**Route**: `GET /live-component/{id}/lazy-load`
|
||||
**Controller**: `LiveComponentController::handleLazyLoad()`
|
||||
|
||||
```php
|
||||
#[Route('/live-component/{id}/lazy-load', method: Method::GET)]
|
||||
public function handleLazyLoad(string $id, HttpRequest $request): JsonResult
|
||||
{
|
||||
try {
|
||||
$componentId = ComponentId::fromString($id);
|
||||
$component = $this->componentRegistry->resolve($componentId, initialData: null);
|
||||
$html = $this->componentRegistry->renderWithWrapper($component);
|
||||
|
||||
return new JsonResult([
|
||||
'success' => true,
|
||||
'html' => $html,
|
||||
'state' => $component->getData()->toArray(),
|
||||
'csrf_token' => $this->generateCsrfToken($componentId),
|
||||
'component_id' => $componentId->toString()
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return new JsonResult([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response Format**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"html": "<div data-live-component='counter:demo'>...</div>",
|
||||
"state": {
|
||||
"count": 0,
|
||||
"label": "Counter"
|
||||
},
|
||||
"csrf_token": "abc123...",
|
||||
"component_id": "counter:demo"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Frontend Implementation
|
||||
|
||||
### LazyComponentLoader
|
||||
|
||||
**Location**: `resources/js/modules/livecomponent/LazyComponentLoader.js`
|
||||
|
||||
**Features**:
|
||||
- IntersectionObserver für Viewport Detection
|
||||
- Priority-basierte Loading Queue
|
||||
- Configurable threshold und root margin
|
||||
- Error Handling mit Retry Logic
|
||||
- Statistics Tracking
|
||||
|
||||
**Initialization**:
|
||||
```javascript
|
||||
// Automatic initialization via LiveComponent module
|
||||
import { LiveComponent } from './modules/livecomponent/index.js';
|
||||
|
||||
// LazyComponentLoader wird automatisch initialisiert
|
||||
LiveComponent.initLazyLoading();
|
||||
```
|
||||
|
||||
**Manual Usage** (optional):
|
||||
```javascript
|
||||
import { LazyComponentLoader } from './modules/livecomponent/LazyComponentLoader.js';
|
||||
import { LiveComponent } from './modules/livecomponent/index.js';
|
||||
|
||||
const lazyLoader = new LazyComponentLoader(LiveComponent);
|
||||
lazyLoader.init();
|
||||
```
|
||||
|
||||
### Loading Process
|
||||
|
||||
1. **Scan DOM** für `[data-live-component-lazy]` Elemente
|
||||
2. **Register Components** mit IntersectionObserver
|
||||
3. **Detect Visibility** basierend auf threshold
|
||||
4. **Queue by Priority**: high → normal → low
|
||||
5. **Fetch from Server**: `/live-component/{id}/lazy-load`
|
||||
6. **Replace Placeholder**: Update DOM mit Component HTML
|
||||
7. **Initialize Component**: `LiveComponent.init(element)`
|
||||
|
||||
### Configuration Options
|
||||
|
||||
```javascript
|
||||
class LazyComponentLoader {
|
||||
constructor(liveComponentManager) {
|
||||
this.config = {
|
||||
threshold: 0.1, // Default visibility threshold
|
||||
rootMargin: '0px', // Default root margin
|
||||
priorityWeights: { // Priority processing weights
|
||||
high: 1,
|
||||
normal: 5,
|
||||
low: 10
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Characteristics
|
||||
|
||||
### Loading Performance
|
||||
|
||||
**Metrics** (typical values):
|
||||
- **Initial Scan**: <10ms for 100 components
|
||||
- **IntersectionObserver Setup**: <5ms per component
|
||||
- **Visibility Detection**: <1ms (native browser API)
|
||||
- **Fetch Request**: 50-200ms (network dependent)
|
||||
- **DOM Replacement**: 5-20ms per component
|
||||
- **Component Initialization**: 10-50ms per component
|
||||
|
||||
**Total Load Time**: ~100-300ms per component (network + processing)
|
||||
|
||||
### Priority Queue Performance
|
||||
|
||||
**Processing Strategy**:
|
||||
```javascript
|
||||
// High priority: Process immediately
|
||||
// Normal priority: 5ms delay between loads
|
||||
// Low priority: 10ms delay between loads
|
||||
```
|
||||
|
||||
**Concurrent Loading**:
|
||||
- Max 3 concurrent requests (browser limit)
|
||||
- Queue processes in priority order
|
||||
- Automatic retry on failure (max 3 attempts)
|
||||
|
||||
### Memory Footprint
|
||||
|
||||
- **LazyComponentLoader**: ~5KB
|
||||
- **Per Component**: ~500 bytes (metadata + observer)
|
||||
- **100 Lazy Components**: ~55KB total overhead
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### When to Use Lazy Loading
|
||||
|
||||
**✅ Use Lazy Loading For**:
|
||||
- Below-the-fold content
|
||||
- Heavy components (charts, tables, complex UI)
|
||||
- Optional features (comments, related articles)
|
||||
- User-specific content (notifications, profile widgets)
|
||||
- Analytics and tracking components
|
||||
|
||||
**❌ Don't Use Lazy Loading For**:
|
||||
- Above-the-fold critical content
|
||||
- Navigation elements
|
||||
- Essential UI components
|
||||
- Small, lightweight components
|
||||
- Content needed for SEO
|
||||
|
||||
### Priority Guidelines
|
||||
|
||||
**High Priority**:
|
||||
```php
|
||||
{{ lazy_component('user-notifications:current', {'priority': 'high'}) }}
|
||||
{{ lazy_component('shopping-cart:summary', {'priority': 'high'}) }}
|
||||
```
|
||||
|
||||
**Normal Priority**:
|
||||
```php
|
||||
{{ lazy_component('article-list:category-123', {'priority': 'normal'}) }}
|
||||
{{ lazy_component('comment-section:post-456', {'priority': 'normal'}) }}
|
||||
```
|
||||
|
||||
**Low Priority**:
|
||||
```php
|
||||
{{ lazy_component('related-articles:post-789', {'priority': 'low'}) }}
|
||||
{{ lazy_component('ad-banner:sidebar', {'priority': 'low'}) }}
|
||||
```
|
||||
|
||||
### Skeleton Loader Selection
|
||||
|
||||
**Match Skeleton to Component Structure**:
|
||||
```php
|
||||
<!-- User Card Component → Card Skeleton -->
|
||||
{{ lazy_component('user-card:123', {'class': 'skeleton-card'}) }}
|
||||
|
||||
<!-- Data Table Component → Table Skeleton -->
|
||||
{{ lazy_component('analytics-table:dashboard', {'class': 'skeleton-table'}) }}
|
||||
|
||||
<!-- Activity Feed → Feed Skeleton -->
|
||||
{{ lazy_component('activity-feed:user-456', {'class': 'skeleton-feed'}) }}
|
||||
```
|
||||
|
||||
### Threshold Configuration
|
||||
|
||||
**Viewport Thresholds**:
|
||||
- `0.0` - Load as soon as any pixel is visible
|
||||
- `0.1` - Load when 10% visible (default, recommended)
|
||||
- `0.5` - Load when 50% visible
|
||||
- `1.0` - Load only when fully visible
|
||||
|
||||
**Root Margin** (preloading):
|
||||
```php
|
||||
<!-- Load 200px before entering viewport -->
|
||||
{{ lazy_component('image-gallery:album-1', {
|
||||
'rootMargin': '200px'
|
||||
}) }}
|
||||
|
||||
<!-- Load only when fully in viewport -->
|
||||
{{ lazy_component('video-player:clip-1', {
|
||||
'threshold': '1.0',
|
||||
'rootMargin': '0px'
|
||||
}) }}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Retry Logic
|
||||
|
||||
```javascript
|
||||
// LazyComponentLoader retry configuration
|
||||
async loadComponent(config) {
|
||||
const maxRetries = 3;
|
||||
let attempt = 0;
|
||||
|
||||
while (attempt < maxRetries) {
|
||||
try {
|
||||
const response = await fetch(`/live-component/${config.id}/lazy-load`);
|
||||
// ... process response
|
||||
return;
|
||||
} catch (error) {
|
||||
attempt++;
|
||||
if (attempt >= maxRetries) {
|
||||
this.showError(config.element, error);
|
||||
}
|
||||
await this.delay(1000 * attempt); // Exponential backoff
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Error Display
|
||||
|
||||
```javascript
|
||||
showError(element, error) {
|
||||
element.innerHTML = `
|
||||
<div class="lazy-load-error">
|
||||
<p>Failed to load component</p>
|
||||
<button onclick="window.location.reload()">Retry</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Debugging
|
||||
|
||||
### Enable Debug Logging
|
||||
|
||||
```javascript
|
||||
// In browser console
|
||||
localStorage.setItem('livecomponent-debug', 'true');
|
||||
location.reload();
|
||||
```
|
||||
|
||||
**Debug Output**:
|
||||
```
|
||||
[LazyComponentLoader] Initialized
|
||||
[LazyComponentLoader] Found 15 lazy components
|
||||
[LazyComponentLoader] Registered: counter:lazy-1 (priority: normal)
|
||||
[LazyComponentLoader] Component visible: counter:lazy-1
|
||||
[LazyComponentLoader] Loading: counter:lazy-1
|
||||
[LazyComponentLoader] Loaded successfully: counter:lazy-1 (142ms)
|
||||
```
|
||||
|
||||
### Statistics
|
||||
|
||||
```javascript
|
||||
// Get loading statistics
|
||||
const stats = LiveComponent.lazyLoader.getStats();
|
||||
|
||||
console.log(stats);
|
||||
// {
|
||||
// total_components: 15,
|
||||
// loaded: 8,
|
||||
// pending: 7,
|
||||
// failed: 0,
|
||||
// average_load_time_ms: 125
|
||||
// }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
### Manual Testing
|
||||
|
||||
```html
|
||||
<!-- Test Page -->
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<h1>Lazy Loading Test</h1>
|
||||
|
||||
<!-- Above fold - should NOT lazy load -->
|
||||
{{{ counter }}}
|
||||
|
||||
<div style="height: 2000px;"></div>
|
||||
|
||||
<!-- Below fold - should lazy load -->
|
||||
{{ lazy_component('timer:demo', {
|
||||
'priority': 'normal',
|
||||
'class': 'skeleton-card'
|
||||
}) }}
|
||||
|
||||
<script type="module">
|
||||
import { LiveComponent } from '/assets/js/main.js';
|
||||
LiveComponent.initLazyLoading();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### E2E Testing (Playwright)
|
||||
|
||||
```javascript
|
||||
// tests/e2e/lazy-loading.spec.js
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('lazy loads component on scroll', async ({ page }) => {
|
||||
await page.goto('/test/lazy-loading');
|
||||
|
||||
// Component should not be loaded initially
|
||||
const lazyComponent = page.locator('[data-live-component-lazy="timer:demo"]');
|
||||
await expect(lazyComponent).toBeVisible();
|
||||
await expect(lazyComponent).toContainText(''); // Empty placeholder
|
||||
|
||||
// Scroll to component
|
||||
await lazyComponent.scrollIntoViewIfNeeded();
|
||||
|
||||
// Wait for loading
|
||||
await page.waitForSelector('[data-live-component="timer:demo"]', {
|
||||
timeout: 5000
|
||||
});
|
||||
|
||||
// Component should be loaded
|
||||
const loadedComponent = page.locator('[data-live-component="timer:demo"]');
|
||||
await expect(loadedComponent).toBeVisible();
|
||||
await expect(loadedComponent).not.toBeEmpty();
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**1. Component not loading**
|
||||
- Check browser console for errors
|
||||
- Verify component ID format: `name:instance`
|
||||
- Check network tab for 404 errors
|
||||
- Ensure component is registered in ComponentRegistry
|
||||
|
||||
**2. Skeleton loader not showing**
|
||||
- Verify CSS is loaded: `component-playground.css`
|
||||
- Check class name in template matches skeleton variant
|
||||
- Inspect HTML for correct skeleton structure
|
||||
|
||||
**3. Loading too slow**
|
||||
- Check network tab for request time
|
||||
- Reduce rootMargin to preload earlier
|
||||
- Increase priority for important components
|
||||
- Optimize backend endpoint response time
|
||||
|
||||
**4. Multiple loads of same component**
|
||||
- Ensure unique instance IDs
|
||||
- Check for duplicate lazy_component() calls
|
||||
- Verify IntersectionObserver cleanup
|
||||
|
||||
---
|
||||
|
||||
## Framework Integration
|
||||
|
||||
**Template System**: Integrated via `TemplateFunctions`
|
||||
**View Module**: Uses `LiveComponentRenderer`
|
||||
**HTTP**: Standard Route + Controller
|
||||
**JavaScript**: Core Module with auto-initialization
|
||||
**CSS**: Component Layer with @layer architecture
|
||||
|
||||
**Dependencies**:
|
||||
- PlaceholderReplacer (template processing)
|
||||
- ComponentRegistry (component resolution)
|
||||
- LiveComponentController (HTTP endpoint)
|
||||
- LiveComponent Module (frontend initialization)
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
Das Lazy Loading System bietet:
|
||||
|
||||
✅ **Performance**: Reduziert initiale Ladezeit um 40-60% für content-heavy Pages
|
||||
✅ **User Experience**: Professional Skeleton Loaders mit Shimmer Animation
|
||||
✅ **Developer Experience**: Simple Template Syntax `{{ lazy_component() }}`
|
||||
✅ **Flexibility**: 8 Skeleton Variants, Priority Levels, Configurable Thresholds
|
||||
✅ **Accessibility**: Dark Mode, Reduced Motion Support
|
||||
✅ **Robustness**: Error Handling, Retry Logic, Statistics Tracking
|
||||
✅ **Framework Compliance**: Value Objects, Readonly Classes, Convention over Configuration
|
||||
@@ -1,701 +0,0 @@
|
||||
# LiveComponents Monitoring & Debugging
|
||||
|
||||
**Status**: ✅ Implemented
|
||||
**Date**: 2025-10-09
|
||||
|
||||
Comprehensive monitoring and debugging infrastructure for LiveComponents system.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Production-ready monitoring and development debugging tools for LiveComponents:
|
||||
- **Production Monitoring**: Metrics, health checks, performance tracking
|
||||
- **Development Debugging**: Debug panel, component inspector
|
||||
- **Admin-Only Security**: All endpoints require admin authentication
|
||||
|
||||
---
|
||||
|
||||
## 1. Production Monitoring
|
||||
|
||||
### 1.1 Monitoring Controller
|
||||
|
||||
**Location**: `src/Framework/LiveComponents/Controllers/LiveComponentMonitoringController.php`
|
||||
|
||||
**Dependencies**:
|
||||
- `CacheMetricsCollector` - Cache performance metrics
|
||||
- `ComponentRegistry` - Component statistics
|
||||
- `ComponentMetadataCache` - Metadata caching info
|
||||
- `ComponentStateCache` - State caching info
|
||||
- `ProcessorPerformanceTracker` - Optional template processor profiling
|
||||
|
||||
### 1.2 Monitoring Endpoints
|
||||
|
||||
#### GET `/api/livecomponents/metrics`
|
||||
**Auth**: Admin only (`#[Auth(roles: ['admin'])]`)
|
||||
|
||||
Comprehensive system metrics including:
|
||||
```json
|
||||
{
|
||||
"cache": {
|
||||
"overall": {
|
||||
"hit_rate": "85.50%",
|
||||
"miss_rate": "14.50%",
|
||||
"total_requests": 1000,
|
||||
"average_lookup_time_ms": 0.15
|
||||
},
|
||||
"by_type": {
|
||||
"state": { "hit_rate": "70.00%", ... },
|
||||
"slot": { "hit_rate": "60.00%", ... },
|
||||
"template": { "hit_rate": "80.00%", ... }
|
||||
},
|
||||
"performance_assessment": {
|
||||
"state_cache": { "grade": "B", "meets_target": true },
|
||||
"slot_cache": { "grade": "B", "meets_target": true },
|
||||
"template_cache": { "grade": "A", "meets_target": true },
|
||||
"overall_grade": "B+"
|
||||
}
|
||||
},
|
||||
"registry": {
|
||||
"total_components": 15,
|
||||
"component_names": ["counter", "timer", "chat", ...],
|
||||
"memory_estimate": 76800
|
||||
},
|
||||
"processors": {
|
||||
"enabled": true,
|
||||
"metrics": { ... }
|
||||
},
|
||||
"system": {
|
||||
"memory_usage": 12582912,
|
||||
"peak_memory": 15728640
|
||||
},
|
||||
"timestamp": 1696857600
|
||||
}
|
||||
```
|
||||
|
||||
**Use Cases**:
|
||||
- Production monitoring dashboards
|
||||
- Performance trend analysis
|
||||
- Capacity planning
|
||||
- Alerting integration
|
||||
|
||||
#### GET `/api/livecomponents/health`
|
||||
**Auth**: Public (no authentication required)
|
||||
|
||||
Quick health check for monitoring systems:
|
||||
```json
|
||||
{
|
||||
"status": "healthy", // or "degraded"
|
||||
"components": {
|
||||
"registry": true,
|
||||
"cache": true
|
||||
},
|
||||
"warnings": [],
|
||||
"timestamp": 1696857600
|
||||
}
|
||||
```
|
||||
|
||||
**HTTP Status Codes**:
|
||||
- `200 OK` - System healthy
|
||||
- `503 Service Unavailable` - System degraded
|
||||
|
||||
**Use Cases**:
|
||||
- Load balancer health checks
|
||||
- Uptime monitoring (Pingdom, UptimeRobot, etc.)
|
||||
- Auto-scaling triggers
|
||||
- Alerting systems
|
||||
|
||||
#### GET `/api/livecomponents/metrics/cache`
|
||||
**Auth**: Admin only
|
||||
|
||||
Focused cache metrics:
|
||||
```json
|
||||
{
|
||||
"overall": { ... },
|
||||
"by_type": { ... },
|
||||
"performance_assessment": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
#### GET `/api/livecomponents/metrics/registry`
|
||||
**Auth**: Admin only
|
||||
|
||||
Component registry statistics:
|
||||
```json
|
||||
{
|
||||
"total_components": 15,
|
||||
"component_names": [...],
|
||||
"memory_estimate": 76800
|
||||
}
|
||||
```
|
||||
|
||||
#### POST `/api/livecomponents/metrics/reset`
|
||||
**Auth**: Admin only
|
||||
**Environment**: Development only
|
||||
|
||||
Reset all collected metrics:
|
||||
```json
|
||||
{
|
||||
"message": "Metrics reset successfully",
|
||||
"timestamp": 1696857600
|
||||
}
|
||||
```
|
||||
|
||||
Returns `403 Forbidden` in production.
|
||||
|
||||
---
|
||||
|
||||
## 2. Component Inspector
|
||||
|
||||
### 2.1 Inspector Endpoints
|
||||
|
||||
#### GET `/api/livecomponents/inspect/{componentId}`
|
||||
**Auth**: Admin only
|
||||
|
||||
Detailed component inspection for debugging:
|
||||
```json
|
||||
{
|
||||
"component": {
|
||||
"id": "counter:demo",
|
||||
"name": "counter",
|
||||
"instance_id": "demo",
|
||||
"class": "App\\Application\\LiveComponents\\CounterComponent"
|
||||
},
|
||||
"metadata": {
|
||||
"properties": [
|
||||
{
|
||||
"name": "count",
|
||||
"type": "int",
|
||||
"nullable": false,
|
||||
"hasDefault": true
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"name": "increment",
|
||||
"parameters": []
|
||||
},
|
||||
{
|
||||
"name": "decrement",
|
||||
"parameters": []
|
||||
}
|
||||
],
|
||||
"constructor_params": ["id", "initialData"],
|
||||
"compiled_at": "2025-10-09 01:45:23"
|
||||
},
|
||||
"state": {
|
||||
"cached": true,
|
||||
"data": {
|
||||
"count": 5,
|
||||
"label": "My Counter"
|
||||
}
|
||||
},
|
||||
"cache_info": {
|
||||
"metadata_cached": true,
|
||||
"state_cached": true
|
||||
},
|
||||
"timestamp": 1696857600
|
||||
}
|
||||
```
|
||||
|
||||
**Use Cases**:
|
||||
- Runtime debugging
|
||||
- State inspection
|
||||
- Metadata verification
|
||||
- Cache status checking
|
||||
- Development troubleshooting
|
||||
|
||||
#### GET `/api/livecomponents/instances`
|
||||
**Auth**: Admin only
|
||||
|
||||
List all available component types:
|
||||
```json
|
||||
{
|
||||
"total": 15,
|
||||
"instances": [
|
||||
{
|
||||
"name": "counter",
|
||||
"class": "App\\Application\\LiveComponents\\CounterComponent",
|
||||
"metadata_cached": true
|
||||
},
|
||||
{
|
||||
"name": "timer",
|
||||
"class": "App\\Application\\LiveComponents\\TimerComponent",
|
||||
"metadata_cached": true
|
||||
}
|
||||
],
|
||||
"timestamp": 1696857600
|
||||
}
|
||||
```
|
||||
|
||||
**Use Cases**:
|
||||
- Component discovery
|
||||
- System overview
|
||||
- Debugging aid
|
||||
- Cache status overview
|
||||
|
||||
---
|
||||
|
||||
## 3. Development Debug Panel
|
||||
|
||||
### 3.1 Debug Panel Renderer
|
||||
|
||||
**Location**: `src/Framework/LiveComponents/Debug/DebugPanelRenderer.php`
|
||||
|
||||
**Features**:
|
||||
- Auto-rendered in development environment
|
||||
- Collapsible panel with component details
|
||||
- State inspection with JSON preview
|
||||
- Performance metrics (render time, memory usage)
|
||||
- Cache hit/miss indicators
|
||||
- Component metadata display
|
||||
- Zero overhead in production
|
||||
|
||||
### 3.2 Activation
|
||||
|
||||
**Environment Variables**:
|
||||
```bash
|
||||
# Option 1: Development environment
|
||||
APP_ENV=development
|
||||
|
||||
# Option 2: Explicit debug flag
|
||||
LIVECOMPONENT_DEBUG=true
|
||||
```
|
||||
|
||||
**Auto-Detection**:
|
||||
```php
|
||||
DebugPanelRenderer::shouldRender()
|
||||
// Returns true if APP_ENV=development OR LIVECOMPONENT_DEBUG=true
|
||||
```
|
||||
|
||||
### 3.3 Debug Panel Display
|
||||
|
||||
**Visual Example**:
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ 🔧 counter ▼ │
|
||||
├─────────────────────────────────────────────────────┤
|
||||
│ Component: App\Application\LiveComponents\... │
|
||||
│ Render Time: 2.35ms │
|
||||
│ Memory: 2.5 MB │
|
||||
│ Cache: ✅ HIT │
|
||||
│ │
|
||||
│ State: │
|
||||
│ { │
|
||||
│ "count": 5, │
|
||||
│ "label": "My Counter" │
|
||||
│ } │
|
||||
│ │
|
||||
│ Actions: increment, decrement, reset │
|
||||
│ Metadata: 3 properties, 3 actions │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Features**:
|
||||
- Click header to collapse/expand
|
||||
- Inline styles (no external CSS needed)
|
||||
- JSON-formatted state with syntax highlighting
|
||||
- Performance metrics
|
||||
- Cache status indicators
|
||||
|
||||
### 3.4 Integration
|
||||
|
||||
**Automatic Injection**:
|
||||
- Debug panel automatically appended after component rendering
|
||||
- Only in development environment
|
||||
- No code changes required in components
|
||||
- Fully transparent to production
|
||||
|
||||
**Component Registry Integration**:
|
||||
```php
|
||||
// In ComponentRegistry::render()
|
||||
if ($this->debugPanel !== null && DebugPanelRenderer::shouldRender()) {
|
||||
$renderTime = (microtime(true) - $startTime) * 1000;
|
||||
$html .= $this->renderDebugPanel($component, $renderTime, $cacheHit);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Metrics Collection
|
||||
|
||||
### 4.1 Cache Metrics Collector
|
||||
|
||||
**Location**: `src/Framework/LiveComponents/Cache/CacheMetricsCollector.php`
|
||||
|
||||
**Features**:
|
||||
- Real-time metric collection
|
||||
- Per-cache-type tracking (State, Slot, Template)
|
||||
- Aggregate metrics across all caches
|
||||
- Performance target validation
|
||||
- Automatic performance grading (A-F)
|
||||
|
||||
**Metrics Tracked**:
|
||||
```php
|
||||
public function recordHit(CacheType $cacheType, float $lookupTimeMs): void
|
||||
public function recordMiss(CacheType $cacheType, float $lookupTimeMs): void
|
||||
public function recordInvalidation(CacheType $cacheType): void
|
||||
public function updateSize(CacheType $cacheType, int $size): void
|
||||
```
|
||||
|
||||
**Performance Assessment**:
|
||||
```php
|
||||
$assessment = $collector->assessPerformance();
|
||||
// [
|
||||
// 'state_cache' => [
|
||||
// 'target' => '70.0%',
|
||||
// 'actual' => '85.5%',
|
||||
// 'meets_target' => true,
|
||||
// 'grade' => 'A'
|
||||
// ],
|
||||
// ...
|
||||
// ]
|
||||
```
|
||||
|
||||
**Performance Targets**:
|
||||
- State Cache: 70% hit rate (faster initialization)
|
||||
- Slot Cache: 60% hit rate (faster resolution)
|
||||
- Template Cache: 80% hit rate (faster rendering)
|
||||
|
||||
**Grading Scale**:
|
||||
- A: ≥90% hit rate
|
||||
- B: 80-89%
|
||||
- C: 70-79%
|
||||
- D: 60-69%
|
||||
- F: <60%
|
||||
|
||||
### 4.2 Performance Warnings
|
||||
|
||||
**Automatic Detection**:
|
||||
```php
|
||||
if ($collector->hasPerformanceIssues()) {
|
||||
$warnings = $collector->getPerformanceWarnings();
|
||||
// [
|
||||
// "State cache hit rate (65.2%) below target (70.0%)",
|
||||
// "Template cache hit rate (75.3%) below target (80.0%)"
|
||||
// ]
|
||||
}
|
||||
```
|
||||
|
||||
**Integration**:
|
||||
- Health check endpoint includes warnings
|
||||
- Monitoring alerts can trigger on warnings
|
||||
- Debug panel shows performance issues
|
||||
|
||||
---
|
||||
|
||||
## 5. Processor Performance Tracking
|
||||
|
||||
### 5.1 Performance Tracker
|
||||
|
||||
**Location**: `src/Framework/View/ProcessorPerformanceTracker.php`
|
||||
|
||||
**Features**:
|
||||
- Optional profiling (disable in production)
|
||||
- Minimal overhead (<0.1ms when enabled)
|
||||
- Per-processor metrics
|
||||
- Performance grading (A-F)
|
||||
- Bottleneck identification
|
||||
|
||||
**Activation**:
|
||||
```bash
|
||||
# Enable via environment variable
|
||||
ENABLE_TEMPLATE_PROFILING=true
|
||||
```
|
||||
|
||||
**Metrics Tracked**:
|
||||
```php
|
||||
public function measure(string $processorClass, callable $execution): string
|
||||
// Tracks:
|
||||
// - Execution time (ms)
|
||||
// - Memory usage (bytes)
|
||||
// - Invocation count
|
||||
// - Average/min/max times
|
||||
```
|
||||
|
||||
**Performance Report**:
|
||||
```php
|
||||
$report = $tracker->generateReport();
|
||||
// ProcessorPerformanceReport {
|
||||
// processors: [
|
||||
// 'PlaceholderReplacer' => [
|
||||
// 'total_time_ms' => 15.3,
|
||||
// 'invocation_count' => 100,
|
||||
// 'average_time_ms' => 0.153,
|
||||
// 'grade' => 'A'
|
||||
// ],
|
||||
// ...
|
||||
// ],
|
||||
// bottlenecks: ['ForProcessor'],
|
||||
// overall_grade: 'B+'
|
||||
// }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Usage Examples
|
||||
|
||||
### 6.1 Production Monitoring
|
||||
|
||||
**Prometheus/Grafana Integration**:
|
||||
```bash
|
||||
# Scrape metrics endpoint
|
||||
curl -s https://api.example.com/api/livecomponents/metrics \
|
||||
-H "Authorization: Bearer $ADMIN_TOKEN" \
|
||||
| jq '.cache.overall.hit_rate'
|
||||
```
|
||||
|
||||
**Health Check Monitoring**:
|
||||
```bash
|
||||
# Simple uptime check
|
||||
curl -f https://api.example.com/api/livecomponents/health || alert_team
|
||||
|
||||
# Detailed health with warnings
|
||||
curl -s https://api.example.com/api/livecomponents/health | jq '.warnings[]'
|
||||
```
|
||||
|
||||
**Alerting Rules**:
|
||||
```yaml
|
||||
# Prometheus alert rule
|
||||
- alert: LiveComponentsCacheDegraded
|
||||
expr: livecomponents_cache_hit_rate < 0.7
|
||||
for: 5m
|
||||
annotations:
|
||||
summary: "LiveComponents cache performance degraded"
|
||||
```
|
||||
|
||||
### 6.2 Development Debugging
|
||||
|
||||
**Component Inspection**:
|
||||
```bash
|
||||
# Inspect specific component instance
|
||||
curl https://localhost/api/livecomponents/inspect/counter:demo \
|
||||
-H "Authorization: Bearer $ADMIN_TOKEN" \
|
||||
| jq '.state.data'
|
||||
|
||||
# List all available components
|
||||
curl https://localhost/api/livecomponents/instances \
|
||||
-H "Authorization: Bearer $ADMIN_TOKEN" \
|
||||
| jq '.instances[].name'
|
||||
```
|
||||
|
||||
**Debug Panel**:
|
||||
```bash
|
||||
# Enable debug panel
|
||||
export APP_ENV=development
|
||||
|
||||
# Or via dedicated flag
|
||||
export LIVECOMPONENT_DEBUG=true
|
||||
|
||||
# Debug panel auto-appears in rendered components
|
||||
# Click panel header to collapse/expand
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Security Considerations
|
||||
|
||||
### 7.1 Authentication
|
||||
|
||||
**Admin-Only Endpoints**:
|
||||
- All monitoring endpoints require `admin` role
|
||||
- Health check endpoint is public (by design)
|
||||
- Component inspector admin-only
|
||||
- Metrics reset admin-only + development-only
|
||||
|
||||
**Authentication Pattern**:
|
||||
```php
|
||||
#[Route('/api/livecomponents/metrics', method: Method::GET)]
|
||||
#[Auth(roles: ['admin'])]
|
||||
public function metrics(): JsonResult
|
||||
```
|
||||
|
||||
### 7.2 Environment Restrictions
|
||||
|
||||
**Production Safety**:
|
||||
- Debug panel disabled in production (APP_ENV check)
|
||||
- Metrics reset blocked in production
|
||||
- Performance tracking optional (minimal overhead)
|
||||
|
||||
**Development Features**:
|
||||
- Debug panel auto-enabled in development
|
||||
- Metrics reset available
|
||||
- Component inspector with full details
|
||||
|
||||
---
|
||||
|
||||
## 8. Performance Impact
|
||||
|
||||
### 8.1 Production Overhead
|
||||
|
||||
**Metrics Collection**:
|
||||
- **Memory**: ~5KB per component metadata
|
||||
- **CPU**: <0.1ms per metric recording
|
||||
- **Storage**: In-memory metrics (no persistence)
|
||||
|
||||
**Health Check Endpoint**:
|
||||
- **Response Time**: <10ms
|
||||
- **Memory**: Negligible
|
||||
- **CPU**: Minimal
|
||||
|
||||
**Monitoring Endpoints**:
|
||||
- **Response Time**: 50-100ms (includes metric aggregation)
|
||||
- **Memory**: Temporary allocation for JSON serialization
|
||||
- **CPU**: Metric calculation and formatting
|
||||
|
||||
### 8.2 Development Overhead
|
||||
|
||||
**Debug Panel**:
|
||||
- **Render Time**: +1-2ms per component
|
||||
- **Memory**: +10KB per component (metadata + panel HTML)
|
||||
- **Zero Overhead**: Completely disabled in production
|
||||
|
||||
**Component Inspector**:
|
||||
- **Query Time**: 10-50ms (metadata + state lookup)
|
||||
- **Memory**: Temporary allocation
|
||||
- **No Impact**: On-demand only
|
||||
|
||||
---
|
||||
|
||||
## 9. Integration with Performance Optimizations
|
||||
|
||||
### 9.1 Metrics Integration
|
||||
|
||||
**Cache Metrics**:
|
||||
- ComponentMetadataCache reports to CacheMetricsCollector
|
||||
- ComponentStateCache reports to CacheMetricsCollector
|
||||
- SlotContentCache reports to CacheMetricsCollector
|
||||
- TemplateFragmentCache reports to CacheMetricsCollector
|
||||
|
||||
**Performance Tracking**:
|
||||
- ProcessorPerformanceTracker integrates with TemplateProcessor
|
||||
- Optional profiling via environment variable
|
||||
- Minimal overhead when disabled
|
||||
|
||||
### 9.2 Debug Integration
|
||||
|
||||
**Debug Panel Data Sources**:
|
||||
- ComponentMetadataCache for metadata
|
||||
- ComponentStateCache for state
|
||||
- Render timing from ComponentRegistry
|
||||
- Memory usage from PHP runtime
|
||||
|
||||
**Component Inspector**:
|
||||
- ComponentMetadataCache for structure
|
||||
- ComponentStateCache for runtime state
|
||||
- ComponentRegistry for class mapping
|
||||
|
||||
---
|
||||
|
||||
## 10. Future Enhancements
|
||||
|
||||
### 10.1 Planned Features
|
||||
|
||||
**Metrics Persistence**:
|
||||
- Store metrics in database for historical analysis
|
||||
- Metric retention policies
|
||||
- Trend analysis and visualization
|
||||
|
||||
**Advanced Alerting**:
|
||||
- Custom alert rules
|
||||
- Slack/Email notifications
|
||||
- Automated incident creation
|
||||
|
||||
**Component Profiler**:
|
||||
- Detailed performance profiling per component
|
||||
- Flame graphs for render pipeline
|
||||
- Bottleneck identification
|
||||
|
||||
**Interactive Debug UI**:
|
||||
- Web-based debug panel (alternative to inline)
|
||||
- State manipulation
|
||||
- Action testing
|
||||
- Component playground
|
||||
|
||||
### 10.2 Integration Opportunities
|
||||
|
||||
**APM Integration**:
|
||||
- New Relic integration
|
||||
- Datadog integration
|
||||
- Elastic APM integration
|
||||
|
||||
**Logging Integration**:
|
||||
- Structured logging for all metrics
|
||||
- Log aggregation (ELK, Splunk)
|
||||
- Metric-to-log correlation
|
||||
|
||||
---
|
||||
|
||||
## 11. Troubleshooting
|
||||
|
||||
### 11.1 Common Issues
|
||||
|
||||
**Metrics Not Updating**:
|
||||
```bash
|
||||
# Check if metrics collector is registered
|
||||
curl https://localhost/api/livecomponents/metrics/cache | jq '.overall.total_requests'
|
||||
|
||||
# Reset metrics (development only)
|
||||
curl -X POST https://localhost/api/livecomponents/metrics/reset \
|
||||
-H "Authorization: Bearer $ADMIN_TOKEN"
|
||||
```
|
||||
|
||||
**Debug Panel Not Showing**:
|
||||
```bash
|
||||
# Verify environment
|
||||
echo $APP_ENV # Should be "development"
|
||||
echo $LIVECOMPONENT_DEBUG # Should be "true"
|
||||
|
||||
# Check DebugPanelRenderer registration
|
||||
# Should be auto-registered via DebugPanelInitializer
|
||||
```
|
||||
|
||||
**Health Check Failing**:
|
||||
```bash
|
||||
# Check detailed health status
|
||||
curl -s https://localhost/api/livecomponents/health | jq '.'
|
||||
|
||||
# Check warnings
|
||||
curl -s https://localhost/api/livecomponents/health | jq '.warnings'
|
||||
```
|
||||
|
||||
### 11.2 Performance Degradation
|
||||
|
||||
**Cache Hit Rate Low**:
|
||||
- Check cache TTL configuration
|
||||
- Verify cache key generation
|
||||
- Review cache invalidation patterns
|
||||
- Analyze workload patterns
|
||||
|
||||
**High Memory Usage**:
|
||||
- Check component count (registry statistics)
|
||||
- Review metadata cache size
|
||||
- Analyze state cache retention
|
||||
- Consider cache eviction policies
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
Comprehensive monitoring and debugging infrastructure providing:
|
||||
|
||||
**Production**:
|
||||
- ✅ Metrics endpoint (cache, registry, performance)
|
||||
- ✅ Health check endpoint (200/503 responses)
|
||||
- ✅ Cache metrics collection with grading
|
||||
- ✅ Performance tracking (optional)
|
||||
- ✅ Admin-only security
|
||||
|
||||
**Development**:
|
||||
- ✅ Debug panel (auto-rendered)
|
||||
- ✅ Component inspector (detailed runtime info)
|
||||
- ✅ Component instance listing
|
||||
- ✅ Metrics reset capability
|
||||
- ✅ Zero production overhead
|
||||
|
||||
**Integration**:
|
||||
- ✅ Works with all performance optimizations
|
||||
- ✅ Integrates with cache layers
|
||||
- ✅ Hooks into component registry
|
||||
- ✅ Template processor profiling support
|
||||
- ✅ Framework-compliant patterns
|
||||
@@ -1,540 +0,0 @@
|
||||
# LiveComponents Observability System
|
||||
|
||||
**Complete observability, metrics, and debugging infrastructure for LiveComponents.**
|
||||
|
||||
## Overview
|
||||
|
||||
The Observability system provides comprehensive monitoring, debugging, and performance analysis for LiveComponents through:
|
||||
|
||||
1. **Backend Metrics Collection** - ComponentMetricsCollector for server-side tracking
|
||||
2. **Frontend DevTools** - Interactive debugging overlay with real-time insights
|
||||
3. **Performance Profiling** - Execution timeline, flamegraph, and memory tracking
|
||||
4. **DOM Badges** - Visual component identification on the page
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ LiveComponent Lifecycle │
|
||||
└────────────────────┬────────────────────────────────────────────┘
|
||||
│
|
||||
┌───────────┴───────────┐
|
||||
│ │
|
||||
┌────▼────┐ ┌──────▼──────┐
|
||||
│ Backend │ │ Frontend │
|
||||
│ Metrics │ │ DevTools │
|
||||
└────┬────┘ └──────┬──────┘
|
||||
│ │
|
||||
┌────▼────────────┐ ┌───▼─────────────────┐
|
||||
│ ComponentMetrics │ │ LiveComponentDevTools│
|
||||
│ Collector │ │ (Overlay) │
|
||||
│ │ │ │
|
||||
│ - Render times │ │ - Component Tree │
|
||||
│ - Action metrics │ │ - Action Log │
|
||||
│ - Cache stats │ │ - Event Log │
|
||||
│ - Event tracking │ │ - Network Log │
|
||||
│ - Upload metrics │ │ - Performance Tab │
|
||||
│ - Batch ops │ │ - DOM Badges │
|
||||
└──────────────────┘ └──────────────────────┘
|
||||
```
|
||||
|
||||
## Backend Metrics Collection
|
||||
|
||||
### ComponentMetricsCollector
|
||||
|
||||
**Location**: `src/Framework/LiveComponents/Observability/ComponentMetricsCollector.php`
|
||||
|
||||
**Purpose**: Server-side metrics collection for component performance and behavior tracking.
|
||||
|
||||
**Metrics Categories**:
|
||||
|
||||
1. **Render Metrics**
|
||||
- `livecomponent_renders_total` - Total component renders (cached/non-cached)
|
||||
- `livecomponent_render_duration_ms` - Render duration histogram
|
||||
|
||||
2. **Action Metrics**
|
||||
- `livecomponent_actions_total` - Total actions executed (success/error)
|
||||
- `livecomponent_action_duration_ms` - Action execution time histogram
|
||||
- `livecomponent_action_errors_total` - Failed action count
|
||||
|
||||
3. **Cache Metrics**
|
||||
- `livecomponent_cache_hits_total` - Cache hit count
|
||||
- `livecomponent_cache_misses_total` - Cache miss count
|
||||
- Cache hit rate calculated in summary
|
||||
|
||||
4. **Event Metrics**
|
||||
- `livecomponent_events_dispatched_total` - Events dispatched by components
|
||||
- `livecomponent_events_received_total` - Events received by components
|
||||
|
||||
5. **Hydration Metrics**
|
||||
- `livecomponent_hydration_duration_ms` - Client-side hydration time
|
||||
|
||||
6. **Batch Operations**
|
||||
- `livecomponent_batch_operations_total` - Batch operations executed
|
||||
- `livecomponent_batch_success_total` - Successful batch items
|
||||
- `livecomponent_batch_failure_total` - Failed batch items
|
||||
- `livecomponent_batch_size` - Batch size histogram
|
||||
- `livecomponent_batch_duration_ms` - Batch execution time
|
||||
|
||||
7. **Fragment Updates**
|
||||
- `livecomponent_fragment_updates_total` - Fragment update count
|
||||
- `livecomponent_fragment_count` - Fragments per update
|
||||
- `livecomponent_fragment_duration_ms` - Fragment update duration
|
||||
|
||||
8. **Upload Metrics**
|
||||
- `livecomponent_upload_chunks_total` - Upload chunks processed
|
||||
- `livecomponent_upload_chunk_duration_ms` - Chunk upload time
|
||||
- `livecomponent_uploads_completed_total` - Complete uploads
|
||||
- `livecomponent_upload_total_duration_ms` - Total upload duration
|
||||
- `livecomponent_upload_chunk_count` - Chunks per upload
|
||||
|
||||
### Usage Example
|
||||
|
||||
```php
|
||||
use App\Framework\LiveComponents\Observability\ComponentMetricsCollector;
|
||||
|
||||
final readonly class LiveComponentService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ComponentMetricsCollector $metricsCollector
|
||||
) {}
|
||||
|
||||
public function renderComponent(string $componentId): string
|
||||
{
|
||||
$startTime = microtime(true);
|
||||
|
||||
// Render logic
|
||||
$html = $this->renderer->render($componentId);
|
||||
|
||||
$duration = (microtime(true) - $startTime) * 1000; // milliseconds
|
||||
$this->metricsCollector->recordRender($componentId, $duration, $cached = false);
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
public function executeAction(string $componentId, string $actionName): array
|
||||
{
|
||||
$startTime = microtime(true);
|
||||
|
||||
try {
|
||||
$result = $this->actionExecutor->execute($componentId, $actionName);
|
||||
$duration = (microtime(true) - $startTime) * 1000;
|
||||
|
||||
$this->metricsCollector->recordAction($componentId, $actionName, $duration, true);
|
||||
|
||||
return $result;
|
||||
} catch (\Exception $e) {
|
||||
$duration = (microtime(true) - $startTime) * 1000;
|
||||
$this->metricsCollector->recordAction($componentId, $actionName, $duration, false);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Metrics Export
|
||||
|
||||
**Summary Statistics**:
|
||||
```php
|
||||
$summary = $metricsCollector->getSummary();
|
||||
// Returns:
|
||||
// [
|
||||
// 'total_renders' => 150,
|
||||
// 'total_actions' => 45,
|
||||
// 'cache_hits' => 100,
|
||||
// 'cache_misses' => 50,
|
||||
// 'total_events' => 30,
|
||||
// 'action_errors' => 2,
|
||||
// 'avg_render_time_ms' => 25.5,
|
||||
// 'avg_action_time_ms' => 15.3,
|
||||
// 'cache_hit_rate' => 66.67
|
||||
// ]
|
||||
```
|
||||
|
||||
**Prometheus Export**:
|
||||
```php
|
||||
$prometheus = $metricsCollector->exportPrometheus();
|
||||
// Returns Prometheus-formatted metrics:
|
||||
// # HELP LiveComponents metrics
|
||||
// # TYPE livecomponent_* counter/histogram
|
||||
//
|
||||
// livecomponent_renders_total{component_id=user-profile,cached=false} 45.00 1704067200
|
||||
// livecomponent_render_duration_ms{component_id=user-profile,cached=false} 25.50 1704067200
|
||||
// ...
|
||||
```
|
||||
|
||||
### Performance Integration
|
||||
|
||||
The ComponentMetricsCollector integrates with the Framework's PerformanceCollector:
|
||||
|
||||
```php
|
||||
$collector = new ComponentMetricsCollector($performanceCollector);
|
||||
|
||||
$collector->recordRender('comp-123', 45.5, false);
|
||||
// Also records to PerformanceCollector:
|
||||
// - Metric name: "livecomponent.render.comp-123"
|
||||
// - Category: RENDERING
|
||||
// - Duration: 45.5ms
|
||||
// - Metadata: ['cached' => false]
|
||||
```
|
||||
|
||||
## Frontend DevTools
|
||||
|
||||
### LiveComponentDevTools
|
||||
|
||||
**Location**: `resources/js/modules/LiveComponentDevTools.js`
|
||||
|
||||
**Purpose**: Interactive debugging overlay for real-time component monitoring and analysis.
|
||||
|
||||
**Features**:
|
||||
|
||||
1. **Component Tree Tab**
|
||||
- Hierarchical view of all active components
|
||||
- Component state inspection
|
||||
- Real-time component count
|
||||
- Component selection for detailed view
|
||||
|
||||
2. **Actions Tab**
|
||||
- Chronological action execution log
|
||||
- Action name, componentId, duration, timestamp
|
||||
- Success/failure status
|
||||
- Filter by component or action name
|
||||
- Clear log functionality
|
||||
- Export as JSON
|
||||
|
||||
3. **Events Tab**
|
||||
- Event dispatch and receive tracking
|
||||
- Event name, source, timestamp
|
||||
- Event data inspection
|
||||
- Filter by event type
|
||||
|
||||
4. **Network Tab**
|
||||
- HTTP request tracking
|
||||
- Method, URL, status code, duration
|
||||
- Request/response body inspection
|
||||
- Filter by status or method
|
||||
- Performance analysis
|
||||
|
||||
5. **Performance Tab** (NEW)
|
||||
- **Recording Controls**: Start/stop performance profiling
|
||||
- **Summary Statistics**: Total events, actions, renders, avg times
|
||||
- **Flamegraph**: Execution breakdown by component/action
|
||||
- **Timeline**: Chronological execution visualization
|
||||
- **Memory Chart**: JavaScript heap usage over time
|
||||
|
||||
### Performance Profiling
|
||||
|
||||
**Recording**:
|
||||
```javascript
|
||||
// Toggle recording with button or programmatically
|
||||
devTools.togglePerformanceRecording();
|
||||
|
||||
// Recording captures:
|
||||
// - Action execution times
|
||||
// - Component render times
|
||||
// - Memory snapshots (every 500ms)
|
||||
// - Execution timeline data
|
||||
```
|
||||
|
||||
**Data Structures**:
|
||||
```javascript
|
||||
// Performance recording entry
|
||||
{
|
||||
type: 'action' | 'render',
|
||||
componentId: 'comp-abc-123',
|
||||
actionName: 'handleClick', // for actions only
|
||||
duration: 25.5, // milliseconds
|
||||
startTime: 1000.0, // performance.now()
|
||||
endTime: 1025.5, // performance.now()
|
||||
timestamp: 1704067200 // Date.now()
|
||||
}
|
||||
|
||||
// Memory snapshot
|
||||
{
|
||||
timestamp: 1704067200,
|
||||
usedJSHeapSize: 25000000,
|
||||
totalJSHeapSize: 50000000,
|
||||
jsHeapSizeLimit: 2000000000
|
||||
}
|
||||
```
|
||||
|
||||
**Visualizations**:
|
||||
|
||||
1. **Flamegraph** - Top 10 most expensive operations
|
||||
- Horizontal bars showing total execution time
|
||||
- Execution count (×N)
|
||||
- Average time per execution
|
||||
- Color coded: Actions (yellow), Renders (blue)
|
||||
|
||||
2. **Timeline** - Chronological execution view
|
||||
- Horizontal bars showing when and how long
|
||||
- Stacked vertically for readability
|
||||
- Time labels (0ms, middle, end)
|
||||
- Limited to 12 concurrent events for clarity
|
||||
|
||||
3. **Memory Chart** - Memory usage over time
|
||||
- Used Heap, Total Heap, Heap Limit, Delta
|
||||
- SVG line chart visualization
|
||||
- Color coded delta (green = reduction, red = increase)
|
||||
|
||||
### DOM Badges
|
||||
|
||||
**Purpose**: Visual component identification directly on the page.
|
||||
|
||||
**Features**:
|
||||
- Badge shows component name and truncated ID
|
||||
- Action counter updates in real-time
|
||||
- Click badge to focus component in DevTools
|
||||
- Hover badge to highlight component with blue outline
|
||||
- Toggle visibility with "⚡ Badges" button
|
||||
|
||||
**Badge Appearance**:
|
||||
- Dark semi-transparent background (#1e1e1e, 95% opacity)
|
||||
- Blue border (#007acc) changing to green (#4ec9b0) on hover
|
||||
- Backdrop filter (4px blur) for modern glass-morphism
|
||||
- Positioned at top-left of component element
|
||||
|
||||
**Badge Content**:
|
||||
```
|
||||
⚡ UserProfile (comp-abc1...)
|
||||
Actions: 5
|
||||
```
|
||||
|
||||
**Auto-Management**:
|
||||
- Created when component initializes
|
||||
- Updated when actions execute
|
||||
- Removed when component destroyed
|
||||
- Position updates on DOM changes (MutationObserver)
|
||||
|
||||
### Keyboard Shortcuts
|
||||
|
||||
- **Ctrl+Shift+D**: Toggle DevTools visibility
|
||||
|
||||
### DevTools Initialization
|
||||
|
||||
**Automatic** (Development only):
|
||||
```html
|
||||
<html data-env="development">
|
||||
<!-- DevTools automatically initializes -->
|
||||
</html>
|
||||
```
|
||||
|
||||
**Manual**:
|
||||
```javascript
|
||||
import { LiveComponentDevTools } from './modules/LiveComponentDevTools.js';
|
||||
|
||||
const devTools = new LiveComponentDevTools();
|
||||
// Auto-initializes if data-env="development"
|
||||
```
|
||||
|
||||
## Integration with LiveComponent Lifecycle
|
||||
|
||||
### Event-Driven Architecture
|
||||
|
||||
The Observability system integrates via custom events:
|
||||
|
||||
```javascript
|
||||
// Component Initialization
|
||||
document.dispatchEvent(new CustomEvent('livecomponent:registered', {
|
||||
detail: {
|
||||
componentId: 'comp-abc-123',
|
||||
componentName: 'UserProfile',
|
||||
initialState: { userId: 1, name: 'John' }
|
||||
}
|
||||
}));
|
||||
|
||||
// Action Execution
|
||||
document.dispatchEvent(new CustomEvent('livecomponent:action', {
|
||||
detail: {
|
||||
componentId: 'comp-abc-123',
|
||||
actionName: 'handleClick',
|
||||
startTime: 1000.0,
|
||||
endTime: 1025.5,
|
||||
duration: 25.5,
|
||||
success: true
|
||||
}
|
||||
}));
|
||||
|
||||
// Component Destruction
|
||||
document.dispatchEvent(new CustomEvent('livecomponent:destroyed', {
|
||||
detail: {
|
||||
componentId: 'comp-abc-123'
|
||||
}
|
||||
}));
|
||||
```
|
||||
|
||||
### Backend Integration
|
||||
|
||||
```php
|
||||
// In LiveComponent implementation
|
||||
final class UserProfileComponent
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ComponentMetricsCollector $metrics
|
||||
) {}
|
||||
|
||||
public function render(): string
|
||||
{
|
||||
$start = microtime(true);
|
||||
|
||||
$html = $this->renderTemplate();
|
||||
|
||||
$duration = (microtime(true) - $start) * 1000;
|
||||
$this->metrics->recordRender($this->componentId, $duration, false);
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
public function handleAction(string $actionName): array
|
||||
{
|
||||
$start = microtime(true);
|
||||
|
||||
try {
|
||||
$result = $this->executeAction($actionName);
|
||||
|
||||
$duration = (microtime(true) - $start) * 1000;
|
||||
$this->metrics->recordAction($this->componentId, $actionName, $duration, true);
|
||||
|
||||
return $result;
|
||||
} catch (\Exception $e) {
|
||||
$duration = (microtime(true) - $start) * 1000;
|
||||
$this->metrics->recordAction($this->componentId, $actionName, $duration, false);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Backend Tests
|
||||
|
||||
**Location**: `tests/Framework/LiveComponents/Observability/ComponentMetricsCollectorSimpleTest.php`
|
||||
|
||||
**Coverage**:
|
||||
- ✅ Metrics recording (render, action, cache, events, etc.)
|
||||
- ✅ Summary statistics calculation
|
||||
- ✅ Prometheus export format
|
||||
- ✅ Cache hit rate calculation
|
||||
- ✅ Error tracking
|
||||
- ✅ Multiple component tracking
|
||||
- ✅ Reset functionality
|
||||
- ✅ Edge cases (zero operations, all hits/misses)
|
||||
|
||||
**Run Tests**:
|
||||
```bash
|
||||
docker exec php ./vendor/bin/pest tests/Framework/LiveComponents/Observability/
|
||||
```
|
||||
|
||||
**Test Results**: ✅ 20 passed (35 assertions)
|
||||
|
||||
### Frontend Tests
|
||||
|
||||
**Location**: `tests/Feature/LiveComponentDevToolsTest.php`
|
||||
|
||||
**Coverage**:
|
||||
- ✅ DevTools initialization (development/production)
|
||||
- ✅ Component tracking (initialization, actions, state, destruction)
|
||||
- ✅ Network logging
|
||||
- ✅ Action log filtering and export
|
||||
- ✅ DOM badge management
|
||||
- ✅ Performance recording
|
||||
- ✅ Memory snapshots
|
||||
- ✅ Data aggregation
|
||||
- ✅ Byte formatting
|
||||
- ✅ Performance summary calculation
|
||||
|
||||
## Performance Characteristics
|
||||
|
||||
### Backend Metrics
|
||||
|
||||
**Memory Usage**:
|
||||
- ComponentMetricsCollector: ~50KB base memory
|
||||
- Per metric: ~1KB (including labels and metadata)
|
||||
- Typical project: ~500KB for 500 metrics
|
||||
|
||||
**Performance Impact**:
|
||||
- Metric recording: <0.1ms per operation
|
||||
- Summary calculation: <5ms for 1000 metrics
|
||||
- Prometheus export: <10ms for 1000 metrics
|
||||
|
||||
**Prometheus Integration**: ✅ Full Prometheus format support for external monitoring
|
||||
|
||||
### Frontend DevTools
|
||||
|
||||
**Memory Usage**:
|
||||
- DevTools overlay: ~100KB base
|
||||
- Per component: ~2KB in tree
|
||||
- Per action log entry: ~500 bytes
|
||||
- Performance recording (100 entries): ~50KB
|
||||
|
||||
**Performance Impact**:
|
||||
- Event listener overhead: <0.01ms per event
|
||||
- Badge rendering: <1ms per badge
|
||||
- DOM mutation observer: Batched, minimal impact
|
||||
- Performance recording: <0.1ms per recording entry
|
||||
|
||||
**Data Limits**:
|
||||
- Action log: Last 100 entries auto-trimmed
|
||||
- Performance recording: Last 100 entries
|
||||
- Memory snapshots: Last 100 snapshots
|
||||
- Prevents unbounded memory growth
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Backend Metrics
|
||||
|
||||
1. **Selective Recording**: Record only relevant metrics in production
|
||||
2. **Batch Operations**: Use batch recording methods for multiple operations
|
||||
3. **Regular Reset**: Reset metrics periodically to prevent memory buildup
|
||||
4. **Export Strategy**: Export to monitoring systems (Prometheus, Grafana) regularly
|
||||
|
||||
### Frontend DevTools
|
||||
|
||||
1. **Development Only**: Never enable in production (data-env check)
|
||||
2. **Performance Recording**: Use recording only when actively debugging
|
||||
3. **Badge Visibility**: Disable badges when not needed to reduce DOM overhead
|
||||
4. **Log Management**: Clear logs regularly during long debugging sessions
|
||||
5. **Export Data**: Export action logs for offline analysis
|
||||
|
||||
### Integration
|
||||
|
||||
1. **Event Consistency**: Use standard event names for consistent tracking
|
||||
2. **Error Handling**: Always record failed actions with proper error context
|
||||
3. **Component Naming**: Use descriptive component names for easier debugging
|
||||
4. **Action Granularity**: Keep actions focused and well-named
|
||||
|
||||
## Color Scheme (VS Code Dark Theme)
|
||||
|
||||
All visualizations use consistent VS Code dark theme colors:
|
||||
|
||||
- **Primary Blue** (#569cd6): Component names, borders
|
||||
- **Yellow** (#dcdcaa): Actions, metrics, numbers
|
||||
- **Green** (#4ec9b0): Success, cache hits, memory reduction
|
||||
- **Red** (#f48771): Errors, failures, memory increase
|
||||
- **Gray** (#858585): Subdued text, labels
|
||||
- **Dark Background** (#1e1e1e): Panels, overlays
|
||||
- **Border** (#3c3c3c): Separators, containers
|
||||
|
||||
## Summary
|
||||
|
||||
The LiveComponents Observability system provides:
|
||||
|
||||
✅ **Comprehensive Metrics** - Backend tracking of all component operations
|
||||
✅ **Real-Time Debugging** - Interactive DevTools overlay with 5 tabs
|
||||
✅ **Performance Profiling** - Flamegraph, timeline, and memory analysis
|
||||
✅ **Visual Identification** - DOM badges for quick component location
|
||||
✅ **Production Ready** - Prometheus export and performance optimized
|
||||
✅ **Developer Experience** - Keyboard shortcuts, filtering, export
|
||||
✅ **Fully Tested** - 20 backend tests, integration tests
|
||||
✅ **Framework Integration** - Event-driven, lifecycle-aware
|
||||
|
||||
This system enables developers to:
|
||||
- Monitor component performance in real-time
|
||||
- Debug issues with comprehensive logging
|
||||
- Analyze performance bottlenecks with profiling tools
|
||||
- Track metrics for production monitoring
|
||||
- Visualize component hierarchy and relationships
|
||||
- Export data for offline analysis
|
||||
@@ -1,725 +0,0 @@
|
||||
# LiveComponents Performance Optimizations
|
||||
|
||||
Umfassende Dokumentation aller Performance-Optimierungen für das LiveComponents-System.
|
||||
|
||||
## Übersicht
|
||||
|
||||
Das LiveComponents-System wurde für **maximale Performance** optimiert mit messbaren Verbesserungen in allen kritischen Bereichen:
|
||||
|
||||
**Gesamt-Performance-Steigerung**: ~70-80% schnelleres Component Rendering
|
||||
|
||||
**Validiert durch Benchmarks**: Alle Performance-Claims wurden durch automatisierte Benchmarks in `tests/Performance/LiveComponentsPerformanceBenchmark.php` validiert.
|
||||
|
||||
## Optimization Layers
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Layer 1: Component Registry (Metadata Cache) │
|
||||
│ ~90% faster registration, ~85% faster lookup │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Layer 2: Multi-Layer Caching (State/Slot/Template) │
|
||||
│ ~70% faster init, ~60% faster slots, ~80% faster templates │
|
||||
└─────────────────────────────────────────────────────────────┐
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Layer 3: Template Processing Optimization │
|
||||
│ ~30-40% faster via processor chain optimization │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Layer 4: Smart Invalidation & Memory Management │
|
||||
│ Minimal cache clearing, efficient memory usage │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 1. Component Registry Optimizations
|
||||
|
||||
### Compiled Metadata Caching
|
||||
|
||||
**Problem**: Reflection overhead bei jedem Component-Zugriff (~5-10ms pro Component)
|
||||
|
||||
**Solution**: `ComponentMetadataCache` mit vorcompilierten Metadata
|
||||
|
||||
**Performance Gain**: ~99% faster metadata access (5-10ms → ~0.01ms)
|
||||
|
||||
**Implementation**:
|
||||
```php
|
||||
// ComponentMetadataCache warmup beim Registry-Init
|
||||
private function buildNameMap(): array
|
||||
{
|
||||
// ... build map ...
|
||||
|
||||
// Batch warm metadata cache (~85% faster)
|
||||
if (!empty($classNames)) {
|
||||
$this->metadataCache->warmCache($classNames);
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
|
||||
// Fast metadata access - no reflection
|
||||
$metadata = $registry->getMetadata('counter');
|
||||
$hasAction = $metadata->hasAction('increment'); // ~0.01ms
|
||||
```
|
||||
|
||||
**Key Features**:
|
||||
- Pre-compiled properties, actions, constructor params
|
||||
- Automatic staleness detection via file modification time
|
||||
- 24-hour TTL (metadata changes rarely)
|
||||
- Batch operations for registry initialization
|
||||
- ~5KB memory per cached component
|
||||
|
||||
### Metadata Components
|
||||
|
||||
**CompiledComponentMetadata**:
|
||||
- Component name and class name
|
||||
- Public properties with types
|
||||
- Action methods with signatures
|
||||
- Constructor parameters
|
||||
- Attributes
|
||||
- Compiled timestamp for staleness check
|
||||
|
||||
**ComponentMetadataCompiler**:
|
||||
- One-time reflection per component
|
||||
- Extracts all metadata upfront
|
||||
- Batch compilation support
|
||||
|
||||
**ComponentMetadataCache**:
|
||||
- Long TTL caching (24h)
|
||||
- Automatic staleness detection
|
||||
- Batch warmup for startup performance
|
||||
|
||||
### Performance Metrics
|
||||
|
||||
| Operation | Before | After | Improvement |
|
||||
|-----------|--------|-------|-------------|
|
||||
| Component Registration | 5-10ms | 0.5ms | **90% faster** |
|
||||
| Metadata Lookup | 2-3ms | 0.01ms | **99% faster** |
|
||||
| Registry Initialization | 50-100ms | 5-10ms | **85% faster** |
|
||||
|
||||
## 2. Multi-Layer Caching System
|
||||
|
||||
### Component State Cache
|
||||
|
||||
**Performance**: ~70% faster component initialization
|
||||
|
||||
**Key Optimizations**:
|
||||
- State hash-based cache keys
|
||||
- Auto-TTL per component type (counter: 5min, layout: 24h)
|
||||
- Lazy invalidation with timestamps
|
||||
- Batch state operations
|
||||
|
||||
```php
|
||||
// Auto-optimized TTL
|
||||
$cache->storeWithAutoTTL($componentId, $state, 'counter');
|
||||
|
||||
// Fast retrieval
|
||||
$cachedState = $cache->retrieve($componentId, $currentState);
|
||||
// ~70% faster than fresh initialization
|
||||
```
|
||||
|
||||
### Slot Content Cache
|
||||
|
||||
**Performance**: ~60% faster slot resolution
|
||||
|
||||
**Key Optimizations**:
|
||||
- Content hash-based automatic invalidation
|
||||
- Batch operations for multiple slots
|
||||
- Shared slot caching across component instances
|
||||
|
||||
```php
|
||||
// Batch store - single cache operation
|
||||
$cache->storeBatch($componentId, [
|
||||
'header' => $headerHtml,
|
||||
'footer' => $footerHtml,
|
||||
'sidebar' => $sidebarHtml
|
||||
]);
|
||||
|
||||
// Batch retrieve - ~60% faster
|
||||
$cachedSlots = $cache->getBatch($componentId, $slotNames);
|
||||
```
|
||||
|
||||
### Template Fragment Cache
|
||||
|
||||
**Performance**: ~80% faster template rendering
|
||||
|
||||
**Key Optimizations**:
|
||||
- Remember pattern for elegant caching
|
||||
- Static template caching (layouts, headers)
|
||||
- Data hash-based auto-invalidation
|
||||
- Variant support for template variations
|
||||
|
||||
```php
|
||||
// Remember pattern - cache-aware rendering
|
||||
$html = $cache->remember(
|
||||
componentType: 'card',
|
||||
data: $templateData,
|
||||
callback: fn() => $this->renderer->render('card.view.php', $templateData),
|
||||
ttl: Duration::fromHours(2)
|
||||
);
|
||||
// ~80% faster on cache hit
|
||||
```
|
||||
|
||||
### Cache Performance Metrics
|
||||
|
||||
| Cache Type | Hit Rate Target | Typical Hit Rate | Performance Gain |
|
||||
|------------|-----------------|------------------|------------------|
|
||||
| State | 70%+ | 85-90% | ~70% faster |
|
||||
| Slot | 60%+ | 70-80% | ~60% faster |
|
||||
| Template | 80%+ | 90-95% | ~80% faster |
|
||||
|
||||
## 3. Template Processing Optimization
|
||||
|
||||
### Processor Chain Optimization
|
||||
|
||||
**Performance**: ~30-40% schnellere Template-Verarbeitung durch optimierte Processor-Reihenfolge
|
||||
|
||||
**Key Optimizations**:
|
||||
- Template-Content-Analyse für Processor-Relevanz
|
||||
- Dynamische Processor-Reihenfolge basierend auf Template-Features
|
||||
- Früher Exit für irrelevante Processors
|
||||
- Cached Processor-Order (24h TTL)
|
||||
|
||||
```php
|
||||
// Automatische Processor-Optimierung im TemplateProcessor
|
||||
$processors = $this->chainOptimizer !== null
|
||||
? $this->optimizeProcessorChain($this->stringProcessors, $html)
|
||||
: $this->stringProcessors;
|
||||
|
||||
// Optimierte Reihenfolge: Häufig verwendete Processors zuerst
|
||||
// 1. PlaceholderReplacer (Score: 100 + placeholder_count)
|
||||
// 2. IfProcessor (Score: 80 + if_count * 5)
|
||||
// 3. ForProcessor (Score: 70 + for_count * 10)
|
||||
// 4. ComponentProcessor (Score: 60 + component_count * 8)
|
||||
// ...
|
||||
```
|
||||
|
||||
**Processor Scoring Strategy**:
|
||||
- **Häufigkeit**: Häufig verwendete Processors bekommen höheren Score
|
||||
- **Performance**: Schnelle Processors werden bevorzugt
|
||||
- **Relevanz**: Irrelevante Processors (Score 0) werden übersprungen
|
||||
|
||||
### Compiled Template Caching
|
||||
|
||||
**Performance**: ~50-60% schnelleres Re-Rendering mit unterschiedlichen Daten
|
||||
|
||||
**Key Features**:
|
||||
- Cached vorverarbeitete Templates (AST)
|
||||
- Staleness-Detection via File-Modification-Time
|
||||
- Strukturiertes Caching statt nur HTML
|
||||
|
||||
```php
|
||||
// CompiledTemplate Value Object
|
||||
final readonly class CompiledTemplate
|
||||
{
|
||||
public function __construct(
|
||||
public string $templatePath,
|
||||
public array $placeholders, // Extracted placeholders
|
||||
public array $components, // Referenced components
|
||||
public array $instructions, // Processor instructions
|
||||
public int $compiledAt, // Compilation timestamp
|
||||
public Hash $contentHash // Content hash for validation
|
||||
) {}
|
||||
}
|
||||
|
||||
// Usage via CompiledTemplateCache
|
||||
$compiled = $this->compiledTemplateCache->remember(
|
||||
$templatePath,
|
||||
fn($content) => $this->compiler->compile($content)
|
||||
);
|
||||
```
|
||||
|
||||
### Processor Performance Tracking
|
||||
|
||||
**Performance**: Minimal overhead (< 0.1ms), nur Development/Profiling
|
||||
|
||||
**Key Features**:
|
||||
- Execution Time Tracking pro Processor
|
||||
- Memory Usage Measurement
|
||||
- Performance Grade Calculation (A-F)
|
||||
- Bottleneck Identification (> 10ms threshold)
|
||||
|
||||
```php
|
||||
// Optional Performance Tracking (aktiviert via ENABLE_TEMPLATE_PROFILING=true)
|
||||
if ($this->performanceTracker !== null) {
|
||||
$html = $this->performanceTracker->measure(
|
||||
$processorClass,
|
||||
fn() => $processor->process($html, $context)
|
||||
);
|
||||
}
|
||||
|
||||
// Performance Report generieren
|
||||
$report = $this->performanceTracker->generateReport();
|
||||
/*
|
||||
Processor Details:
|
||||
--------------------------------------------------------------------------------
|
||||
PlaceholderReplacer | 1250 calls | Avg: 0.45ms | Grade: A
|
||||
IfProcessor | 850 calls | Avg: 1.20ms | Grade: B
|
||||
ForProcessor | 420 calls | Avg: 3.80ms | Grade: B
|
||||
ComponentProcessor | 180 calls | Avg: 8.50ms | Grade: C
|
||||
*/
|
||||
```
|
||||
|
||||
### Template Processing Performance Metrics
|
||||
|
||||
| Optimization | Performance Gain | Memory Impact | Cache Strategy |
|
||||
|--------------|------------------|---------------|----------------|
|
||||
| Processor Chain Optimization | ~30-40% faster | Minimal | 24h TTL, structure-based key |
|
||||
| Compiled Template Cache | ~50-60% faster | ~5KB/template | 24h TTL, file staleness detection |
|
||||
| Performance Tracking | < 0.1ms overhead | ~2KB/processor | Development only |
|
||||
|
||||
**Integration**:
|
||||
```php
|
||||
// In TemplateRendererInitializer
|
||||
$chainOptimizer = new ProcessorChainOptimizer($cache);
|
||||
$compiledTemplateCache = new CompiledTemplateCache($cache);
|
||||
|
||||
// Performance Tracker nur in Development/Profiling
|
||||
$performanceTracker = null;
|
||||
if (getenv('ENABLE_TEMPLATE_PROFILING') === 'true') {
|
||||
$performanceTracker = new ProcessorPerformanceTracker();
|
||||
$performanceTracker->enable();
|
||||
}
|
||||
|
||||
$templateProcessor = new TemplateProcessor(
|
||||
domProcessors: $doms,
|
||||
stringProcessors: $strings,
|
||||
container: $this->container,
|
||||
chainOptimizer: $chainOptimizer,
|
||||
compiledTemplateCache: $compiledTemplateCache,
|
||||
performanceTracker: $performanceTracker
|
||||
);
|
||||
```
|
||||
|
||||
## 4. Event System Optimization (SSE)
|
||||
|
||||
### Event Batching for SSE
|
||||
|
||||
**Performance**: ~40-50% reduzierte HTTP-Overhead durch Event-Batching
|
||||
|
||||
**Key Features**:
|
||||
- Time-based Batching (default: 100ms)
|
||||
- Size-based Batching (default: 10 events)
|
||||
- Automatic Flush Management
|
||||
- Optional per Channel
|
||||
|
||||
```php
|
||||
// Enable batching in SseBroadcaster
|
||||
$broadcaster = $container->get(SseBroadcaster::class);
|
||||
$broadcaster->enableBatching(
|
||||
maxBatchSize: 10,
|
||||
maxBatchDelayMs: 100
|
||||
);
|
||||
|
||||
// Events werden automatisch gebatched
|
||||
$broadcaster->broadcastComponentUpdate($componentId, $state, $html);
|
||||
$broadcaster->broadcastComponentUpdate($componentId2, $state2, $html2);
|
||||
// ... bis zu 10 Events oder 100ms vergehen
|
||||
|
||||
// Manual flush wenn nötig
|
||||
$broadcaster->flushAll();
|
||||
|
||||
// Disable batching
|
||||
$broadcaster->disableBatching(); // Flushed automatisch vor Deaktivierung
|
||||
```
|
||||
|
||||
**Batching Strategy**:
|
||||
- **Size-based**: Batch wird gesendet wenn `maxBatchSize` erreicht
|
||||
- **Time-based**: Batch wird gesendet nach `maxBatchDelayMs` Millisekunden
|
||||
- **Mixed**: Whichever condition is met first
|
||||
|
||||
**Batch Event Format**:
|
||||
```json
|
||||
{
|
||||
"type": "batch",
|
||||
"count": 5,
|
||||
"events": [
|
||||
{"event": "component-update", "data": {...}, "id": "..."},
|
||||
{"event": "component-update", "data": {...}, "id": "..."},
|
||||
...
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### SSE Performance Metrics
|
||||
|
||||
| Optimization | Performance Gain | Bandwidth Reduction | Latency Impact |
|
||||
|--------------|------------------|---------------------|----------------|
|
||||
| Event Batching | ~40-50% faster | ~30% less data | +100ms max delay |
|
||||
| Dead Connection Cleanup | ~10% faster | N/A | Automatic |
|
||||
| Channel-based Routing | ~20% faster | N/A | Negligible |
|
||||
|
||||
**Client-Side Batch Handling**:
|
||||
```javascript
|
||||
// In SSE client
|
||||
eventSource.addEventListener('batch', (e) => {
|
||||
const batch = JSON.parse(e.data);
|
||||
|
||||
// Process all batched events
|
||||
batch.events.forEach(event => {
|
||||
// Handle each event based on type
|
||||
if (event.event === 'component-update') {
|
||||
updateComponent(JSON.parse(event.data));
|
||||
}
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**Production Usage**:
|
||||
```php
|
||||
// In SSE initialization (optional, default: disabled)
|
||||
if (getenv('SSE_BATCHING_ENABLED') === 'true') {
|
||||
$broadcaster->enableBatching(
|
||||
maxBatchSize: (int) getenv('SSE_BATCH_SIZE') ?: 10,
|
||||
maxBatchDelayMs: (int) getenv('SSE_BATCH_DELAY_MS') ?: 100
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## 5. Smart Invalidation Strategy
|
||||
|
||||
### Coordinated Multi-Layer Invalidation
|
||||
|
||||
**Key Features**:
|
||||
- Invalidate only affected caches
|
||||
- State-aware slot invalidation
|
||||
- Bulk invalidation for related components
|
||||
- Lazy invalidation with timestamps
|
||||
|
||||
```php
|
||||
// Smart invalidation - only affected caches
|
||||
$result = $strategy->invalidateOnStateChange(
|
||||
$componentId,
|
||||
$oldState,
|
||||
$newState
|
||||
);
|
||||
|
||||
// Only invalidates slots if state keys affect slots:
|
||||
// sidebarWidth, sidebarCollapsed, isOpen, padding, theme, variant
|
||||
```
|
||||
|
||||
### Invalidation Performance
|
||||
|
||||
| Invalidation Type | Operations | Time | Affected Caches |
|
||||
|-------------------|------------|------|-----------------|
|
||||
| Smart State Change | 1-2 | <1ms | 1-2 layers |
|
||||
| Component | 2 | <1ms | State + Slots |
|
||||
| Template Type | 1 | <1ms | Templates only |
|
||||
| Bulk (100 components) | 200 | ~10ms | State + Slots |
|
||||
|
||||
## 4. Performance Monitoring
|
||||
|
||||
### Real-time Metrics Collection
|
||||
|
||||
**Decorator Pattern** für transparente Metrics:
|
||||
- No performance overhead from metrics (~0.1ms per operation)
|
||||
- Automatic hit/miss tracking
|
||||
- Average lookup time measurement
|
||||
- Performance grade calculation (A-F)
|
||||
|
||||
```php
|
||||
// Automatic metrics via decorator
|
||||
$cache = new MetricsAwareComponentStateCache($baseCache, $metricsCollector);
|
||||
|
||||
// Normal operations - metrics collected automatically
|
||||
$state = $cache->retrieve($componentId, $currentState);
|
||||
|
||||
// Get performance insights
|
||||
$summary = $metricsCollector->getSummary();
|
||||
// Hit rates, lookup times, performance grades
|
||||
```
|
||||
|
||||
### Performance Targets & Validation
|
||||
|
||||
```php
|
||||
// Automatic target validation
|
||||
$assessment = $metricsCollector->assessPerformance();
|
||||
|
||||
/*
|
||||
[
|
||||
'state_cache' => [
|
||||
'target' => '70.0%',
|
||||
'actual' => '85.50%',
|
||||
'meets_target' => true,
|
||||
'grade' => 'A'
|
||||
],
|
||||
...
|
||||
]
|
||||
*/
|
||||
|
||||
// Performance warnings
|
||||
if ($metricsCollector->hasPerformanceIssues()) {
|
||||
$warnings = $metricsCollector->getPerformanceWarnings();
|
||||
// "State cache hit rate (65.00%) below target (70.0%)"
|
||||
}
|
||||
```
|
||||
|
||||
## 5. Memory Management
|
||||
|
||||
### Efficient Memory Usage
|
||||
|
||||
**Optimizations**:
|
||||
- Shared metadata across component instances (~75% memory reduction)
|
||||
- Lazy loading of component classes
|
||||
- Cache size limits with LRU eviction
|
||||
- Weak references for temporary objects
|
||||
|
||||
**Memory Footprint**:
|
||||
```
|
||||
Per Component:
|
||||
- Compiled Metadata: ~5KB (shared)
|
||||
- Cached State: ~2KB per instance
|
||||
- Cached Slot: ~1KB per slot
|
||||
- Cached Template: ~5KB per variant
|
||||
|
||||
Total (10,000 components):
|
||||
- Metadata Cache: ~50MB (one-time)
|
||||
- State Cache: ~20MB (active instances)
|
||||
- Slot Cache: ~10MB (common slots)
|
||||
- Template Cache: ~50MB (variants)
|
||||
Total: ~130MB (reasonable for high-traffic app)
|
||||
```
|
||||
|
||||
### Memory Optimization Techniques
|
||||
|
||||
```php
|
||||
// 1. Lazy loading - only load when needed
|
||||
$metadata = $registry->getMetadata('counter'); // Loads on demand
|
||||
|
||||
// 2. Shared metadata - single instance per component class
|
||||
$counterMeta1 = $registry->getMetadata('counter');
|
||||
$counterMeta2 = $registry->getMetadata('counter');
|
||||
// Same CompiledComponentMetadata instance
|
||||
|
||||
// 3. Cache limits - prevent unbounded growth
|
||||
// Configured in ComponentStateCache, SlotContentCache, etc.
|
||||
|
||||
// 4. Batch operations - single allocation for multiple operations
|
||||
$metadata = $metadataCache->getBatch($classNames); // Single array allocation
|
||||
```
|
||||
|
||||
## 6. Production Performance
|
||||
|
||||
### Real-World Performance Metrics
|
||||
|
||||
**Production Environment** (10,000+ requests/hour):
|
||||
|
||||
| Metric | Without Optimizations | With Optimizations | Improvement |
|
||||
|--------|----------------------|-------------------|-------------|
|
||||
| Avg Component Render | 21.4ms | 5.1ms | **76% faster** |
|
||||
| Registry Lookup | 2.3ms | 0.01ms | **99% faster** |
|
||||
| State Init | 5.2ms | 1.5ms | **71% faster** |
|
||||
| Slot Resolution | 3.8ms | 1.5ms | **61% faster** |
|
||||
| Template Render | 12.4ms | 2.1ms | **83% faster** |
|
||||
| Memory Usage (10K comp) | ~520MB | ~130MB | **75% reduction** |
|
||||
|
||||
### Cache Hit Rates (Production)
|
||||
|
||||
```
|
||||
State Cache: 87.3% hit rate (Target: 70%) Grade: A
|
||||
Slot Cache: 76.8% hit rate (Target: 60%) Grade: C
|
||||
Template Cache: 93.2% hit rate (Target: 80%) Grade: A
|
||||
|
||||
Overall Grade: A
|
||||
```
|
||||
|
||||
### Throughput Improvements
|
||||
|
||||
```
|
||||
Before Optimizations:
|
||||
- ~500 components/second
|
||||
- ~150ms p95 latency
|
||||
- ~400MB memory baseline
|
||||
|
||||
After Optimizations:
|
||||
- ~2000 components/second (+300%)
|
||||
- ~40ms p95 latency (-73%)
|
||||
- ~130MB memory baseline (-67%)
|
||||
```
|
||||
|
||||
## 7. Best Practices
|
||||
|
||||
### Do's ✅
|
||||
|
||||
1. **Use Auto-TTL Methods**
|
||||
```php
|
||||
$cache->storeWithAutoTTL($id, $state, 'counter');
|
||||
```
|
||||
|
||||
2. **Batch Operations**
|
||||
```php
|
||||
$cache->storeBatch($id, $slots);
|
||||
$cache->getBatch($id, $slotNames);
|
||||
```
|
||||
|
||||
3. **Remember Pattern for Templates**
|
||||
```php
|
||||
$html = $cache->remember($type, $data, fn() => $this->render($data));
|
||||
```
|
||||
|
||||
4. **Smart Invalidation**
|
||||
```php
|
||||
$strategy->invalidateOnStateChange($id, $old, $new);
|
||||
```
|
||||
|
||||
5. **Monitor Performance**
|
||||
```php
|
||||
if ($metricsCollector->hasPerformanceIssues()) {
|
||||
// Alert or auto-tune
|
||||
}
|
||||
```
|
||||
|
||||
### Don'ts ❌
|
||||
|
||||
1. **Manual TTL ohne Component-Type Consideration**
|
||||
```php
|
||||
// ❌ Bad - too long for counter
|
||||
$cache->store($id, $state, Duration::fromHours(24));
|
||||
|
||||
// ✅ Good - auto-optimized
|
||||
$cache->storeWithAutoTTL($id, $state, 'counter');
|
||||
```
|
||||
|
||||
2. **Individual Calls statt Batch**
|
||||
```php
|
||||
// ❌ Bad - 3 cache operations
|
||||
foreach ($slots as $name => $content) {
|
||||
$cache->storeResolvedContent($id, $name, $content);
|
||||
}
|
||||
|
||||
// ✅ Good - 1 cache operation
|
||||
$cache->storeBatch($id, $slots);
|
||||
```
|
||||
|
||||
3. **Always Invalidate All**
|
||||
```php
|
||||
// ❌ Bad - invalidates unnecessary caches
|
||||
$strategy->invalidateComponent($id);
|
||||
|
||||
// ✅ Good - only invalidates affected
|
||||
$strategy->invalidateOnStateChange($id, $old, $new);
|
||||
```
|
||||
|
||||
## 8. Optimization Checklist
|
||||
|
||||
### Application Startup
|
||||
- ✅ Warmup metadata cache for all components
|
||||
- ✅ Batch load component metadata
|
||||
- ✅ Pre-compile frequently used templates
|
||||
|
||||
### Component Rendering
|
||||
- ✅ Check state cache before initialization
|
||||
- ✅ Use batch slot operations
|
||||
- ✅ Apply remember pattern for templates
|
||||
- ✅ Smart invalidation on state changes
|
||||
|
||||
### Production Deployment
|
||||
- ✅ Monitor cache hit rates (targets: 70%, 60%, 80%)
|
||||
- ✅ Set up performance alerts
|
||||
- ✅ Configure cache drivers (Redis for best performance)
|
||||
- ✅ Implement cache warmup on deployment
|
||||
- ✅ Monitor memory usage
|
||||
|
||||
### Continuous Optimization
|
||||
- ✅ Review performance metrics weekly
|
||||
- ✅ Adjust TTL strategies based on hit rates
|
||||
- ✅ Identify and optimize cache misses
|
||||
- ✅ Profile slow components
|
||||
- ✅ Update metadata cache on component changes
|
||||
|
||||
## 9. Troubleshooting
|
||||
|
||||
### Low Hit Rates
|
||||
|
||||
**Symptom**: Hit rate below target
|
||||
|
||||
**Diagnosis**:
|
||||
```php
|
||||
$summary = $metricsCollector->getSummary();
|
||||
// Check hit_rate_percent for each cache type
|
||||
```
|
||||
|
||||
**Solutions**:
|
||||
1. Increase TTL if cache expiring too fast
|
||||
2. Review invalidation strategy (too aggressive?)
|
||||
3. Check for cache size limits (evictions?)
|
||||
4. Monitor for high variance in data (prevents caching)
|
||||
|
||||
### High Memory Usage
|
||||
|
||||
**Symptom**: Memory usage > 200MB for moderate load
|
||||
|
||||
**Diagnosis**:
|
||||
```php
|
||||
$stats = $registry->getRegistryStats();
|
||||
// Check total_components and metadata_loaded
|
||||
```
|
||||
|
||||
**Solutions**:
|
||||
1. Implement cache size limits
|
||||
2. Review TTL settings (too long?)
|
||||
3. Clear memory cache periodically
|
||||
4. Use Redis instead of file cache
|
||||
|
||||
### Slow Component Rendering
|
||||
|
||||
**Symptom**: Render time > 10ms even with caching
|
||||
|
||||
**Diagnosis**:
|
||||
```php
|
||||
$metrics = $metricsCollector->getMetrics(CacheType::TEMPLATE);
|
||||
// Check average_lookup_time_ms
|
||||
```
|
||||
|
||||
**Solutions**:
|
||||
1. Profile template rendering (bottleneck in template?)
|
||||
2. Check cache driver performance (Redis vs File)
|
||||
3. Optimize template complexity
|
||||
4. Use static templates where possible
|
||||
|
||||
## Zusammenfassung
|
||||
|
||||
Das LiveComponents Performance-Optimierungssystem bietet:
|
||||
|
||||
✅ **~76% schnelleres Component Rendering** (21.4ms → 5.1ms)
|
||||
✅ **~99% schnellerer Registry Lookup** (2.3ms → 0.01ms)
|
||||
✅ **~75% weniger Memory Usage** (520MB → 130MB)
|
||||
✅ **~300% höherer Throughput** (500 → 2000 comp/s)
|
||||
✅ **~30-40% schnelleres Template Processing** durch Processor Chain Optimization
|
||||
✅ **~40-50% reduzierte SSE-Overhead** durch Event Batching
|
||||
|
||||
**Optimization Stack**:
|
||||
- **Layer 1**: Component Registry mit Compiled Metadata Cache (~90% faster)
|
||||
- **Layer 2**: 3-Layer Caching System (State, Slot, Template) (~70-80% faster)
|
||||
- **Layer 3**: Template Processing Optimization (~30-40% faster)
|
||||
- **Layer 4**: Event System (SSE) Batching (~40-50% faster)
|
||||
- **Layer 5**: Smart Invalidation Strategy (minimal cache clearing)
|
||||
- **Layer 6**: Real-time Performance Monitoring (metrics & grades)
|
||||
- **Layer 7**: Efficient Memory Management (~75% reduction)
|
||||
|
||||
**Validierung**:
|
||||
- Alle Performance-Claims durch automatisierte Benchmarks validiert
|
||||
- Benchmark-Suite in `tests/Performance/LiveComponentsPerformanceBenchmark.php`
|
||||
- Production-Metriken über 10,000+ requests/hour
|
||||
|
||||
**Framework-Konform**:
|
||||
- Value Objects (CacheType, CacheMetrics, Percentage, Hash, Duration)
|
||||
- Readonly Classes (wo möglich)
|
||||
- Immutable State (Transformation Methods)
|
||||
- Decorator Pattern für Metrics
|
||||
- Type Safety überall
|
||||
- Composition over Inheritance
|
||||
- Explicit Dependency Injection
|
||||
|
||||
**Neue Performance-Klassen**:
|
||||
- `ComponentMetadataCache` - Compiled metadata caching
|
||||
- `ComponentMetadataCompiler` - One-time reflection
|
||||
- `CompiledComponentMetadata` - Metadata Value Objects
|
||||
- `ProcessorChainOptimizer` - Template processor optimization
|
||||
- `CompiledTemplateCache` - Template AST caching
|
||||
- `ProcessorPerformanceTracker` - Template profiling
|
||||
- `CacheMetricsCollector` - Real-time metrics
|
||||
- `MetricsAware*Cache` - Decorator pattern für alle Caches
|
||||
- `SseBroadcaster` (erweitert) - Event batching für SSE
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -1,504 +0,0 @@
|
||||
# LiveComponents Test Harness
|
||||
|
||||
Comprehensive test harness für LiveComponents mit ComponentTestCase trait und ComponentFactory.
|
||||
|
||||
## Übersicht
|
||||
|
||||
Der Test-Harness bietet:
|
||||
- **ComponentTestCase trait**: Umfassende Test-Helper-Methoden
|
||||
- **ComponentFactory**: Builder-Pattern für Test-Component-Erstellung
|
||||
- **Automatische Setup**: CSRF, Authorization, State Validation Integration
|
||||
- **Assertions**: State, Action, Authorization und Event Assertions
|
||||
|
||||
## Quick Start
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
use Tests\Framework\LiveComponents\ComponentFactory;
|
||||
use Tests\Framework\LiveComponents\ComponentTestCase;
|
||||
|
||||
// Use ComponentTestCase trait
|
||||
uses(ComponentTestCase::class);
|
||||
|
||||
// Setup before each test
|
||||
beforeEach(function () {
|
||||
$this->setUpComponentTest();
|
||||
});
|
||||
|
||||
it('executes component action', function () {
|
||||
$component = ComponentFactory::counter(initialCount: 5);
|
||||
|
||||
$result = $this->callAction($component, 'increment');
|
||||
|
||||
expect($result->state->data['count'])->toBe(6);
|
||||
});
|
||||
```
|
||||
|
||||
## ComponentTestCase Trait
|
||||
|
||||
### Setup Methode
|
||||
|
||||
**`setUpComponentTest()`** - Initialisiert Test-Environment:
|
||||
- Erstellt Session mit CSRF-Token-Generator
|
||||
- Initialisiert LiveComponentHandler mit allen Dependencies (CSRF, Auth, Validation)
|
||||
- Setzt EventDispatcher, AuthorizationChecker, StateValidator, SchemaCache auf
|
||||
|
||||
```php
|
||||
beforeEach(function () {
|
||||
$this->setUpComponentTest();
|
||||
});
|
||||
```
|
||||
|
||||
### Authentication Helper
|
||||
|
||||
**`actingAs(array $permissions = [], int $userId = 1)`** - Mock authenticated user:
|
||||
|
||||
```php
|
||||
$this->actingAs(['posts.edit', 'posts.delete']);
|
||||
|
||||
$result = $this->callAction($component, 'deletePost', ['id' => 123]);
|
||||
```
|
||||
|
||||
### Action Execution
|
||||
|
||||
**`callAction(LiveComponentContract $component, string $method, array $params = [])`** - Execute action with automatic CSRF:
|
||||
|
||||
```php
|
||||
$component = ComponentFactory::counter();
|
||||
|
||||
// Automatic CSRF token generation
|
||||
$result = $this->callAction($component, 'increment');
|
||||
|
||||
// With parameters
|
||||
$result = $this->callAction($component, 'addItem', ['item' => 'New Task']);
|
||||
```
|
||||
|
||||
### Action Assertions
|
||||
|
||||
**`assertActionExecutes()`** - Assert action executes successfully:
|
||||
|
||||
```php
|
||||
$result = $this->assertActionExecutes($component, 'increment');
|
||||
|
||||
expect($result->state->data['count'])->toBe(1);
|
||||
```
|
||||
|
||||
**`assertActionThrows()`** - Assert action throws exception:
|
||||
|
||||
```php
|
||||
$component = ComponentFactory::make()
|
||||
->withId('error:component')
|
||||
->withState(['data' => 'test'])
|
||||
->withAction('fail', function() {
|
||||
throw new \RuntimeException('Expected error');
|
||||
})
|
||||
->create();
|
||||
|
||||
$this->assertActionThrows($component, 'fail', \RuntimeException::class);
|
||||
```
|
||||
|
||||
**`assertActionRequiresAuth()`** - Assert action requires authentication:
|
||||
|
||||
```php
|
||||
// Note: Requires real component class with #[RequiresPermission] attribute
|
||||
// ComponentFactory closures don't support attributes
|
||||
$this->assertActionRequiresAuth($component, 'protectedAction');
|
||||
```
|
||||
|
||||
**`assertActionRequiresPermission()`** - Assert action requires specific permission:
|
||||
|
||||
```php
|
||||
$this->actingAs(['posts.view']); // Insufficient permission
|
||||
|
||||
$this->assertActionRequiresPermission(
|
||||
$component,
|
||||
'deletePost',
|
||||
['posts.view'] // Should fail with only 'view' permission
|
||||
);
|
||||
```
|
||||
|
||||
### State Assertions
|
||||
|
||||
**`assertStateEquals(ComponentUpdate $result, array $expected)`** - Assert state matches expected:
|
||||
|
||||
```php
|
||||
$result = $this->callAction($component, 'increment');
|
||||
|
||||
$this->assertStateEquals($result, ['count' => 1]);
|
||||
```
|
||||
|
||||
**`assertStateHas(ComponentUpdate $result, string $key)`** - Assert state has key:
|
||||
|
||||
```php
|
||||
$this->assertStateHas($result, 'items');
|
||||
```
|
||||
|
||||
**`assertStateValidates(ComponentUpdate $result)`** - Assert state passes validation:
|
||||
|
||||
```php
|
||||
$result = $this->callAction($component, 'updateData', ['value' => 'test']);
|
||||
|
||||
$this->assertStateValidates($result);
|
||||
```
|
||||
|
||||
**`getStateValue(ComponentUpdate $result, string $key)`** - Get specific state value:
|
||||
|
||||
```php
|
||||
$count = $this->getStateValue($result, 'count');
|
||||
|
||||
expect($count)->toBe(5);
|
||||
```
|
||||
|
||||
### Event Assertions
|
||||
|
||||
**`assertEventDispatched(ComponentUpdate $result, string $eventName)`** - Assert event was dispatched:
|
||||
|
||||
```php
|
||||
$result = $this->callAction($component, 'submitForm');
|
||||
|
||||
$this->assertEventDispatched($result, 'form:submitted');
|
||||
```
|
||||
|
||||
**`assertNoEventsDispatched(ComponentUpdate $result)`** - Assert no events were dispatched:
|
||||
|
||||
```php
|
||||
$result = $this->callAction($component, 'increment');
|
||||
|
||||
$this->assertNoEventsDispatched($result);
|
||||
```
|
||||
|
||||
**`assertEventCount(ComponentUpdate $result, int $count)`** - Assert event count:
|
||||
|
||||
```php
|
||||
$result = $this->callAction($component, 'bulkOperation');
|
||||
|
||||
$this->assertEventCount($result, 3);
|
||||
```
|
||||
|
||||
## ComponentFactory
|
||||
|
||||
### Builder Pattern
|
||||
|
||||
**`ComponentFactory::make()`** - Start builder:
|
||||
|
||||
```php
|
||||
$component = ComponentFactory::make()
|
||||
->withId('posts:manager')
|
||||
->withState(['posts' => [], 'count' => 0])
|
||||
->withAction('addPost', function(string $title) {
|
||||
$this->state['posts'][] = $title;
|
||||
$this->state['count']++;
|
||||
return ComponentData::fromArray($this->state);
|
||||
})
|
||||
->create();
|
||||
```
|
||||
|
||||
### Builder Methods
|
||||
|
||||
- **`withId(string $id)`** - Set component ID
|
||||
- **`withState(array $state)`** - Set initial state (cannot be empty!)
|
||||
- **`withAction(string $name, callable $handler)`** - Add custom action
|
||||
- **`withTemplate(string $template)`** - Set template name
|
||||
- **`create()`** - Create component instance
|
||||
|
||||
### Pre-configured Components
|
||||
|
||||
**`ComponentFactory::counter(int $initialCount = 0)`** - Counter component:
|
||||
|
||||
```php
|
||||
$component = ComponentFactory::counter(initialCount: 5);
|
||||
|
||||
// Actions: increment, decrement, reset
|
||||
$result = $this->callAction($component, 'increment');
|
||||
|
||||
expect($result->state->data['count'])->toBe(6);
|
||||
```
|
||||
|
||||
**`ComponentFactory::list(array $initialItems = [])`** - List component:
|
||||
|
||||
```php
|
||||
$component = ComponentFactory::list(['item1', 'item2']);
|
||||
|
||||
// Actions: addItem, removeItem, clear
|
||||
$result = $this->callAction($component, 'addItem', ['item' => 'item3']);
|
||||
|
||||
expect($result->state->data['items'])->toHaveCount(3);
|
||||
```
|
||||
|
||||
## Integration Features
|
||||
|
||||
### Automatic CSRF Protection
|
||||
|
||||
```php
|
||||
// CSRF token automatically generated and validated
|
||||
$result = $this->callAction($component, 'action');
|
||||
|
||||
// CSRF token: 'livecomponent:{componentId}' form ID
|
||||
```
|
||||
|
||||
### Automatic State Validation
|
||||
|
||||
```php
|
||||
// State automatically validated against derived schema
|
||||
$result = $this->callAction($component, 'updateState');
|
||||
|
||||
// Schema derived on first getData() call
|
||||
// Cached for subsequent validations
|
||||
```
|
||||
|
||||
### Authorization Integration
|
||||
|
||||
```php
|
||||
// Mock authenticated user with permissions
|
||||
$this->actingAs(['admin.access']);
|
||||
|
||||
// Authorization automatically checked for #[RequiresPermission] attributes
|
||||
$result = $this->callAction($component, 'adminAction');
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### State Must Not Be Empty
|
||||
|
||||
```php
|
||||
// ❌ Empty state causes schema derivation error
|
||||
$component = ComponentFactory::make()
|
||||
->withState([])
|
||||
->create();
|
||||
|
||||
// ✅ Always provide at least one state field
|
||||
$component = ComponentFactory::make()
|
||||
->withState(['initialized' => true])
|
||||
->create();
|
||||
```
|
||||
|
||||
### Authorization Testing Requires Real Classes
|
||||
|
||||
```php
|
||||
// ❌ Closures don't support attributes for authorization
|
||||
$component = ComponentFactory::make()
|
||||
->withAction('protectedAction',
|
||||
#[RequiresPermission('admin')] // Attribute wird ignoriert
|
||||
function() { }
|
||||
)
|
||||
->create();
|
||||
|
||||
// ✅ Use real component class for authorization testing
|
||||
final readonly class TestComponent implements LiveComponentContract
|
||||
{
|
||||
#[RequiresPermission('admin')]
|
||||
public function protectedAction(): ComponentData
|
||||
{
|
||||
// Implementation
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Action Closures Have Access to Component State
|
||||
|
||||
```php
|
||||
$component = ComponentFactory::make()
|
||||
->withState(['count' => 0])
|
||||
->withAction('increment', function() {
|
||||
// $this->state available via closure binding
|
||||
$this->state['count']++;
|
||||
return ComponentData::fromArray($this->state);
|
||||
})
|
||||
->create();
|
||||
```
|
||||
|
||||
### Multiple Actions in Sequence
|
||||
|
||||
```php
|
||||
it('handles multiple actions', function () {
|
||||
$component = ComponentFactory::counter();
|
||||
|
||||
$result1 = $this->callAction($component, 'increment');
|
||||
$result2 = $this->callAction($component, 'increment');
|
||||
$result3 = $this->callAction($component, 'decrement');
|
||||
|
||||
// Note: Component state is immutable
|
||||
// Each call returns new state, doesn't mutate original
|
||||
expect($result1->state->data['count'])->toBe(1);
|
||||
expect($result2->state->data['count'])->toBe(2);
|
||||
expect($result3->state->data['count'])->toBe(1);
|
||||
});
|
||||
```
|
||||
|
||||
## Test Organization
|
||||
|
||||
```
|
||||
tests/
|
||||
├── Framework/LiveComponents/
|
||||
│ ├── ComponentTestCase.php # Trait with helper methods
|
||||
│ └── ComponentFactory.php # Builder for test components
|
||||
└── Feature/Framework/LiveComponents/
|
||||
├── TestHarnessDemo.php # Demo of all features
|
||||
├── SimpleTestHarnessTest.php # Simple examples
|
||||
└── ExceptionTestHarnessTest.php # Exception handling examples
|
||||
```
|
||||
|
||||
## Complete Example
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
use Tests\Framework\LiveComponents\ComponentFactory;
|
||||
use Tests\Framework\LiveComponents\ComponentTestCase;
|
||||
|
||||
uses(ComponentTestCase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->setUpComponentTest();
|
||||
});
|
||||
|
||||
describe('Shopping Cart Component', function () {
|
||||
it('adds items to cart', function () {
|
||||
$component = ComponentFactory::make()
|
||||
->withId('shopping-cart')
|
||||
->withState(['items' => [], 'total' => 0])
|
||||
->withAction('addItem', function(string $product, int $price) {
|
||||
$this->state['items'][] = ['product' => $product, 'price' => $price];
|
||||
$this->state['total'] += $price;
|
||||
return ComponentData::fromArray($this->state);
|
||||
})
|
||||
->create();
|
||||
|
||||
$result = $this->callAction($component, 'addItem', [
|
||||
'product' => 'Laptop',
|
||||
'price' => 999
|
||||
]);
|
||||
|
||||
$this->assertStateHas($result, 'items');
|
||||
expect($result->state->data['items'])->toHaveCount(1);
|
||||
expect($result->state->data['total'])->toBe(999);
|
||||
});
|
||||
|
||||
it('requires authentication for checkout', function () {
|
||||
$component = ComponentFactory::make()
|
||||
->withId('shopping-cart')
|
||||
->withState(['items' => [['product' => 'Laptop', 'price' => 999]]])
|
||||
->withAction('checkout', function() {
|
||||
// Checkout logic
|
||||
return ComponentData::fromArray($this->state);
|
||||
})
|
||||
->create();
|
||||
|
||||
// Without authentication
|
||||
// Note: For authorization testing, use real component classes
|
||||
|
||||
// With authentication
|
||||
$this->actingAs(['checkout.access']);
|
||||
$result = $this->assertActionExecutes($component, 'checkout');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Known Limitations
|
||||
|
||||
### 1. Closure Attributes
|
||||
|
||||
Attributes on closures passed to `withAction()` are not supported for authorization checks:
|
||||
|
||||
```php
|
||||
// ❌ Doesn't work - attribute ignored
|
||||
$component = ComponentFactory::make()
|
||||
->withAction('protectedAction',
|
||||
#[RequiresPermission('admin')]
|
||||
function() { }
|
||||
)
|
||||
->create();
|
||||
```
|
||||
|
||||
**Workaround**: Create real component class for authorization testing.
|
||||
|
||||
### 2. Empty State Not Allowed
|
||||
|
||||
Components must have at least one state field for schema derivation:
|
||||
|
||||
```php
|
||||
// ❌ Throws InvalidArgumentException: 'Schema cannot be empty'
|
||||
$component = ComponentFactory::make()
|
||||
->withState([])
|
||||
->create();
|
||||
|
||||
// ✅ Provide at least one field
|
||||
$component = ComponentFactory::make()
|
||||
->withState(['initialized' => true])
|
||||
->create();
|
||||
```
|
||||
|
||||
### 3. Magic Method Reflection
|
||||
|
||||
ComponentFactory uses `__call()` for actions, which limits reflection-based parameter analysis. The handler falls back to direct parameter passing for magic methods.
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
- **Schema Caching**: Schema derived once per component class and cached
|
||||
- **CSRF Generation**: CSRF token generated per test, not reused
|
||||
- **Session State**: Session state reset in `setUpComponentTest()`
|
||||
- **Event Dispatcher**: Events collected per action call, not persisted
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Method not found" Error
|
||||
|
||||
```
|
||||
BadMethodCallException: Method increment not found on component
|
||||
```
|
||||
|
||||
**Fix**: Ensure `method_exists()` check supports `__call()` magic methods:
|
||||
|
||||
```php
|
||||
// LiveComponentHandler checks both real and magic methods
|
||||
if (!method_exists($component, $method) && !is_callable([$component, $method])) {
|
||||
throw new \BadMethodCallException(...);
|
||||
}
|
||||
```
|
||||
|
||||
### "Schema cannot be empty" Error
|
||||
|
||||
```
|
||||
InvalidArgumentException: Schema cannot be empty
|
||||
```
|
||||
|
||||
**Fix**: Provide non-empty state:
|
||||
|
||||
```php
|
||||
// ❌ Empty state
|
||||
->withState([])
|
||||
|
||||
// ✅ Non-empty state
|
||||
->withState(['data' => 'test'])
|
||||
```
|
||||
|
||||
### Reflection Exception for Actions
|
||||
|
||||
```
|
||||
ReflectionException: Method increment() does not exist
|
||||
```
|
||||
|
||||
**Fix**: Handler catches ReflectionException and falls back to direct call:
|
||||
|
||||
```php
|
||||
try {
|
||||
$reflection = new \ReflectionMethod($component, $method);
|
||||
// Parameter analysis
|
||||
} catch (\ReflectionException $e) {
|
||||
// Direct call for magic methods
|
||||
return $component->$method(...$params->toArray());
|
||||
}
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
Der Test-Harness bietet:
|
||||
- ✅ **Einfache Component-Erstellung** via ComponentFactory
|
||||
- ✅ **Umfassende Assertions** für State, Actions, Events
|
||||
- ✅ **Automatische Integration** mit CSRF, Auth, Validation
|
||||
- ✅ **Flexible Test-Components** via Builder Pattern
|
||||
- ✅ **Pre-configured Components** (Counter, List)
|
||||
- ⚠️ **Known Limitations** mit Closure-Attributes
|
||||
|
||||
**Framework-Integration**: Vollständig integriert mit LiveComponentHandler, StateValidator, AuthorizationChecker und EventDispatcher.
|
||||
@@ -1,202 +0,0 @@
|
||||
# Migration System Quick Reference
|
||||
|
||||
## TL;DR
|
||||
|
||||
**Default**: Migrations sind forward-only (nur `up()` Methode).
|
||||
**Optional**: Implementiere `SafelyReversible` nur wenn Rollback OHNE Datenverlust möglich ist.
|
||||
|
||||
## Quick Decision Tree
|
||||
|
||||
```
|
||||
Is rollback safe (no data loss)?
|
||||
│
|
||||
├─ YES (e.g., create table, add nullable column, create index)
|
||||
│ └─ Implement: Migration, SafelyReversible
|
||||
│
|
||||
└─ NO (e.g., drop column, transform data, delete data)
|
||||
└─ Implement: Migration only
|
||||
```
|
||||
|
||||
## Code Templates
|
||||
|
||||
### Forward-Only Migration (Default)
|
||||
|
||||
```php
|
||||
use App\Framework\Database\Migration\Migration;
|
||||
use App\Framework\Database\Migration\MigrationVersion;
|
||||
use App\Framework\Database\ConnectionInterface;
|
||||
use App\Framework\Database\Schema\Schema;
|
||||
|
||||
final readonly class YourMigration implements Migration
|
||||
{
|
||||
public function up(ConnectionInterface $connection): void
|
||||
{
|
||||
$schema = new Schema($connection);
|
||||
// Your migration logic
|
||||
$schema->execute();
|
||||
}
|
||||
|
||||
public function getVersion(): MigrationVersion
|
||||
{
|
||||
return MigrationVersion::fromTimestamp("2024_12_20_100000");
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Description of what this migration does';
|
||||
}
|
||||
|
||||
public function getDomain(): string
|
||||
{
|
||||
return "YourDomain";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Safe Rollback Migration
|
||||
|
||||
```php
|
||||
use App\Framework\Database\Migration\{Migration, SafelyReversible};
|
||||
use App\Framework\Database\Migration\MigrationVersion;
|
||||
use App\Framework\Database\ConnectionInterface;
|
||||
use App\Framework\Database\Schema\Schema;
|
||||
|
||||
/**
|
||||
* This migration is safely reversible because:
|
||||
* - [Explain why rollback is safe, e.g., "Creates new empty table"]
|
||||
*/
|
||||
final readonly class YourSafeMigration implements Migration, SafelyReversible
|
||||
{
|
||||
public function up(ConnectionInterface $connection): void
|
||||
{
|
||||
$schema = new Schema($connection);
|
||||
// Your migration logic
|
||||
$schema->execute();
|
||||
}
|
||||
|
||||
public function down(ConnectionInterface $connection): void
|
||||
{
|
||||
$schema = new Schema($connection);
|
||||
// Reverse the changes safely
|
||||
$schema->execute();
|
||||
}
|
||||
|
||||
public function getVersion(): MigrationVersion
|
||||
{
|
||||
return MigrationVersion::fromTimestamp("2024_12_20_100000");
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Description of what this migration does';
|
||||
}
|
||||
|
||||
public function getDomain(): string
|
||||
{
|
||||
return "YourDomain";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Common Operations
|
||||
|
||||
### Create Migration
|
||||
|
||||
```bash
|
||||
php console.php make:migration CreateYourTable
|
||||
```
|
||||
|
||||
### Run Migrations
|
||||
|
||||
```bash
|
||||
php console.php db:migrate
|
||||
```
|
||||
|
||||
### Rollback (Only SafelyReversible)
|
||||
|
||||
```bash
|
||||
# Rollback last migration
|
||||
php console.php db:rollback
|
||||
|
||||
# Rollback last 3 migrations
|
||||
php console.php db:rollback 3
|
||||
```
|
||||
|
||||
### Check Status
|
||||
|
||||
```bash
|
||||
php console.php db:status
|
||||
```
|
||||
|
||||
## Safe vs Unsafe Cheatsheet
|
||||
|
||||
| Operation | Safe? | Implements |
|
||||
|-----------|-------|------------|
|
||||
| Create table | ✅ | Migration, SafelyReversible |
|
||||
| Drop table (empty) | ✅ | Migration, SafelyReversible |
|
||||
| Drop table (with data) | ❌ | Migration only |
|
||||
| Add nullable column | ✅ | Migration, SafelyReversible |
|
||||
| Add NOT NULL column (with default) | ⚠️ | Case by case |
|
||||
| Drop column (empty) | ✅ | Migration, SafelyReversible |
|
||||
| Drop column (with data) | ❌ | Migration only |
|
||||
| Rename column | ✅ | Migration, SafelyReversible |
|
||||
| Change column type | ❌ | Migration only |
|
||||
| Create index | ✅ | Migration, SafelyReversible |
|
||||
| Drop index | ✅ | Migration, SafelyReversible |
|
||||
| Add foreign key | ✅ | Migration, SafelyReversible |
|
||||
| Drop foreign key | ✅ | Migration, SafelyReversible |
|
||||
| Transform data | ❌ | Migration only |
|
||||
| Delete data | ❌ | Migration only |
|
||||
| Merge tables | ❌ | Migration only |
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Rollback Failed (Not SafelyReversible)
|
||||
|
||||
```bash
|
||||
❌ Rollback failed: Migration 2024_12_20_100000 does not support safe rollback
|
||||
|
||||
💡 Recommendation:
|
||||
Create a new forward migration to undo the changes instead:
|
||||
php console.php make:migration FixYourChanges
|
||||
```
|
||||
|
||||
### What to do:
|
||||
|
||||
1. Create new forward migration
|
||||
2. Implement reverse logic in `up()` method
|
||||
3. Run `php console.php db:migrate`
|
||||
|
||||
## Testing Rollback
|
||||
|
||||
```bash
|
||||
# Test cycle in development
|
||||
php console.php db:migrate
|
||||
php console.php db:rollback 1
|
||||
php console.php db:migrate # Should succeed again
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. ✅ **Document why safe**: Always comment why a migration is SafelyReversible
|
||||
2. ✅ **Test rollback cycle**: Test `up() → down() → up()` in development
|
||||
3. ✅ **Default to forward-only**: Only add SafelyReversible if certain
|
||||
4. ❌ **Never rollback in production**: Even "safe" rollbacks should be avoided
|
||||
5. ✅ **Use fix-forward**: Prefer new migrations over rollback
|
||||
6. ✅ **Backup before rollback**: Always backup database before rolling back
|
||||
7. ✅ **Check for data**: Verify column is empty before making it SafelyReversible
|
||||
|
||||
## Links
|
||||
|
||||
- **Full Examples**: `docs/claude/examples/migrations/SafeVsUnsafeMigrations.md`
|
||||
- **Database Patterns**: `docs/claude/database-patterns.md`
|
||||
- **Migration Interface**: `src/Framework/Database/Migration/Migration.php`
|
||||
- **SafelyReversible Interface**: `src/Framework/Database/Migration/SafelyReversible.php`
|
||||
|
||||
## Framework Compliance
|
||||
|
||||
✅ Readonly classes: `final readonly class`
|
||||
✅ Composition over inheritance: Interface-based
|
||||
✅ Explicit contracts: `SafelyReversible` is opt-in
|
||||
✅ Type safety: Strong typing throughout
|
||||
✅ No primitive obsession: Uses Value Objects (MigrationVersion)
|
||||
1316
docs/claude/posix-system.md
Normal file
1316
docs/claude/posix-system.md
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,291 +0,0 @@
|
||||
# Route Authorization System
|
||||
|
||||
Dokumentation des namespace-basierten Route Authorization Systems.
|
||||
|
||||
## Übersicht
|
||||
|
||||
Das Route Authorization System ermöglicht die Zugangskontrolle für Routes auf Basis von:
|
||||
1. **Legacy `#[Auth]` Attribute** - Backward compatibility
|
||||
2. **Namespace-basierte Blockierung** - Blockiere ganze Controller-Namespaces (z.B. `App\Application\Admin\*`)
|
||||
3. **Namespace-basierte IP-Restrictions** - IP-basierte Zugriffskontrolle per Namespace
|
||||
4. **Route-spezifische `#[IpAuth]` Attribute** - Feinkörnige IP-Kontrolle per Route
|
||||
|
||||
## Architektur
|
||||
|
||||
```
|
||||
RouteAuthorizationService
|
||||
├── checkLegacyAuthAttribute()
|
||||
├── checkNamespaceAccessPolicy() [NEU]
|
||||
├── checkNamespaceIpRestrictions()
|
||||
└── checkRouteIpAuthAttribute()
|
||||
```
|
||||
|
||||
Der Service wird in der `RoutingMiddleware` **nach dem Routing** aufgerufen, sodass die gematchte Route bekannt ist.
|
||||
|
||||
## Konfiguration
|
||||
|
||||
### Initializer-basierte Konfiguration
|
||||
|
||||
**Location**: `src/Framework/Auth/RouteAuthorizationServiceInitializer.php`
|
||||
|
||||
```php
|
||||
#[Initializer]
|
||||
public function __invoke(Container $container): RouteAuthorizationService
|
||||
{
|
||||
$namespaceConfig = [
|
||||
// Namespace Pattern => Configuration
|
||||
'App\Application\Admin\*' => [
|
||||
'visibility' => 'admin', // IP-based restriction
|
||||
'access_policy' => NamespaceAccessPolicy::blocked()
|
||||
],
|
||||
];
|
||||
|
||||
return new RouteAuthorizationService(
|
||||
config: $this->config,
|
||||
namespaceConfig: $namespaceConfig
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Namespace Patterns
|
||||
|
||||
**Wildcard-basierte Patterns**:
|
||||
- `App\Application\Admin\*` - Matched alle Admin-Controller
|
||||
- `App\Application\Api\*` - Matched alle API-Controller
|
||||
- `App\Application\*` - Matched alle Application-Controller
|
||||
|
||||
**Exact Match**:
|
||||
- `App\Application\Admin\Dashboard` - Nur exakt dieser Namespace
|
||||
|
||||
## Use Cases
|
||||
|
||||
### 1. Admin-Bereich komplett blockieren
|
||||
|
||||
```php
|
||||
$namespaceConfig = [
|
||||
'App\Application\Admin\*' => [
|
||||
'access_policy' => NamespaceAccessPolicy::blocked()
|
||||
],
|
||||
];
|
||||
```
|
||||
|
||||
**Ergebnis**: Alle Admin-Controller werfen `RouteNotFound` (404)
|
||||
|
||||
### 2. Admin-Bereich mit Allowlist
|
||||
|
||||
```php
|
||||
use App\Application\Admin\LoginController;
|
||||
use App\Application\Admin\HealthCheckController;
|
||||
|
||||
$namespaceConfig = [
|
||||
'App\Application\Admin\*' => [
|
||||
'access_policy' => NamespaceAccessPolicy::blockedExcept(
|
||||
LoginController::class,
|
||||
HealthCheckController::class
|
||||
)
|
||||
],
|
||||
];
|
||||
```
|
||||
|
||||
**Ergebnis**:
|
||||
- ✅ `LoginController` und `HealthCheckController` öffentlich erreichbar
|
||||
- ❌ Alle anderen Admin-Controller blockiert
|
||||
|
||||
### 3. Kombination: IP-Restriction + Namespace-Blocking
|
||||
|
||||
```php
|
||||
$namespaceConfig = [
|
||||
'App\Application\Admin\*' => [
|
||||
'visibility' => 'admin', // Nur Admin-IPs erlaubt
|
||||
'access_policy' => NamespaceAccessPolicy::blockedExcept(
|
||||
LoginController::class // Aber Login ist public
|
||||
)
|
||||
],
|
||||
];
|
||||
```
|
||||
|
||||
**Ergebnis**:
|
||||
- ✅ `LoginController` - öffentlich erreichbar (trotz admin visibility)
|
||||
- 🔒 Alle anderen Admin-Controller - nur von Admin-IPs
|
||||
|
||||
### 4. API-Bereich mit IP-Restriction (ohne Blocking)
|
||||
|
||||
```php
|
||||
$namespaceConfig = [
|
||||
'App\Application\Api\*' => [
|
||||
'visibility' => 'local', // Nur localhost/private IPs
|
||||
// Kein access_policy - keine Namespace-Blockierung
|
||||
],
|
||||
];
|
||||
```
|
||||
|
||||
**Ergebnis**: API nur von localhost/private IPs erreichbar
|
||||
|
||||
### 5. Mehrere Namespace-Policies
|
||||
|
||||
```php
|
||||
$namespaceConfig = [
|
||||
// Admin komplett gesperrt
|
||||
'App\Application\Admin\*' => [
|
||||
'access_policy' => NamespaceAccessPolicy::blocked()
|
||||
],
|
||||
|
||||
// API nur von localhost
|
||||
'App\Application\Api\*' => [
|
||||
'visibility' => 'local'
|
||||
],
|
||||
|
||||
// Internal Tools nur admin IPs
|
||||
'App\Application\Internal\*' => [
|
||||
'visibility' => 'admin',
|
||||
'access_policy' => NamespaceAccessPolicy::blocked()
|
||||
],
|
||||
];
|
||||
```
|
||||
|
||||
## Value Objects
|
||||
|
||||
### NamespaceAccessPolicy
|
||||
|
||||
```php
|
||||
// Alle Controller im Namespace blockieren
|
||||
NamespaceAccessPolicy::blocked()
|
||||
|
||||
// Alle blockieren außer spezifische Controller
|
||||
NamespaceAccessPolicy::blockedExcept(
|
||||
LoginController::class,
|
||||
HealthCheckController::class
|
||||
)
|
||||
|
||||
// Alle erlauben (default)
|
||||
NamespaceAccessPolicy::allowed()
|
||||
|
||||
// Prüfen ob Controller blockiert ist
|
||||
$policy->isControllerBlocked(Dashboard::class) // true/false
|
||||
```
|
||||
|
||||
## Visibility Modes (IP-Restrictions)
|
||||
|
||||
**Predefined Modes**:
|
||||
- `public` - Keine IP-Restrictions
|
||||
- `admin` - Nur Admin-IPs (WireGuard, etc.)
|
||||
- `local` - Nur localhost/127.0.0.1
|
||||
- `development` - Development-IPs
|
||||
- `private` - Alias für `local`
|
||||
- `custom` - Custom IP-Liste via Config
|
||||
|
||||
## Execution Order
|
||||
|
||||
1. **Legacy Auth Attribute** - Backward compatibility check
|
||||
2. **Namespace Access Policy** - Block/Allow basierend auf Controller-Klasse
|
||||
3. **Namespace IP Restrictions** - IP-basierte Zugriffskontrolle
|
||||
4. **Route IP Auth Attribute** - Feinkörnige Route-Level IP-Kontrolle
|
||||
|
||||
Alle Checks werfen `RouteNotFound` bei Failure (versteckt Route-Existenz).
|
||||
|
||||
## Testing
|
||||
|
||||
### Unit Test Beispiel
|
||||
|
||||
```php
|
||||
use App\Framework\Auth\RouteAuthorizationService;
|
||||
use App\Framework\Auth\ValueObjects\NamespaceAccessPolicy;
|
||||
|
||||
it('blocks admin controllers except allowlist', function () {
|
||||
$config = ['App\Application\Admin\*' => [
|
||||
'access_policy' => NamespaceAccessPolicy::blockedExcept(
|
||||
LoginController::class
|
||||
)
|
||||
]];
|
||||
|
||||
$service = new RouteAuthorizationService(
|
||||
config: $this->config,
|
||||
namespaceConfig: $config
|
||||
);
|
||||
|
||||
// Should throw RouteNotFound for Dashboard
|
||||
expect(fn() => $service->authorize($request, $dashboardRoute))
|
||||
->toThrow(RouteNotFound::class);
|
||||
|
||||
// Should allow LoginController
|
||||
expect(fn() => $service->authorize($request, $loginRoute))
|
||||
->not->toThrow(RouteNotFound::class);
|
||||
});
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Namespace-Blocking für Production
|
||||
- Blockiere Admin/Internal-Bereiche in Production
|
||||
- Nutze Allowlist nur für wirklich öffentliche Endpoints (Login, Health)
|
||||
|
||||
### 2. IP-Restrictions für Sensitive Bereiche
|
||||
- Kombiniere Namespace-Blocking mit IP-Restrictions
|
||||
- Nutze `visibility: 'admin'` für maximale Sicherheit
|
||||
|
||||
### 3. Graceful Error Handling
|
||||
- Alle Checks werfen `RouteNotFound` (404)
|
||||
- Versteckt Route-Existenz vor Angreifern
|
||||
- Keine Information Leakage
|
||||
|
||||
### 4. Konfiguration via Initializer
|
||||
- Zentrale Konfiguration in `RouteAuthorizationServiceInitializer`
|
||||
- Environment-spezifische Configs möglich (dev vs. production)
|
||||
- Type-safe via Value Objects
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### Von altem System (RoutingMiddleware::withNamespaceConfig)
|
||||
|
||||
**Alt**:
|
||||
```php
|
||||
RoutingMiddleware::withNamespaceConfig(
|
||||
$router, $dispatcher, $config, $performance, $container,
|
||||
namespaceConfig: [
|
||||
'App\Application\Admin\*' => ['visibility' => 'admin']
|
||||
]
|
||||
);
|
||||
```
|
||||
|
||||
**Neu**:
|
||||
```php
|
||||
// In RouteAuthorizationServiceInitializer
|
||||
$namespaceConfig = [
|
||||
'App\Application\Admin\*' => [
|
||||
'visibility' => 'admin',
|
||||
'access_policy' => NamespaceAccessPolicy::blocked() // NEU
|
||||
]
|
||||
];
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Problem: Route wirft 404 obwohl sie erreichbar sein sollte
|
||||
|
||||
**Debugging**:
|
||||
1. Prüfe `RouteAuthorizationServiceInitializer` Config
|
||||
2. Check ob Controller-Namespace in `namespaceConfig` matched
|
||||
3. Prüfe `access_policy` - ist Controller in Allowlist?
|
||||
4. Check IP-Restrictions (`visibility`)
|
||||
|
||||
### Problem: Allowlist funktioniert nicht
|
||||
|
||||
**Ursache**: Controller-Klasse exakt mit `::class` angeben
|
||||
|
||||
```php
|
||||
// ❌ Falsch
|
||||
NamespaceAccessPolicy::blockedExcept('LoginController')
|
||||
|
||||
// ✅ Korrekt
|
||||
NamespaceAccessPolicy::blockedExcept(
|
||||
\App\Application\Admin\LoginController::class
|
||||
)
|
||||
```
|
||||
|
||||
## Framework Integration
|
||||
|
||||
- **Automatic Discovery**: Service wird via `#[Initializer]` automatisch registriert
|
||||
- **DI Container**: Alle Dependencies werden automatisch injected
|
||||
- **Type Safety**: Value Objects für alle Policies
|
||||
- **Readonly Classes**: Unveränderliche Policies für Thread-Safety
|
||||
- **Framework-konform**: Nutzt bestehende Patterns (IpAuthPolicy, RouteNotFound)
|
||||
@@ -1,306 +0,0 @@
|
||||
# Routing Value Objects
|
||||
|
||||
Das Framework unterstützt **parallele Routing-Ansätze** für maximale Flexibilität und Typsicherheit.
|
||||
|
||||
## Überblick
|
||||
|
||||
```php
|
||||
// ✅ Traditioneller String-Ansatz (schnell & gewohnt)
|
||||
#[Route(path: '/api/users/{id}')]
|
||||
|
||||
// ✅ Value Object-Ansatz (typsicher & framework-konform)
|
||||
#[Route(path: RoutePath::fromElements('api', 'users', Placeholder::fromString('id')))]
|
||||
```
|
||||
|
||||
Beide Ansätze sind **vollständig kompatibel** und können parallel verwendet werden.
|
||||
|
||||
## String-Basierte Routen
|
||||
|
||||
**Einfach und gewohnt** für schnelle Entwicklung:
|
||||
|
||||
```php
|
||||
final readonly class UserController
|
||||
{
|
||||
#[Route(path: '/api/users')]
|
||||
public function index(): JsonResult { /* ... */ }
|
||||
|
||||
#[Route(path: '/api/users/{id}')]
|
||||
public function show(int $id): JsonResult { /* ... */ }
|
||||
|
||||
#[Route(path: '/api/users/{id}/posts/{postId}')]
|
||||
public function showPost(int $id, int $postId): JsonResult { /* ... */ }
|
||||
|
||||
#[Route(path: '/files/{path*}', method: Method::GET)]
|
||||
public function downloadFile(string $path): StreamResult { /* ... */ }
|
||||
}
|
||||
```
|
||||
|
||||
## Value Object-Basierte Routen
|
||||
|
||||
**Typsicher und framework-konform** für Production-Code:
|
||||
|
||||
### Basis-Syntax
|
||||
|
||||
```php
|
||||
use App\Framework\Router\ValueObjects\RoutePath;
|
||||
use App\Framework\Router\ValueObjects\Placeholder;
|
||||
|
||||
final readonly class ImageController
|
||||
{
|
||||
#[Route(path: RoutePath::fromElements('images', Placeholder::fromString('filename')))]
|
||||
public function show(string $filename): ImageResult
|
||||
{
|
||||
return new ImageResult($this->imageService->getImage($filename));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Typisierte Parameter
|
||||
|
||||
```php
|
||||
#[Route(path: RoutePath::fromElements(
|
||||
'api',
|
||||
'users',
|
||||
Placeholder::typed('userId', 'uuid'),
|
||||
'posts',
|
||||
Placeholder::typed('postId', 'int')
|
||||
))]
|
||||
public function getUserPost(string $userId, int $postId): JsonResult
|
||||
{
|
||||
// userId wird automatisch als UUID validiert
|
||||
// postId wird automatisch als Integer validiert
|
||||
}
|
||||
```
|
||||
|
||||
### Verfügbare Parameter-Typen
|
||||
|
||||
| Typ | Regex Pattern | Beispiel |
|
||||
|-----|---------------|----------|
|
||||
| `int` | `(\d+)` | `/users/{id}` → 123 |
|
||||
| `uuid` | `([0-9a-f]{8}-...)` | `/users/{id}` → `550e8400-e29b-...` |
|
||||
| `slug` | `([a-z0-9\-]+)` | `/posts/{slug}` → `my-blog-post` |
|
||||
| `alpha` | `([a-zA-Z]+)` | `/category/{name}` → `Technology` |
|
||||
| `alphanumeric` | `([a-zA-Z0-9]+)` | `/code/{id}` → `ABC123` |
|
||||
| `filename` | `([a-zA-Z0-9._\-]+)` | `/files/{name}` → `image.jpg` |
|
||||
|
||||
### Wildcard-Parameter
|
||||
|
||||
```php
|
||||
#[Route(path: RoutePath::fromElements('files', Placeholder::wildcard('path')))]
|
||||
public function serveFile(string $path): StreamResult
|
||||
{
|
||||
// Matched: /files/uploads/2024/image.jpg
|
||||
// $path = "uploads/2024/image.jpg"
|
||||
}
|
||||
```
|
||||
|
||||
## Fluent Builder API
|
||||
|
||||
**Expressiver Builder** für komplexe Routen:
|
||||
|
||||
```php
|
||||
final readonly class ApiController
|
||||
{
|
||||
#[Route(path: RoutePath::create()
|
||||
->segment('api')
|
||||
->segment('v1')
|
||||
->segment('users')
|
||||
->typedParameter('userId', 'uuid')
|
||||
->segment('posts')
|
||||
->typedParameter('postId', 'int')
|
||||
->build()
|
||||
)]
|
||||
public function getUserPost(string $userId, int $postId): JsonResult { /* ... */ }
|
||||
|
||||
// Quick Helper für häufige Patterns
|
||||
#[Route(path: RoutePath::create()->segments('api', 'users')->uuid())]
|
||||
public function showUser(string $id): JsonResult { /* ... */ }
|
||||
}
|
||||
```
|
||||
|
||||
## Praktische Beispiele
|
||||
|
||||
### RESTful API Routes
|
||||
|
||||
```php
|
||||
final readonly class ProductController
|
||||
{
|
||||
// String-Ansatz für einfache Routen
|
||||
#[Route(path: '/api/products', method: Method::GET)]
|
||||
public function index(): JsonResult { }
|
||||
|
||||
#[Route(path: '/api/products', method: Method::POST)]
|
||||
public function create(CreateProductRequest $request): JsonResult { }
|
||||
|
||||
// Value Object-Ansatz für komplexe Routen
|
||||
#[Route(path: RoutePath::fromElements(
|
||||
'api',
|
||||
'products',
|
||||
Placeholder::typed('productId', 'uuid'),
|
||||
'reviews',
|
||||
Placeholder::typed('reviewId', 'int')
|
||||
), method: Method::GET)]
|
||||
public function getProductReview(string $productId, int $reviewId): JsonResult { }
|
||||
}
|
||||
```
|
||||
|
||||
### File Serving
|
||||
|
||||
```php
|
||||
final readonly class FileController
|
||||
{
|
||||
// Static files (string)
|
||||
#[Route(path: '/assets/{type}/{filename}')]
|
||||
public function staticAsset(string $type, string $filename): StreamResult { }
|
||||
|
||||
// Dynamic file paths (Value Object mit Wildcard)
|
||||
#[Route(path: RoutePath::fromElements(
|
||||
'uploads',
|
||||
Placeholder::wildcard('path')
|
||||
))]
|
||||
public function uploadedFile(string $path): StreamResult { }
|
||||
}
|
||||
```
|
||||
|
||||
### Admin Routes
|
||||
|
||||
```php
|
||||
final readonly class AdminController
|
||||
{
|
||||
// String für bekannte Admin-Pfade
|
||||
#[Route(path: '/admin/dashboard')]
|
||||
#[Auth(strategy: 'ip', allowedIps: ['127.0.0.1'])]
|
||||
public function dashboard(): ViewResult { }
|
||||
|
||||
// Value Objects für dynamische Admin-Actions
|
||||
#[Route(path: RoutePath::fromElements(
|
||||
'admin',
|
||||
'users',
|
||||
Placeholder::typed('userId', 'uuid'),
|
||||
'actions',
|
||||
Placeholder::fromString('action')
|
||||
))]
|
||||
#[Auth(strategy: 'session', roles: ['admin'])]
|
||||
public function userAction(string $userId, string $action): JsonResult { }
|
||||
}
|
||||
```
|
||||
|
||||
## Migration & Kompatibilität
|
||||
|
||||
### Bestehende Routen bleiben unverändert
|
||||
|
||||
```php
|
||||
// ✅ Weiterhin gültig und funktional
|
||||
#[Route(path: '/api/users/{id}')]
|
||||
public function show(int $id): JsonResult { }
|
||||
```
|
||||
|
||||
### Schrittweise Migration
|
||||
|
||||
```php
|
||||
final readonly class UserController
|
||||
{
|
||||
// Phase 1: Strings für einfache Routen
|
||||
#[Route(path: '/users')]
|
||||
public function index(): ViewResult { }
|
||||
|
||||
// Phase 2: Value Objects für neue komplexe Routen
|
||||
#[Route(path: RoutePath::fromElements(
|
||||
'users',
|
||||
Placeholder::typed('userId', 'uuid'),
|
||||
'preferences',
|
||||
Placeholder::fromString('section')
|
||||
))]
|
||||
public function userPreferences(string $userId, string $section): JsonResult { }
|
||||
}
|
||||
```
|
||||
|
||||
### Konsistenz-Check
|
||||
|
||||
```php
|
||||
// Beide Ansätze sind äquivalent
|
||||
$stringRoute = new Route(path: '/api/users/{id}');
|
||||
$objectRoute = new Route(path: RoutePath::fromElements('api', 'users', Placeholder::fromString('id')));
|
||||
|
||||
$stringRoute->getPathAsString() === $objectRoute->getPathAsString(); // true
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Wann String verwenden
|
||||
|
||||
- **Prototyping**: Schnelle Route-Erstellung
|
||||
- **Einfache Routen**: Statische oder 1-Parameter-Routen
|
||||
- **Legacy-Kompatibilität**: Bestehende Routen beibehalten
|
||||
|
||||
```php
|
||||
// ✅ Gut für einfache Fälle
|
||||
#[Route(path: '/health')]
|
||||
#[Route(path: '/api/status')]
|
||||
#[Route(path: '/users/{id}')]
|
||||
```
|
||||
|
||||
### Wann Value Objects verwenden
|
||||
|
||||
- **Production-Code**: Maximale Typsicherheit
|
||||
- **Komplexe Routen**: Mehrere Parameter mit Validierung
|
||||
- **API-Endpoints**: Starke Typisierung für externe Schnittstellen
|
||||
- **Framework-Konsistenz**: Vollständige Value Object-Nutzung
|
||||
|
||||
```php
|
||||
// ✅ Gut für komplexe Fälle
|
||||
#[Route(path: RoutePath::fromElements(
|
||||
'api',
|
||||
'v2',
|
||||
'organizations',
|
||||
Placeholder::typed('orgId', 'uuid'),
|
||||
'projects',
|
||||
Placeholder::typed('projectId', 'slug'),
|
||||
'files',
|
||||
Placeholder::wildcard('filePath')
|
||||
))]
|
||||
```
|
||||
|
||||
### Hybrid-Ansatz (Empfohlen)
|
||||
|
||||
```php
|
||||
final readonly class ProjectController
|
||||
{
|
||||
// String für einfache Routen
|
||||
#[Route(path: '/projects')]
|
||||
public function index(): ViewResult { }
|
||||
|
||||
// Value Objects für komplexe/kritische Routen
|
||||
#[Route(path: RoutePath::fromElements(
|
||||
'api',
|
||||
'projects',
|
||||
Placeholder::typed('projectId', 'uuid'),
|
||||
'members',
|
||||
Placeholder::typed('memberId', 'uuid')
|
||||
))]
|
||||
public function getProjectMember(string $projectId, string $memberId): JsonResult { }
|
||||
}
|
||||
```
|
||||
|
||||
## Router-Integration
|
||||
|
||||
Das Framework konvertiert automatisch zwischen beiden Ansätzen:
|
||||
|
||||
```php
|
||||
// Route-Attribute bietet einheitliche Interface
|
||||
public function getPathAsString(): string // Immer String für Router
|
||||
public function getRoutePath(): RoutePath // Immer RoutePath-Objekt
|
||||
|
||||
// RouteCompiler verwendet automatisch getPathAsString()
|
||||
$path = $routeAttribute->getPathAsString(); // Funktioniert für beide
|
||||
```
|
||||
|
||||
## Fazit
|
||||
|
||||
- **String-Routen**: Schnell, gewohnt, ideal für einfache Fälle
|
||||
- **Value Object-Routen**: Typsicher, framework-konform, ideal für komplexe Fälle
|
||||
- **Vollständige Kompatibilität**: Beide Ansätze parallel nutzbar
|
||||
- **Keine Breaking Changes**: Bestehender Code funktioniert weiterhin
|
||||
- **Schrittweise Adoption**: Migration nach Bedarf möglich
|
||||
|
||||
**Empfehlung**: Hybrid-Ansatz mit Strings für einfache und Value Objects für komplexe Routen.
|
||||
@@ -1,458 +0,0 @@
|
||||
# Scheduler-Queue Pipeline
|
||||
|
||||
Komplette Dokumentation der Scheduler-Queue Integration Pipeline im Custom PHP Framework.
|
||||
|
||||
## Übersicht
|
||||
|
||||
Die Scheduler-Queue Pipeline ist eine vollständig integrierte Lösung für zeitbasierte Aufgabenplanung und asynchrone Job-Ausführung. Sie kombiniert das Framework's Scheduler System mit dem erweiterten Queue System für robuste, skalierbare Background-Processing.
|
||||
|
||||
## Architektur
|
||||
|
||||
```
|
||||
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||
│ Scheduler │───▶│ Job Dispatch │───▶│ Queue System │───▶│ Background Exec │
|
||||
│ System │ │ & Validation │ │ (13 Tables) │ │ & Logging │
|
||||
└─────────────────┘ └─────────────────┘ └─────────────────┘ └─────────────────┘
|
||||
│ │ │ │
|
||||
Cron/Interval JobPayload FileQueue/Redis Named Job Classes
|
||||
OneTime/Manual Value Objects w/ Metrics w/ handle() Method
|
||||
Schedule Types Type Safety & Monitoring Result Logging
|
||||
```
|
||||
|
||||
## Pipeline Komponenten
|
||||
|
||||
### 1. Scheduler System
|
||||
**Location**: `src/Framework/Scheduler/`
|
||||
|
||||
**Schedule Types**:
|
||||
- **CronSchedule**: Traditionelle Cron-basierte Zeitplanung
|
||||
- **IntervalSchedule**: Wiederholende Ausführung in festen Intervallen
|
||||
- **OneTimeSchedule**: Einmalige Ausführung zu bestimmter Zeit
|
||||
- **ManualSchedule**: Manuelle Trigger-basierte Ausführung
|
||||
|
||||
**Core Services**:
|
||||
- `SchedulerService`: Hauptservice für Task-Management
|
||||
- `TaskExecutionResult`: Value Object für Execution-Ergebnisse
|
||||
- `Timestamp`/`Duration`: Zeit-Value Objects mit Framework-Compliance
|
||||
|
||||
### 2. Job Dispatch & Validation
|
||||
**Integration Layer zwischen Scheduler und Queue**
|
||||
|
||||
**Key Features**:
|
||||
- **Type Safety**: Verwendung von JobPayload Value Objects
|
||||
- **Named Job Classes**: Vermeidung von Anonymous Classes (ClassName Restrictions)
|
||||
- **Validation**: Input-Validierung vor Queue-Dispatch
|
||||
- **Error Handling**: Graceful Fallbacks bei Dispatch-Fehlern
|
||||
|
||||
### 3. Queue System
|
||||
**Location**: `src/Framework/Queue/`
|
||||
|
||||
**Database Schema** (13 Specialized Tables):
|
||||
```sql
|
||||
jobs -- Haupt-Job-Queue
|
||||
job_batches -- Batch-Job-Verwaltung
|
||||
job_history -- Execution-Historie
|
||||
job_metrics -- Performance-Metriken
|
||||
dead_letter_jobs -- Fehlgeschlagene Jobs
|
||||
job_priorities -- Prioritäts-basierte Scheduling
|
||||
job_dependencies -- Inter-Job Dependencies
|
||||
delayed_jobs -- Zeit-verzögerte Ausführung
|
||||
worker_processes -- Worker-Management
|
||||
job_locks -- Distributed Locking
|
||||
job_progress -- Progress Tracking
|
||||
recurring_jobs -- Wiederkehrende Patterns
|
||||
job_tags -- Kategorisierung & Filtering
|
||||
```
|
||||
|
||||
**Core Features**:
|
||||
- **Multi-Driver Support**: FileQueue, Redis, Database
|
||||
- **Priority Scheduling**: High/Medium/Low Priority Queues
|
||||
- **Metrics & Monitoring**: Real-time Performance Tracking
|
||||
- **Dead Letter Queue**: Automatic Failed Job Management
|
||||
- **Distributed Locking**: Multi-Worker Coordination
|
||||
|
||||
### 4. Background Execution & Logging
|
||||
**Job Processing mit umfassender Protokollierung**
|
||||
|
||||
**Execution Pattern**:
|
||||
```php
|
||||
final class ScheduledBackgroundJob
|
||||
{
|
||||
public function handle(): array
|
||||
{
|
||||
// Business Logic Execution
|
||||
$result = $this->performWork();
|
||||
|
||||
// Automatic Logging
|
||||
$this->logExecution($result);
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Verwendung
|
||||
|
||||
### Basic Scheduler-Queue Integration
|
||||
|
||||
```php
|
||||
use App\Framework\Scheduler\Services\SchedulerService;
|
||||
use App\Framework\Scheduler\Schedules\IntervalSchedule;
|
||||
use App\Framework\Queue\Queue;
|
||||
use App\Framework\Queue\ValueObjects\JobPayload;
|
||||
use App\Framework\Core\ValueObjects\Duration;
|
||||
|
||||
// 1. Schedule Setup
|
||||
$scheduler = $container->get(SchedulerService::class);
|
||||
$queue = $container->get(Queue::class);
|
||||
|
||||
// 2. Create Interval Schedule (every 10 minutes)
|
||||
$schedule = IntervalSchedule::every(Duration::fromMinutes(10));
|
||||
|
||||
// 3. Register Task with Queue Dispatch
|
||||
$scheduler->schedule('email-cleanup', $schedule, function() use ($queue) {
|
||||
$job = new EmailCleanupJob(
|
||||
olderThan: Duration::fromDays(30),
|
||||
batchSize: 100
|
||||
);
|
||||
|
||||
$payload = JobPayload::immediate($job);
|
||||
$queue->push($payload);
|
||||
|
||||
return ['status' => 'queued', 'timestamp' => time()];
|
||||
});
|
||||
```
|
||||
|
||||
### Advanced Pipeline Configuration
|
||||
|
||||
```php
|
||||
// Complex Multi-Stage Pipeline
|
||||
final class ReportGenerationPipeline
|
||||
{
|
||||
public function __construct(
|
||||
private readonly SchedulerService $scheduler,
|
||||
private readonly Queue $queue
|
||||
) {}
|
||||
|
||||
public function setupDailyReports(): void
|
||||
{
|
||||
// Stage 1: Data Collection (Daily at 2 AM)
|
||||
$this->scheduler->schedule(
|
||||
'daily-data-collection',
|
||||
CronSchedule::fromExpression('0 2 * * *'),
|
||||
fn() => $this->dispatchDataCollection()
|
||||
);
|
||||
|
||||
// Stage 2: Report Generation (After data collection)
|
||||
$this->scheduler->schedule(
|
||||
'daily-report-generation',
|
||||
CronSchedule::fromExpression('30 2 * * *'),
|
||||
fn() => $this->dispatchReportGeneration()
|
||||
);
|
||||
|
||||
// Stage 3: Distribution (After generation)
|
||||
$this->scheduler->schedule(
|
||||
'daily-report-distribution',
|
||||
CronSchedule::fromExpression('0 3 * * *'),
|
||||
fn() => $this->dispatchReportDistribution()
|
||||
);
|
||||
}
|
||||
|
||||
private function dispatchDataCollection(): array
|
||||
{
|
||||
$job = new DataCollectionJob(
|
||||
sources: ['database', 'analytics', 'external_apis'],
|
||||
target_date: Timestamp::yesterday()
|
||||
);
|
||||
|
||||
$payload = JobPayload::withPriority($job, Priority::HIGH);
|
||||
$this->queue->push($payload);
|
||||
|
||||
return ['stage' => 'data_collection', 'queued_at' => time()];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Named Job Classes (Best Practice)
|
||||
|
||||
```php
|
||||
// ✅ Framework-Compliant Job Class
|
||||
final class EmailCleanupJob
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Duration $olderThan,
|
||||
private readonly int $batchSize = 100
|
||||
) {}
|
||||
|
||||
public function handle(): array
|
||||
{
|
||||
$deleted = $this->emailService->deleteOldEmails(
|
||||
$this->olderThan,
|
||||
$this->batchSize
|
||||
);
|
||||
|
||||
$this->logCleanupResults($deleted);
|
||||
|
||||
return [
|
||||
'deleted_count' => count($deleted),
|
||||
'batch_size' => $this->batchSize,
|
||||
'cleanup_threshold' => $this->olderThan->toDays() . ' days'
|
||||
];
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return 'email-cleanup';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Pipeline Monitoring
|
||||
|
||||
### Health Checks
|
||||
|
||||
```php
|
||||
// Pipeline Health Verification
|
||||
final class PipelineHealthChecker
|
||||
{
|
||||
public function checkPipelineHealth(): PipelineHealthReport
|
||||
{
|
||||
return new PipelineHealthReport([
|
||||
'scheduler_status' => $this->checkSchedulerHealth(),
|
||||
'queue_status' => $this->checkQueueHealth(),
|
||||
'integration_status' => $this->checkIntegrationHealth(),
|
||||
'performance_metrics' => $this->gatherPerformanceMetrics()
|
||||
]);
|
||||
}
|
||||
|
||||
private function checkSchedulerHealth(): array
|
||||
{
|
||||
$dueTasks = $this->scheduler->getDueTasks();
|
||||
$nextExecution = $this->scheduler->getNextExecutionTime();
|
||||
|
||||
return [
|
||||
'due_tasks_count' => count($dueTasks),
|
||||
'next_execution' => $nextExecution?->format('Y-m-d H:i:s'),
|
||||
'status' => count($dueTasks) < 100 ? 'healthy' : 'overloaded'
|
||||
];
|
||||
}
|
||||
|
||||
private function checkQueueHealth(): array
|
||||
{
|
||||
$stats = $this->queue->getStats();
|
||||
|
||||
return [
|
||||
'queue_size' => $stats['total_size'],
|
||||
'priority_distribution' => $stats['priority_breakdown'],
|
||||
'processing_rate' => $this->calculateProcessingRate(),
|
||||
'status' => $stats['total_size'] < 1000 ? 'healthy' : 'backlog'
|
||||
];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Performance Metrics
|
||||
|
||||
```php
|
||||
// Real-time Pipeline Performance
|
||||
final class PipelineMetricsCollector
|
||||
{
|
||||
public function collectMetrics(string $timeframe = '1hour'): array
|
||||
{
|
||||
return [
|
||||
'scheduler_metrics' => [
|
||||
'tasks_executed' => $this->getExecutedTasksCount($timeframe),
|
||||
'average_execution_time' => $this->getAverageExecutionTime($timeframe),
|
||||
'success_rate' => $this->getSchedulerSuccessRate($timeframe)
|
||||
],
|
||||
'queue_metrics' => [
|
||||
'jobs_processed' => $this->getProcessedJobsCount($timeframe),
|
||||
'average_wait_time' => $this->getAverageWaitTime($timeframe),
|
||||
'throughput' => $this->calculateThroughput($timeframe)
|
||||
],
|
||||
'integration_metrics' => [
|
||||
'dispatch_success_rate' => $this->getDispatchSuccessRate($timeframe),
|
||||
'end_to_end_latency' => $this->getEndToEndLatency($timeframe),
|
||||
'error_rate' => $this->getIntegrationErrorRate($timeframe)
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Häufige Probleme
|
||||
|
||||
**1. Jobs werden nicht ausgeführt**
|
||||
```bash
|
||||
# Diagnose Queue Status
|
||||
docker exec php php console.php queue:status
|
||||
|
||||
# Check Scheduler Tasks
|
||||
docker exec php php console.php scheduler:status
|
||||
|
||||
# Verify Integration
|
||||
docker exec php php tests/debug/test-scheduler-queue-integration-fixed.php
|
||||
```
|
||||
|
||||
**2. Memory Leaks bei großen Jobs**
|
||||
```php
|
||||
// Memory-effiziente Job Implementation
|
||||
final class LargeDataProcessingJob
|
||||
{
|
||||
public function handle(): array
|
||||
{
|
||||
// Batch Processing to prevent memory exhaustion
|
||||
$batch = $this->dataSource->getBatch($this->batchSize);
|
||||
|
||||
while (!empty($batch)) {
|
||||
$this->processBatch($batch);
|
||||
|
||||
// Force garbage collection
|
||||
gc_collect_cycles();
|
||||
|
||||
$batch = $this->dataSource->getNextBatch($this->batchSize);
|
||||
}
|
||||
|
||||
return ['processed' => $this->totalProcessed];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**3. Queue Backlog Management**
|
||||
```php
|
||||
// Automatic Backlog Resolution
|
||||
final class BacklogManager
|
||||
{
|
||||
public function resolveBacklog(): void
|
||||
{
|
||||
$stats = $this->queue->getStats();
|
||||
|
||||
if ($stats['total_size'] > $this->backlogThreshold) {
|
||||
// Scale up workers
|
||||
$this->workerManager->scaleUp($this->calculateRequiredWorkers($stats));
|
||||
|
||||
// Prioritize critical jobs
|
||||
$this->queue->reprioritize(['high_priority_types']);
|
||||
|
||||
// Alert operations team
|
||||
$this->alertManager->sendBacklogAlert($stats);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Integration Tests
|
||||
|
||||
```php
|
||||
// Complete Pipeline Testing
|
||||
describe('Scheduler Queue Pipeline', function () {
|
||||
it('handles full pipeline flow', function () {
|
||||
// 1. Setup Scheduler Task
|
||||
$schedule = IntervalSchedule::every(Duration::fromSeconds(1));
|
||||
$this->scheduler->schedule('test-task', $schedule, function() {
|
||||
$job = new TestPipelineJob('pipeline-test');
|
||||
$payload = JobPayload::immediate($job);
|
||||
$this->queue->push($payload);
|
||||
return ['dispatched' => true];
|
||||
});
|
||||
|
||||
// 2. Execute Scheduler
|
||||
$results = $this->scheduler->executeDueTasks();
|
||||
expect($results)->toHaveCount(1);
|
||||
expect($results[0]->success)->toBeTrue();
|
||||
|
||||
// 3. Process Queue
|
||||
$jobPayload = $this->queue->pop();
|
||||
expect($jobPayload)->not->toBeNull();
|
||||
|
||||
$result = $jobPayload->job->handle();
|
||||
expect($result['status'])->toBe('completed');
|
||||
|
||||
// 4. Verify End-to-End
|
||||
expect($this->queue->size())->toBe(0);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Performance Tests
|
||||
|
||||
```php
|
||||
// Pipeline Performance Benchmarks
|
||||
describe('Pipeline Performance', function () {
|
||||
it('processes 1000 jobs within 30 seconds', function () {
|
||||
$startTime = microtime(true);
|
||||
|
||||
// Dispatch 1000 jobs via scheduler
|
||||
for ($i = 0; $i < 1000; $i++) {
|
||||
$job = new BenchmarkJob("job-{$i}");
|
||||
$payload = JobPayload::immediate($job);
|
||||
$this->queue->push($payload);
|
||||
}
|
||||
|
||||
// Process all jobs
|
||||
while ($this->queue->size() > 0) {
|
||||
$jobPayload = $this->queue->pop();
|
||||
$jobPayload->job->handle();
|
||||
}
|
||||
|
||||
$executionTime = microtime(true) - $startTime;
|
||||
expect($executionTime)->toBeLessThan(30.0);
|
||||
expect($this->queue->size())->toBe(0);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Job Design
|
||||
- **Named Classes**: Immer Named Classes statt Anonymous Functions verwenden
|
||||
- **Type Safety**: JobPayload Value Objects für alle Queue-Operationen
|
||||
- **Idempotency**: Jobs sollten mehrfach ausführbar sein ohne Seiteneffekte
|
||||
- **Error Handling**: Graceful Degradation bei Fehlern implementieren
|
||||
|
||||
### 2. Scheduler Configuration
|
||||
- **Reasonable Intervals**: Nicht zu aggressive Scheduling-Intervalle
|
||||
- **Resource Awareness**: CPU/Memory-Limits bei Task-Design beachten
|
||||
- **Monitoring**: Kontinuierliche Überwachung der Execution-Times
|
||||
- **Failover**: Backup-Strategien für kritische Tasks
|
||||
|
||||
### 3. Queue Management
|
||||
- **Priority Classes**: Sinnvolle Priorisierung für verschiedene Job-Types
|
||||
- **Batch Processing**: Große Datenmengen in Batches aufteilen
|
||||
- **Dead Letter Handling**: Automatische Failed-Job-Recovery
|
||||
- **Metrics Collection**: Performance-Daten für Optimierung sammeln
|
||||
|
||||
### 4. Production Deployment
|
||||
- **Health Checks**: Regelmäßige Pipeline-Health-Verification
|
||||
- **Alerting**: Automatische Benachrichtigungen bei Problemen
|
||||
- **Scaling**: Auto-Scaling für Worker-Processes
|
||||
- **Backup**: Disaster-Recovery-Strategien für Queue-Daten
|
||||
|
||||
## Framework Integration
|
||||
|
||||
Die Pipeline nutzt konsequent Framework-Patterns:
|
||||
- **Value Objects**: Timestamp, Duration, JobPayload, Priority
|
||||
- **Readonly Classes**: Unveränderliche Job-Definitionen
|
||||
- **Event System**: Integration mit Framework's Event Dispatcher
|
||||
- **DI Container**: Automatic Service Resolution
|
||||
- **Attribute Discovery**: Convention-over-Configuration
|
||||
- **MCP Integration**: AI-gestützte Pipeline-Analyse
|
||||
|
||||
## Performance Charakteristiken
|
||||
|
||||
**Typische Performance-Werte**:
|
||||
- **Job Dispatch Latency**: < 50ms
|
||||
- **Queue Throughput**: 1000+ Jobs/Minute (FileQueue)
|
||||
- **Memory Usage**: < 50MB für Standard-Jobs
|
||||
- **Scheduler Precision**: ±1 Second für Cron-basierte Tasks
|
||||
- **End-to-End Latency**: < 500ms für einfache Jobs
|
||||
|
||||
**Skalierungscharakteristiken**:
|
||||
- **Horizontal Scaling**: Multi-Worker Support
|
||||
- **Queue Capacity**: 100,000+ Jobs (Database-backed)
|
||||
- **Scheduler Load**: 10,000+ concurrent scheduled tasks
|
||||
- **Memory Efficiency**: Linear scaling mit Job-Complexity
|
||||
File diff suppressed because it is too large
Load Diff
671
docs/claude/sockets-module.md
Normal file
671
docs/claude/sockets-module.md
Normal file
@@ -0,0 +1,671 @@
|
||||
# Sockets Module Documentation
|
||||
|
||||
## Übersicht
|
||||
|
||||
Das Sockets-Modul (`src/Framework/Sockets/`) bietet eine vollständige, type-safe Abstraktion über PHP's Socket-Erweiterung. Es ermöglicht:
|
||||
|
||||
- **Type-safe Socket-Operationen**: Alle Socket-Operationen verwenden Value Objects statt primitiver Typen
|
||||
- **TCP/UDP Server & Client**: Vollständige Server- und Client-Implementierung
|
||||
- **Unix Domain Sockets**: Unterstützung für Unix Domain Sockets
|
||||
- **PCNTL Integration**: Socket-Server in geforkten Worker-Prozessen
|
||||
- **Async Integration**: Non-blocking Socket I/O mit Fibers
|
||||
- **Connection Pooling**: Automatisches Management von Socket-Verbindungen
|
||||
|
||||
## Architektur
|
||||
|
||||
### Modulstruktur
|
||||
|
||||
```
|
||||
src/Framework/Sockets/
|
||||
├── SocketService.php # Haupt-Service (Facade)
|
||||
├── SocketFactory.php # Socket-Erstellung
|
||||
├── SocketServer.php # Server-Operationen
|
||||
├── SocketClient.php # Client-Operationen
|
||||
├── SocketConnection.php # Connection Wrapper
|
||||
├── SocketConnectionPool.php # Connection Pool Management
|
||||
├── SocketsInitializer.php # DI-Initializer
|
||||
├── Exceptions/ # Exception-Klassen
|
||||
├── ValueObjects/ # Type-safe Value Objects
|
||||
└── Integration/ # PCNTL & Async Integration
|
||||
```
|
||||
|
||||
## Value Objects
|
||||
|
||||
### SocketAddress
|
||||
|
||||
Repräsentiert eine Socket-Adresse (IPv4, IPv6 oder Unix Domain Socket).
|
||||
|
||||
```php
|
||||
use App\Framework\Sockets\ValueObjects\SocketAddress;
|
||||
|
||||
// IPv4 Adresse
|
||||
$address = SocketAddress::ipv4('127.0.0.1', 8080);
|
||||
|
||||
// IPv6 Adresse
|
||||
$address = SocketAddress::ipv6('::1', 8080);
|
||||
|
||||
// Unix Domain Socket
|
||||
$address = SocketAddress::unix('/tmp/socket.sock');
|
||||
|
||||
// Von String parsen
|
||||
$address = SocketAddress::fromString('127.0.0.1:8080');
|
||||
$address = SocketAddress::fromString('/tmp/socket.sock');
|
||||
|
||||
// Eigenschaften
|
||||
$host = $address->getHost(); // null für Unix Sockets
|
||||
$port = $address->getPort(); // null für Unix Sockets
|
||||
$path = $address->getUnixPath(); // null für Network Sockets
|
||||
$protocol = $address->getProtocol(); // SocketProtocol Enum
|
||||
$isUnix = $address->isUnixSocket();
|
||||
$isNetwork = $address->isNetworkSocket();
|
||||
$string = $address->toString(); // "127.0.0.1:8080" oder "/tmp/socket.sock"
|
||||
```
|
||||
|
||||
### SocketType
|
||||
|
||||
Enum für Socket-Typen.
|
||||
|
||||
```php
|
||||
use App\Framework\Sockets\ValueObjects\SocketType;
|
||||
|
||||
SocketType::TCP // SOCK_STREAM
|
||||
SocketType::UDP // SOCK_DGRAM
|
||||
SocketType::RAW // SOCK_RAW
|
||||
SocketType::RDM // SOCK_RDM
|
||||
SocketType::SEQPACKET // SOCK_SEQPACKET
|
||||
|
||||
// Methoden
|
||||
$type->getName(); // "TCP", "UDP", etc.
|
||||
$type->getValue(); // SOCK_STREAM, SOCK_DGRAM, etc.
|
||||
$type->isReliable(); // true für TCP, SEQPACKET
|
||||
$type->isConnectionless(); // true für UDP
|
||||
```
|
||||
|
||||
### SocketProtocol
|
||||
|
||||
Enum für Protokoll-Familien.
|
||||
|
||||
```php
|
||||
use App\Framework\Sockets\ValueObjects\SocketProtocol;
|
||||
|
||||
SocketProtocol::IPv4 // AF_INET
|
||||
SocketProtocol::IPv6 // AF_INET6
|
||||
SocketProtocol::UNIX // AF_UNIX
|
||||
|
||||
// Methoden
|
||||
$protocol->getName(); // "IPv4", "IPv6", "Unix Domain"
|
||||
$protocol->getValue(); // AF_INET, AF_INET6, AF_UNIX
|
||||
$protocol->isNetwork(); // true für IPv4/IPv6
|
||||
$protocol->isUnix(); // true für Unix Domain
|
||||
```
|
||||
|
||||
### SocketOption
|
||||
|
||||
Enum für Socket-Optionen.
|
||||
|
||||
```php
|
||||
use App\Framework\Sockets\ValueObjects\SocketOption;
|
||||
|
||||
SocketOption::SO_REUSEADDR
|
||||
SocketOption::SO_REUSEPORT
|
||||
SocketOption::SO_KEEPALIVE
|
||||
SocketOption::SO_LINGER
|
||||
SocketOption::SO_RCVBUF
|
||||
SocketOption::SO_SNDBUF
|
||||
SocketOption::SO_RCVTIMEO
|
||||
SocketOption::SO_SNDTIMEO
|
||||
SocketOption::TCP_NODELAY
|
||||
SocketOption::TCP_KEEPIDLE
|
||||
SocketOption::TCP_KEEPINTVL
|
||||
SocketOption::TCP_KEEPCNT
|
||||
|
||||
// Methoden
|
||||
$option->getName(); // "SO_REUSEADDR", etc.
|
||||
$option->getLevel(); // SOL_SOCKET oder SOL_TCP
|
||||
$option->getValue(); // SO_REUSEADDR, etc.
|
||||
```
|
||||
|
||||
### SocketResource
|
||||
|
||||
Type-safe Wrapper für Socket-Ressourcen mit automatischem Cleanup.
|
||||
|
||||
```php
|
||||
use App\Framework\Sockets\ValueObjects\SocketResource;
|
||||
|
||||
$resource = SocketResource::fromResource($socket, $type, $protocol);
|
||||
|
||||
// Eigenschaften
|
||||
$socketResource = $resource->getResource(); // Native Socket-Resource
|
||||
$type = $resource->getType(); // SocketType
|
||||
$protocol = $resource->getProtocol(); // SocketProtocol
|
||||
$isClosed = $resource->isClosed();
|
||||
|
||||
// Manuelles Schließen (automatisch im Destructor)
|
||||
$resource->close();
|
||||
```
|
||||
|
||||
## Core Services
|
||||
|
||||
### SocketFactory
|
||||
|
||||
Factory für Socket-Erstellung mit automatischem Option-Setup.
|
||||
|
||||
```php
|
||||
use App\Framework\Sockets\SocketFactory;
|
||||
use App\Framework\Sockets\ValueObjects\SocketAddress;
|
||||
use App\Framework\Sockets\ValueObjects\SocketType;
|
||||
use App\Framework\Sockets\ValueObjects\SocketProtocol;
|
||||
|
||||
$factory = new SocketFactory();
|
||||
|
||||
// TCP Socket erstellen
|
||||
$address = SocketAddress::ipv4('127.0.0.1', 8080);
|
||||
$socket = $factory->createTcp($address);
|
||||
|
||||
// UDP Socket erstellen
|
||||
$socket = $factory->createUdp($address);
|
||||
|
||||
// Unix Domain Socket erstellen
|
||||
$unixAddress = SocketAddress::unix('/tmp/socket.sock');
|
||||
$socket = $factory->createUnix($unixAddress);
|
||||
|
||||
// Generische Erstellung
|
||||
$socket = $factory->createServer(SocketType::TCP, SocketProtocol::IPv4);
|
||||
$socket = $factory->createClient(SocketType::TCP, SocketProtocol::IPv4);
|
||||
```
|
||||
|
||||
### SocketServer
|
||||
|
||||
Server-Operationen für Socket-Verwaltung.
|
||||
|
||||
```php
|
||||
use App\Framework\Sockets\SocketServer;
|
||||
use App\Framework\Sockets\ValueObjects\SocketAddress;
|
||||
use App\Framework\Sockets\ValueObjects\SocketResource;
|
||||
|
||||
$server = new SocketServer();
|
||||
$address = SocketAddress::ipv4('0.0.0.0', 8080);
|
||||
|
||||
// Socket binden
|
||||
$server->bind($socket, $address);
|
||||
|
||||
// Auf Verbindungen lauschen
|
||||
$server->listen($socket, 10); // backlog = 10
|
||||
|
||||
// Verbindung akzeptieren (non-blocking)
|
||||
$connection = $server->accept($socket);
|
||||
if ($connection !== null) {
|
||||
// Verbindung verarbeiten
|
||||
$clientSocket = $connection->getSocket();
|
||||
$clientAddress = $connection->getAddress();
|
||||
}
|
||||
|
||||
// Socket-Auswahl (select)
|
||||
$read = [$socket];
|
||||
$write = [];
|
||||
$except = [];
|
||||
$numReady = $server->select($read, $write, $except, 1); // 1 Sekunde Timeout
|
||||
|
||||
// Non-blocking/Blocking Mode
|
||||
$server->setNonBlocking($socket);
|
||||
$server->setBlocking($socket);
|
||||
```
|
||||
|
||||
### SocketClient
|
||||
|
||||
Client-Operationen für Socket-Verbindungen.
|
||||
|
||||
```php
|
||||
use App\Framework\Sockets\SocketClient;
|
||||
use App\Framework\Sockets\ValueObjects\SocketAddress;
|
||||
use App\Framework\Sockets\ValueObjects\SocketResource;
|
||||
|
||||
$client = new SocketClient();
|
||||
$address = SocketAddress::ipv4('127.0.0.1', 8080);
|
||||
|
||||
// Verbindung herstellen
|
||||
$client->connect($socket, $address);
|
||||
|
||||
// Daten lesen
|
||||
$data = $client->read($socket, 1024); // null wenn keine Daten (non-blocking)
|
||||
|
||||
// Daten schreiben
|
||||
$bytesWritten = $client->write($socket, "Hello, World!");
|
||||
|
||||
// Daten senden (mit Flags)
|
||||
$bytesSent = $client->send($socket, $data, MSG_DONTWAIT);
|
||||
|
||||
// Daten empfangen
|
||||
$data = $client->receive($socket, 1024, MSG_DONTWAIT);
|
||||
|
||||
// Verbindung schließen
|
||||
$client->close($socket);
|
||||
```
|
||||
|
||||
### SocketConnection
|
||||
|
||||
Wrapper für Socket-Verbindungen mit Metadaten.
|
||||
|
||||
```php
|
||||
use App\Framework\Sockets\SocketConnection;
|
||||
|
||||
$connection = SocketConnection::create($socketResource, $address);
|
||||
|
||||
// Eigenschaften
|
||||
$socket = $connection->getSocket();
|
||||
$address = $connection->getAddress();
|
||||
$connectionId = $connection->getConnectionId();
|
||||
$isClosed = $connection->isClosed();
|
||||
|
||||
// Verbindung schließen
|
||||
$connection->close();
|
||||
```
|
||||
|
||||
### SocketConnectionPool
|
||||
|
||||
Verwaltung mehrerer Socket-Verbindungen.
|
||||
|
||||
```php
|
||||
use App\Framework\Sockets\SocketConnectionPool;
|
||||
|
||||
$pool = new SocketConnectionPool(
|
||||
maxConnectionsPerAddress: 5,
|
||||
connectionTimeoutSeconds: 300
|
||||
);
|
||||
|
||||
// Verbindung hinzufügen
|
||||
$pool->add($connection);
|
||||
|
||||
// Verbindung entfernen
|
||||
$pool->remove($connection);
|
||||
|
||||
// Verbindung abrufen
|
||||
$connection = $pool->get($connectionId);
|
||||
$connections = $pool->getConnectionsByAddress($address);
|
||||
$connection = $pool->getConnectionForAddress($address);
|
||||
|
||||
// Dead Connections bereinigen
|
||||
$removed = $pool->cleanupDeadConnections();
|
||||
|
||||
// Statistiken
|
||||
$count = $pool->getConnectionCount();
|
||||
$allConnections = $pool->getAllConnections();
|
||||
```
|
||||
|
||||
### SocketService
|
||||
|
||||
Haupt-Facade für alle Socket-Operationen.
|
||||
|
||||
```php
|
||||
use App\Framework\Sockets\SocketService;
|
||||
|
||||
$socketService = $container->get(SocketService::class);
|
||||
|
||||
// Socket-Erstellung
|
||||
$socket = $socketService->createTcp($address);
|
||||
$socket = $socketService->createUdp($address);
|
||||
$socket = $socketService->createUnix($address);
|
||||
|
||||
// Server-Operationen
|
||||
$socketService->bind($socket, $address);
|
||||
$socketService->listen($socket, 10);
|
||||
$connection = $socketService->accept($socket);
|
||||
$socketService->setNonBlocking($socket);
|
||||
|
||||
// Client-Operationen
|
||||
$socketService->connect($socket, $address);
|
||||
$data = $socketService->read($socket, 1024);
|
||||
$bytes = $socketService->write($socket, $data);
|
||||
|
||||
// Connection Pool
|
||||
$socketService->addConnection($connection);
|
||||
$socketService->removeConnection($connection);
|
||||
$connection = $socketService->getConnection($connectionId);
|
||||
$removed = $socketService->cleanupDeadConnections();
|
||||
```
|
||||
|
||||
## Integration
|
||||
|
||||
### PCNTL Integration
|
||||
|
||||
Socket-Server in geforkten Worker-Prozessen für Load Balancing.
|
||||
|
||||
```php
|
||||
use App\Framework\Sockets\Integration\PcntlSocketServer;
|
||||
use App\Framework\Sockets\SocketService;
|
||||
use App\Framework\Pcntl\PcntlService;
|
||||
use App\Framework\Sockets\ValueObjects\SocketAddress;
|
||||
use App\Framework\Sockets\ValueObjects\SocketResource;
|
||||
|
||||
$socketService = $container->get(SocketService::class);
|
||||
$pcntlService = $container->get(PcntlService::class);
|
||||
|
||||
$pcntlServer = new PcntlSocketServer(
|
||||
socketService: $socketService,
|
||||
pcntlService: $pcntlService,
|
||||
maxWorkers: 4
|
||||
);
|
||||
|
||||
// Connection Handler setzen
|
||||
$pcntlServer->setConnectionHandler(function (SocketConnection $connection) {
|
||||
// Verbindung verarbeiten
|
||||
$socket = $connection->getSocket();
|
||||
$data = $socketService->read($socket->getResource(), 1024);
|
||||
// ...
|
||||
});
|
||||
|
||||
// Server starten
|
||||
$address = SocketAddress::ipv4('0.0.0.0', 8080);
|
||||
$socket = $socketService->createTcp($address);
|
||||
$pcntlServer->start($socket, $address, workerCount: 4);
|
||||
|
||||
// Server stoppen (graceful shutdown)
|
||||
$pcntlServer->stop();
|
||||
```
|
||||
|
||||
### Async Integration
|
||||
|
||||
Non-blocking Socket-Server mit Fibers für parallele Connection-Verarbeitung.
|
||||
|
||||
```php
|
||||
use App\Framework\Sockets\Integration\AsyncSocketServer;
|
||||
use App\Framework\Sockets\SocketService;
|
||||
use App\Framework\Async\FiberManager;
|
||||
use App\Framework\Sockets\ValueObjects\SocketAddress;
|
||||
|
||||
$socketService = $container->get(SocketService::class);
|
||||
$fiberManager = $container->get(FiberManager::class);
|
||||
|
||||
$asyncServer = new AsyncSocketServer(
|
||||
socketService: $socketService,
|
||||
fiberManager: $fiberManager
|
||||
);
|
||||
|
||||
// Server starten (gibt Fiber zurück)
|
||||
$address = SocketAddress::ipv4('0.0.0.0', 8080);
|
||||
$socket = $socketService->createTcp($address);
|
||||
|
||||
$serverFiber = $asyncServer->startAsync(
|
||||
socket: $socket,
|
||||
address: $address,
|
||||
connectionHandler: function (SocketConnection $connection) {
|
||||
// Connection in separatem Fiber verarbeiten
|
||||
$socket = $connection->getSocket();
|
||||
|
||||
// Non-blocking lesen
|
||||
$readFiber = $asyncServer->readAsync($connection, 1024);
|
||||
$data = $readFiber->start();
|
||||
|
||||
// Non-blocking schreiben
|
||||
$writeFiber = $asyncServer->writeAsync($connection, "Response");
|
||||
$bytes = $writeFiber->start();
|
||||
}
|
||||
);
|
||||
|
||||
// Server-Fiber starten
|
||||
$serverFiber->start();
|
||||
```
|
||||
|
||||
## Beispiele
|
||||
|
||||
### Einfacher TCP Server
|
||||
|
||||
```php
|
||||
use App\Framework\Sockets\SocketService;
|
||||
use App\Framework\Sockets\ValueObjects\SocketAddress;
|
||||
|
||||
$socketService = $container->get(SocketService::class);
|
||||
$address = SocketAddress::ipv4('0.0.0.0', 8080);
|
||||
|
||||
// Socket erstellen
|
||||
$socket = $socketService->createTcp($address);
|
||||
|
||||
// Binden und lauschen
|
||||
$socketService->bind($socket, $address);
|
||||
$socketService->listen($socket, 10);
|
||||
$socketService->setNonBlocking($socket);
|
||||
|
||||
// Hauptschleife
|
||||
while (true) {
|
||||
// Verbindung akzeptieren
|
||||
$connection = $socketService->accept($socket);
|
||||
|
||||
if ($connection !== null) {
|
||||
$clientSocket = $connection->getSocket();
|
||||
|
||||
// Daten lesen
|
||||
$data = $socketService->read($clientSocket, 1024);
|
||||
|
||||
if ($data !== null) {
|
||||
// Antwort senden
|
||||
$socketService->write($clientSocket, "Echo: " . $data);
|
||||
}
|
||||
|
||||
// Verbindung schließen
|
||||
$connection->close();
|
||||
}
|
||||
|
||||
usleep(10000); // 10ms
|
||||
}
|
||||
```
|
||||
|
||||
### TCP Client
|
||||
|
||||
```php
|
||||
use App\Framework\Sockets\SocketService;
|
||||
use App\Framework\Sockets\ValueObjects\SocketAddress;
|
||||
|
||||
$socketService = $container->get(SocketService::class);
|
||||
$address = SocketAddress::ipv4('127.0.0.1', 8080);
|
||||
|
||||
// Client-Socket erstellen
|
||||
$socket = $socketService->createClient(
|
||||
SocketType::TCP,
|
||||
SocketProtocol::IPv4
|
||||
);
|
||||
|
||||
// Verbinden
|
||||
$socketService->connect($socket, $address);
|
||||
|
||||
// Daten senden
|
||||
$socketService->write($socket, "Hello, Server!");
|
||||
|
||||
// Antwort lesen
|
||||
$response = $socketService->read($socket, 1024);
|
||||
|
||||
// Verbindung schließen
|
||||
$socketService->close($socket);
|
||||
```
|
||||
|
||||
### Unix Domain Socket Server
|
||||
|
||||
```php
|
||||
use App\Framework\Sockets\SocketService;
|
||||
use App\Framework\Sockets\ValueObjects\SocketAddress;
|
||||
|
||||
$socketService = $container->get(SocketService::class);
|
||||
$address = SocketAddress::unix('/tmp/mysocket.sock');
|
||||
|
||||
// Socket erstellen
|
||||
$socket = $socketService->createUnix($address);
|
||||
|
||||
// Binden und lauschen
|
||||
$socketService->bind($socket, $address);
|
||||
$socketService->listen($socket, 10);
|
||||
|
||||
// Verbindungen akzeptieren
|
||||
while (true) {
|
||||
$connection = $socketService->accept($socket);
|
||||
|
||||
if ($connection !== null) {
|
||||
// Verarbeitung
|
||||
$socket = $connection->getSocket();
|
||||
$data = $socketService->read($socket, 1024);
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Migration von bestehendem Code
|
||||
|
||||
### Von `socket_create()` zu `SocketFactory`
|
||||
|
||||
**Vorher:**
|
||||
```php
|
||||
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
|
||||
```
|
||||
|
||||
**Nachher:**
|
||||
```php
|
||||
$address = SocketAddress::ipv4('127.0.0.1', 8080);
|
||||
$socket = $socketService->createTcp($address);
|
||||
```
|
||||
|
||||
### Von `socket_bind()`/`socket_listen()` zu `SocketServer`
|
||||
|
||||
**Vorher:**
|
||||
```php
|
||||
socket_bind($socket, '127.0.0.1', 8080);
|
||||
socket_listen($socket, 10);
|
||||
```
|
||||
|
||||
**Nachher:**
|
||||
```php
|
||||
$address = SocketAddress::ipv4('127.0.0.1', 8080);
|
||||
$socketService->bind($socket, $address);
|
||||
$socketService->listen($socket, 10);
|
||||
```
|
||||
|
||||
### Von `socket_accept()` zu `SocketServer::accept()`
|
||||
|
||||
**Vorher:**
|
||||
```php
|
||||
$clientSocket = socket_accept($socket);
|
||||
```
|
||||
|
||||
**Nachher:**
|
||||
```php
|
||||
$connection = $socketService->accept($socket);
|
||||
if ($connection !== null) {
|
||||
$clientSocket = $connection->getSocket();
|
||||
}
|
||||
```
|
||||
|
||||
### Von `socket_select()` zu `SocketServer::select()`
|
||||
|
||||
**Vorher:**
|
||||
```php
|
||||
socket_select($read, $write, $except, 1);
|
||||
```
|
||||
|
||||
**Nachher:**
|
||||
```php
|
||||
$numReady = $socketService->select($read, $write, $except, 1);
|
||||
```
|
||||
|
||||
### Von `socket_read()`/`socket_write()` zu `SocketClient`
|
||||
|
||||
**Vorher:**
|
||||
```php
|
||||
$data = socket_read($socket, 1024);
|
||||
socket_write($socket, $data, strlen($data));
|
||||
```
|
||||
|
||||
**Nachher:**
|
||||
```php
|
||||
$data = $socketService->read($socket, 1024);
|
||||
$bytes = $socketService->write($socket, $data);
|
||||
```
|
||||
|
||||
### Von `socket_close()` zu `SocketResource` Destructor
|
||||
|
||||
**Vorher:**
|
||||
```php
|
||||
socket_close($socket);
|
||||
```
|
||||
|
||||
**Nachher:**
|
||||
```php
|
||||
// Automatisch im Destructor, oder manuell:
|
||||
$socket->close();
|
||||
```
|
||||
|
||||
## Fehlerbehandlung
|
||||
|
||||
Das Sockets-Modul verwendet spezifische Exceptions für verschiedene Fehlertypen:
|
||||
|
||||
```php
|
||||
use App\Framework\Sockets\Exceptions\SocketException;
|
||||
use App\Framework\Sockets\Exceptions\SocketBindException;
|
||||
use App\Framework\Sockets\Exceptions\SocketConnectException;
|
||||
use App\Framework\Sockets\Exceptions\SocketReadException;
|
||||
|
||||
try {
|
||||
$socketService->bind($socket, $address);
|
||||
} catch (SocketBindException $e) {
|
||||
// Bind-Fehler behandeln
|
||||
error_log("Failed to bind: " . $e->getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
$socketService->connect($socket, $address);
|
||||
} catch (SocketConnectException $e) {
|
||||
// Connect-Fehler behandeln
|
||||
error_log("Failed to connect: " . $e->getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
$data = $socketService->read($socket, 1024);
|
||||
} catch (SocketReadException $e) {
|
||||
// Read-Fehler behandeln
|
||||
if ($e->getMessage() === 'Socket connection closed') {
|
||||
// Verbindung geschlossen
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **SocketService verwenden**: Nutze immer `SocketService` als Haupt-API statt direkter Aufrufe der einzelnen Services
|
||||
2. **Value Objects**: Verwende immer Value Objects (`SocketAddress`, `SocketType`, etc.) statt primitiver Typen
|
||||
3. **Resource Management**: `SocketResource` kümmert sich automatisch um Cleanup, aber manuelles Schließen ist für explizite Kontrolle möglich
|
||||
4. **Non-blocking I/O**: Verwende `setNonBlocking()` für Server-Sockets, um mehrere Verbindungen parallel zu handhaben
|
||||
5. **Connection Pooling**: Nutze `SocketConnectionPool` für Client-Verbindungen, die wiederverwendet werden sollen
|
||||
6. **Error Handling**: Fange spezifische Exceptions (`SocketBindException`, `SocketConnectException`, etc.) für präzise Fehlerbehandlung
|
||||
7. **PCNTL für Load Balancing**: Verwende `PcntlSocketServer` für Socket-Server, die mehrere Worker-Prozesse benötigen
|
||||
8. **Async für Parallelität**: Verwende `AsyncSocketServer` für non-blocking I/O mit Fibers
|
||||
|
||||
## Dependency Injection
|
||||
|
||||
Alle Services werden automatisch über `SocketsInitializer` im DI-Container registriert:
|
||||
|
||||
```php
|
||||
use App\Framework\Sockets\SocketService;
|
||||
use App\Framework\Sockets\SocketFactory;
|
||||
use App\Framework\Sockets\SocketServer;
|
||||
use App\Framework\Sockets\SocketClient;
|
||||
|
||||
// Services sind im Container verfügbar
|
||||
$socketService = $container->get(SocketService::class);
|
||||
$factory = $container->get(SocketFactory::class);
|
||||
$server = $container->get(SocketServer::class);
|
||||
$client = $container->get(SocketClient::class);
|
||||
```
|
||||
|
||||
## Framework-Kompatibilität
|
||||
|
||||
Das Sockets-Modul folgt allen Framework-Prinzipien:
|
||||
|
||||
- ✅ **Final readonly classes** wo möglich
|
||||
- ✅ **Value Objects** statt Primitiven
|
||||
- ✅ **Dependency Injection** überall
|
||||
- ✅ **Composition** statt Inheritance
|
||||
- ✅ **Strict Types** (`declare(strict_types=1)`)
|
||||
- ✅ **PSR-12** Code Style
|
||||
|
||||
## Siehe auch
|
||||
|
||||
- [PCNTL Module Documentation](./pcntl-module.md) - Für PCNTL-Integration
|
||||
- [Async Module Documentation](./async-module.md) - Für Async-Integration
|
||||
- [Framework Guidelines](./guidelines.md) - Allgemeine Framework-Prinzipien
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
1141
docs/claude/typed-string-system.md
Normal file
1141
docs/claude/typed-string-system.md
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user