fix: DockerSecretsResolver - don't normalize absolute paths like /var/www/html/...
Some checks failed
Deploy Application / deploy (push) Has been cancelled

This commit is contained in:
2025-11-24 21:28:25 +01:00
parent 4eb7134853
commit 77abc65cd7
1327 changed files with 91915 additions and 9909 deletions

View File

@@ -772,10 +772,15 @@ interface Component {
### Data Attributes
> **Note**: All data attributes are centrally managed through PHP Enums and JavaScript Constants. See [Data Attributes Reference](../../framework/data-attributes-reference.md) for complete documentation.
#### `data-component-id`
Identifies component root element. **Required** on component container.
**PHP Enum**: `LiveComponentCoreAttribute::COMPONENT_ID`
**JavaScript Constant**: `LiveComponentCoreAttributes.COMPONENT_ID`
```html
<div data-component-id="{component_id}" data-component-name="SearchComponent">
<!-- Component content -->
@@ -788,6 +793,9 @@ Identifies component root element. **Required** on component container.
Two-way data binding for inputs.
**PHP Enum**: `LiveComponentFeatureAttribute::LC_MODEL`
**JavaScript Constant**: `LiveComponentFeatureAttributes.LC_MODEL`
```html
<input
type="text"
@@ -817,6 +825,9 @@ Two-way data binding for inputs.
Trigger action on click.
**PHP Enum**: `LiveComponentCoreAttribute::LIVE_ACTION`
**JavaScript Constant**: `LiveComponentCoreAttributes.LIVE_ACTION`
```html
<button data-lc-action="search">Search</button>

View File

@@ -0,0 +1,877 @@
# LiveComponents End-to-End Guide
**Complete guide to building LiveComponents from scratch to production.**
This guide walks you through creating, testing, and deploying LiveComponents with all features.
---
## Table of Contents
1. [Quick Start (5 Minutes)](#quick-start-5-minutes)
2. [Component Basics](#component-basics)
3. [State Management](#state-management)
4. [Actions & Events](#actions--events)
5. [Advanced Features](#advanced-features)
6. [Performance Optimization](#performance-optimization)
7. [Real-World Examples](#real-world-examples)
---
## Quick Start (5 Minutes)
### Step 1: Create Component Class
```php
<?php
declare(strict_types=1);
namespace App\Application\LiveComponents\Counter;
use App\Framework\LiveComponents\Attributes\Action;
use App\Framework\LiveComponents\Attributes\LiveComponent;
use App\Framework\LiveComponents\Contracts\LiveComponentContract;
use App\Framework\LiveComponents\ValueObjects\ComponentId;
use App\Framework\LiveComponents\ValueObjects\ComponentRenderData;
#[LiveComponent('counter')]
final readonly class CounterComponent implements LiveComponentContract
{
public function __construct(
public ComponentId $id,
public CounterState $state
) {
}
public function getRenderData(): ComponentRenderData
{
return new ComponentRenderData(
templatePath: 'livecomponent-counter',
data: [
'componentId' => $this->id->toString(),
'count' => $this->state->count,
]
);
}
#[Action]
public function increment(): CounterState
{
return $this->state->increment();
}
}
```
### Step 2: Create State Value Object
```php
<?php
declare(strict_types=1);
namespace App\Application\LiveComponents\Counter;
use App\Framework\LiveComponents\ValueObjects\ComponentState;
final readonly class CounterState extends ComponentState
{
public function __construct(
public int $count = 0
) {
}
public function increment(): self
{
return new self(count: $this->count + 1);
}
public static function empty(): self
{
return new self();
}
public static function fromArray(array $data): self
{
return new self(
count: $data['count'] ?? 0
);
}
public function toArray(): array
{
return [
'count' => $this->count,
];
}
}
```
### Step 3: Create Template
**File**: `resources/templates/livecomponent-counter.view.php`
```html
<div data-component-id="{componentId}">
<h2>Counter: {count}</h2>
<button data-live-action="increment">Increment</button>
</div>
```
### Step 4: Use in Template
```html
<x-counter id="demo" />
```
**That's it!** Your component is ready to use.
---
## Component Basics
### Component Structure
Every LiveComponent consists of:
1. **Component Class** - Implements `LiveComponentContract`
2. **State Value Object** - Extends `ComponentState`
3. **Template** - HTML template with placeholders
4. **Actions** - Methods marked with `#[Action]`
### Component ID
Components are identified by a `ComponentId` consisting of:
- **Name**: Component name from `#[LiveComponent]` attribute
- **Instance ID**: Unique identifier (e.g., 'demo', 'user-123')
```php
ComponentId::create('counter', 'demo') // counter:demo
ComponentId::create('user-profile', '123') // user-profile:123
```
### RenderData
`getRenderData()` returns template path and data:
```php
public function getRenderData(): ComponentRenderData
{
return new ComponentRenderData(
templatePath: 'livecomponent-counter',
data: [
'componentId' => $this->id->toString(),
'count' => $this->state->count,
'lastUpdate' => $this->state->lastUpdate,
]
);
}
```
---
## State Management
### State Value Objects
State is managed through immutable Value Objects:
```php
final readonly class CounterState extends ComponentState
{
public function __construct(
public int $count = 0,
public ?string $lastUpdate = null
) {
}
// Factory methods
public static function empty(): self
{
return new self();
}
public static function fromArray(array $data): self
{
return new self(
count: $data['count'] ?? 0,
lastUpdate: $data['lastUpdate'] ?? null
);
}
// Transformation methods (return new instance)
public function increment(): self
{
return new self(
count: $this->count + 1,
lastUpdate: date('H:i:s')
);
}
public function reset(): self
{
return new self(
count: 0,
lastUpdate: date('H:i:s')
);
}
// Serialization
public function toArray(): array
{
return [
'count' => $this->count,
'lastUpdate' => $this->lastUpdate,
];
}
}
```
### State Updates
Actions return new State instances:
```php
#[Action]
public function increment(): CounterState
{
return $this->state->increment();
}
#[Action]
public function addAmount(int $amount): CounterState
{
return $this->state->addAmount($amount);
}
```
---
## Actions & Events
### Actions
Actions are public methods marked with `#[Action]`:
```php
#[Action]
public function increment(): CounterState
{
return $this->state->increment();
}
#[Action(rateLimit: 10, idempotencyTTL: 60)]
public function expensiveOperation(): CounterState
{
// Rate limited to 10 calls per minute
// Idempotent for 60 seconds
return $this->state->performExpensiveOperation();
}
```
### Action Parameters
Actions can accept parameters:
```php
#[Action]
public function addAmount(int $amount): CounterState
{
return $this->state->addAmount($amount);
}
```
**Client-side**:
```html
<button data-live-action="addAmount" data-amount="5">
Add 5
</button>
```
### Events
Dispatch events from actions:
```php
#[Action]
public function increment(?ComponentEventDispatcher $events = null): CounterState
{
$newState = $this->state->increment();
$events?->dispatch('counter:changed', EventPayload::fromArray([
'old_value' => $this->state->count,
'new_value' => $newState->count,
]));
return $newState;
}
```
**Client-side**:
```javascript
document.addEventListener('livecomponent:event', (e) => {
if (e.detail.name === 'counter:changed') {
console.log('Counter changed:', e.detail.data);
}
});
```
---
## Advanced Features
### Caching
Implement `Cacheable` interface:
```php
#[LiveComponent('user-stats')]
final readonly class UserStatsComponent implements LiveComponentContract, Cacheable
{
public function getCacheKey(): string
{
return 'user-stats:' . $this->state->userId;
}
public function getCacheTTL(): Duration
{
return Duration::fromMinutes(5);
}
public function shouldCache(): bool
{
return true;
}
public function getCacheTags(): array
{
return ['user-stats', 'user:' . $this->state->userId];
}
public function getVaryBy(): ?VaryBy
{
return VaryBy::userId();
}
public function getStaleWhileRevalidate(): ?Duration
{
return Duration::fromHours(1);
}
}
```
### Polling
Implement `Pollable` interface:
```php
#[LiveComponent('live-feed')]
final readonly class LiveFeedComponent implements LiveComponentContract, Pollable
{
public function poll(): LiveComponentState
{
// Fetch latest data
$newItems = $this->feedService->getLatest();
return $this->state->withItems($newItems);
}
public function getPollInterval(): int
{
return 5000; // Poll every 5 seconds
}
}
```
### File Uploads
Implement `SupportsFileUpload` interface:
```php
#[LiveComponent('image-uploader')]
final readonly class ImageUploaderComponent implements LiveComponentContract, SupportsFileUpload
{
public function handleUpload(
UploadedFile $file,
?ComponentEventDispatcher $events = null
): LiveComponentState {
// Process upload
$path = $this->storage->store($file);
$events?->dispatch('file:uploaded', EventPayload::fromArray([
'path' => $path,
'size' => $file->getSize(),
]));
return $this->state->withUploadedFile($path);
}
public function validateUpload(UploadedFile $file): array
{
$errors = [];
if (!in_array($file->getMimeType(), ['image/jpeg', 'image/png'])) {
$errors[] = 'Only JPEG and PNG images are allowed';
}
if ($file->getSize() > 5 * 1024 * 1024) {
$errors[] = 'File size must be less than 5MB';
}
return $errors;
}
public function getAllowedMimeTypes(): array
{
return ['image/jpeg', 'image/png'];
}
public function getMaxFileSize(): int
{
return 5 * 1024 * 1024; // 5MB
}
}
```
### Fragments
Use fragments for partial updates:
**Template**:
```html
<div data-lc-fragment="counter-display">
<h2>Count: {count}</h2>
</div>
<div data-lc-fragment="actions">
<button data-live-action="increment" data-lc-fragments="counter-display">
Increment
</button>
</div>
```
**Client-side**: Only `counter-display` fragment is updated, not the entire component.
### Slots
Implement `SupportsSlots` for flexible component composition:
```php
#[LiveComponent('card')]
final readonly class CardComponent implements LiveComponentContract, SupportsSlots
{
public function getSlotDefinitions(): array
{
return [
SlotDefinition::named('header'),
SlotDefinition::default('<p>Default content</p>'),
SlotDefinition::named('footer'),
];
}
public function getSlotContext(string $slotName): SlotContext
{
return SlotContext::create([
'card_title' => $this->state->title,
]);
}
public function processSlotContent(SlotContent $content): SlotContent
{
return $content; // No processing needed
}
public function validateSlots(array $providedSlots): array
{
return []; // No validation errors
}
}
```
**Usage**:
```html
<x-card id="demo" title="My Card">
<slot name="header">
<h1>Custom Header</h1>
</slot>
<slot>
<p>Main content</p>
</slot>
</x-card>
```
### Islands
Use `#[Island]` for isolated rendering:
```php
#[LiveComponent('heavy-widget')]
#[Island(isolated: true, lazy: true, placeholder: 'Loading widget...')]
final readonly class HeavyWidgetComponent implements LiveComponentContract
{
// Component implementation
}
```
---
## Performance Optimization
### Caching Strategies
**Global Cache**:
```php
public function getVaryBy(): ?VaryBy
{
return VaryBy::none(); // Same for all users
}
```
**Per-User Cache**:
```php
public function getVaryBy(): ?VaryBy
{
return VaryBy::userId(); // Different cache per user
}
```
**Per-User Per-Locale Cache**:
```php
public function getVaryBy(): ?VaryBy
{
return VaryBy::userAndLocale(); // Different cache per user and locale
}
```
**With Feature Flags**:
```php
public function getVaryBy(): ?VaryBy
{
return VaryBy::userId()
->withFeatureFlags(['new-ui', 'dark-mode']);
}
```
### Stale-While-Revalidate
Serve stale content while refreshing:
```php
public function getCacheTTL(): Duration
{
return Duration::fromMinutes(5); // Fresh for 5 minutes
}
public function getStaleWhileRevalidate(): ?Duration
{
return Duration::fromHours(1); // Serve stale for 1 hour while refreshing
}
```
### Request Batching
Batch multiple operations:
```javascript
// Client-side
const response = await LiveComponent.executeBatch([
{
componentId: 'counter:demo',
method: 'increment',
params: { amount: 5 },
fragments: ['counter-display']
},
{
componentId: 'stats:user-123',
method: 'refresh'
}
]);
```
### Lazy Loading
Load components when entering viewport:
```php
// In template
{{ lazy_component('notification-center:user-123', [
'priority' => 'high',
'threshold' => '0.1',
'placeholder' => 'Loading notifications...'
]) }}
```
---
## Real-World Examples
### Example 1: User Profile Component
```php
#[LiveComponent('user-profile')]
#[Island(isolated: true, lazy: true)]
final readonly class UserProfileComponent implements LiveComponentContract, Cacheable
{
public function __construct(
public ComponentId $id,
public UserProfileState $state
) {
}
public function getRenderData(): ComponentRenderData
{
return new ComponentRenderData(
templatePath: 'livecomponent-user-profile',
data: [
'componentId' => $this->id->toString(),
'user' => $this->state->user,
'stats' => $this->state->stats,
]
);
}
#[Action]
public function updateProfile(array $data, ?ComponentEventDispatcher $events = null): UserProfileState
{
$updatedUser = $this->userService->update($this->state->user->id, $data);
$events?->dispatch('profile:updated', EventPayload::fromArray([
'user_id' => $updatedUser->id,
]));
return $this->state->withUser($updatedUser);
}
// Cacheable implementation
public function getCacheKey(): string
{
return 'user-profile:' . $this->state->user->id;
}
public function getCacheTTL(): Duration
{
return Duration::fromMinutes(10);
}
public function shouldCache(): bool
{
return true;
}
public function getCacheTags(): array
{
return ['user-profile', 'user:' . $this->state->user->id];
}
public function getVaryBy(): ?VaryBy
{
return VaryBy::userId();
}
public function getStaleWhileRevalidate(): ?Duration
{
return null; // No SWR for user profiles
}
}
```
### Example 2: Search Component with Debouncing
```php
#[LiveComponent('search')]
final readonly class SearchComponent implements LiveComponentContract
{
#[Action(rateLimit: 20)] // Allow 20 searches per minute
public function search(string $query, ?ComponentEventDispatcher $events = null): SearchState
{
if (strlen($query) < 3) {
return $this->state->withResults([]);
}
$results = $this->searchService->search($query);
$events?->dispatch('search:completed', EventPayload::fromArray([
'query' => $query,
'result_count' => count($results),
]));
return $this->state->withQuery($query)->withResults($results);
}
}
```
**Client-side debouncing**:
```javascript
let searchTimeout;
const searchInput = document.querySelector('[data-live-action="search"]');
searchInput.addEventListener('input', (e) => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
LiveComponent.executeAction('search:demo', 'search', {
query: e.target.value
});
}, 300); // 300ms debounce
});
```
### Example 3: Dashboard with Multiple Components
```php
// Dashboard page template
<div class="dashboard">
<!-- Heavy widget - lazy loaded -->
<x-analytics-dashboard id="main" />
<!-- User stats - cached -->
<x-user-stats id="current-user" />
<!-- Live feed - polled -->
<x-live-feed id="feed" />
</div>
```
---
## Testing
### Using LiveComponentTestCase
```php
class CounterComponentTest extends LiveComponentTestCase
{
public function test_increments_counter(): void
{
$this->mount('counter:test', ['count' => 5])
->call('increment')
->seeStateKey('count', 6)
->seeHtmlHas('Count: 6');
}
public function test_resets_counter(): void
{
$this->mount('counter:test', ['count' => 10])
->call('reset')
->seeStateKey('count', 0)
->seeHtmlHas('Count: 0');
}
public function test_dispatches_event_on_increment(): void
{
$this->mount('counter:test', ['count' => 5])
->call('increment')
->seeEventDispatched('counter:changed', [
'old_value' => 5,
'new_value' => 6,
]);
}
}
```
### Snapshot Testing
```php
public function test_renders_counter_correctly(): void
{
$component = mountComponent('counter:test', ['count' => 5]);
expect($component['html'])->toMatchSnapshot('counter-initial-state');
}
```
---
## Best Practices
### 1. Immutable State
Always return new State instances:
```php
// ✅ CORRECT
public function increment(): CounterState
{
return new CounterState(count: $this->state->count + 1);
}
// ❌ WRONG (won't work with readonly)
public function increment(): void
{
$this->state->count++; // Error: Cannot modify readonly property
}
```
### 2. Type-Safe State
Use Value Objects instead of arrays:
```php
// ✅ CORRECT
public function __construct(
public CounterState $state
) {}
// ❌ WRONG
public function __construct(
public array $state
) {}
```
### 3. Action Validation
Validate inputs in actions:
```php
#[Action]
public function addAmount(int $amount): CounterState
{
if ($amount < 0) {
throw new \InvalidArgumentException('Amount must be positive');
}
return $this->state->addAmount($amount);
}
```
### 4. Error Handling
Handle errors gracefully:
```php
#[Action]
public function performOperation(): CounterState
{
try {
return $this->state->performOperation();
} catch (\Exception $e) {
// Log error
error_log("Operation failed: " . $e->getMessage());
// Return unchanged state or error state
return $this->state->withError($e->getMessage());
}
}
```
### 5. Performance
- Use caching for expensive operations
- Use fragments for partial updates
- Use lazy loading for below-the-fold content
- Use Islands for heavy components
- Batch multiple operations
---
## Next Steps
- [Security Guide](security-guide-complete.md) - CSRF, Rate Limiting, Authorization
- [Performance Guide](performance-guide-complete.md) - Caching, Batching, Optimization
- [Upload Guide](upload-guide-complete.md) - File Uploads, Validation, Chunking
- [API Reference](api-reference-complete.md) - Complete API documentation

View File

@@ -0,0 +1,521 @@
# Implementation Plan: Testing Infrastructure, Documentation & Middleware Support
## Übersicht
Dieser Plan umfasst drei große Bereiche:
1. **Test Infrastructure** - Verbesserte Test-Harness und Testing-Tools
2. **Documentation** - Vollständige Dokumentation für alle LiveComponents Features
3. **Middleware Support** - Component-Level Middleware für flexible Request/Response Transformation
---
## Phase 1: Test Infrastructure Verbesserung
### 1.1 LiveComponentTestCase Base Class
**Ziel**: Zentrale Test-Base-Class mit allen Helpers für einfache Component-Tests
**Datei**: `tests/Feature/Framework/LiveComponents/TestHarness/LiveComponentTestCase.php`
**Features**:
- `mount(string $componentId, array $initialData = [])` - Component mounten
- `call(string $action, array $params = [])` - Action aufrufen
- `seeHtmlHas(string $needle)` - HTML-Assertion
- `seeStateEquals(array $expectedState)` - State-Assertion
- `seeStateKey(string $key, mixed $value)` - Einzelne State-Key-Assertion
- `seeEventDispatched(string $eventName, ?array $payload = null)` - Event-Assertion
- `seeFragment(string $fragmentName, string $content)` - Fragment-Assertion
- `assertComponent(LiveComponentContract $component)` - Component-Instance-Assertion
**Architektur**:
```php
abstract class LiveComponentTestCase extends TestCase
{
protected ComponentRegistry $registry;
protected LiveComponentHandler $handler;
protected LiveComponentRenderer $renderer;
protected ?LiveComponentContract $currentComponent = null;
protected function setUp(): void
{
parent::setUp();
$this->registry = container()->get(ComponentRegistry::class);
$this->handler = container()->get(LiveComponentHandler::class);
$this->renderer = container()->get(LiveComponentRenderer::class);
}
protected function mount(string $componentId, array $initialData = []): self
{
$this->currentComponent = $this->registry->resolve(
ComponentId::fromString($componentId),
$initialData
);
return $this;
}
protected function call(string $action, array $params = []): self
{
// Action ausführen und Component aktualisieren
return $this;
}
protected function seeHtmlHas(string $needle): self
{
$html = $this->renderer->render($this->currentComponent);
expect($html)->toContain($needle);
return $this;
}
// ... weitere Helper-Methoden
}
```
### 1.2 Snapshot Testing
**Ziel**: Render-Ausgabe als Snapshots speichern und vergleichen
**Datei**: `tests/Feature/Framework/LiveComponents/TestHarness/ComponentSnapshotTest.php`
**Features**:
- Whitespace-normalisierte Snapshots
- Automatische Snapshot-Generierung
- Snapshot-Vergleich mit Diff-Output
- Snapshot-Update-Modus für Refactoring
**Usage**:
```php
it('renders counter component correctly', function () {
$component = mountComponent('counter:test', ['count' => 5]);
expect($component['html'])->toMatchSnapshot('counter-initial-state');
});
```
### 1.3 Contract Compliance Tests
**Ziel**: Automatische Tests für Interface-Compliance
**Datei**: `tests/Feature/Framework/LiveComponents/ContractComplianceTest.php`
**Tests**:
- `Pollable` Interface Compliance
- `Cacheable` Interface Compliance
- `Uploadable` Interface Compliance
- `LifecycleAware` Interface Compliance
- `SupportsSlots` Interface Compliance
**Features**:
- Reflection-basierte Interface-Checks
- Method-Signature-Validierung
- Return-Type-Validierung
- Required-Method-Checks
### 1.4 Security Tests
**Ziel**: Umfassende Security-Tests für alle Security-Features
**Datei**: `tests/Feature/Framework/LiveComponents/SecurityTest.php`
**Tests**:
- CSRF Protection Tests
- Rate Limiting Tests
- Idempotency Tests
- Action Allow-List Tests
- Authorization Tests (`#[RequiresPermission]`)
**Features**:
- Test für alle Security-Patterns
- Edge-Case-Tests (Token-Manipulation, etc.)
- Integration-Tests mit echten Requests
### 1.5 E2E Tests
**Ziel**: End-to-End Tests für komplexe Szenarien
**Datei**: `tests/e2e/livecomponents/`
**Tests**:
- Partial Rendering E2E
- Batch Operations E2E
- SSE Reconnect E2E
- Upload Chunking E2E
- Island Loading E2E
- Lazy Loading E2E
**Tools**:
- Playwright für Browser-Tests
- Real HTTP Requests
- Component-Interaction-Tests
---
## Phase 2: Documentation Vervollständigung
### 2.1 End-to-End Guide
**Datei**: `docs/livecomponents/end-to-end-guide.md`
**Inhalte**:
- Component erstellen (von Grund auf)
- RenderData definieren
- Actions implementieren
- Events dispatchen
- State Management
- Caching konfigurieren
- Fragments verwenden
- Slots nutzen
- Lifecycle Hooks
- Best Practices
**Struktur**:
1. Quick Start (5 Minuten)
2. Component Basics
3. State Management
4. Actions & Events
5. Advanced Features
6. Performance Optimization
7. Real-World Examples
### 2.2 Security Guide
**Datei**: `docs/livecomponents/security-guide-complete.md`
**Inhalte**:
- CSRF Protection (wie es funktioniert, Best Practices)
- Rate Limiting (Konfiguration, Custom Limits)
- Idempotency (Use Cases, Implementation)
- Action Allow-List (Security-Pattern)
- Authorization (`#[RequiresPermission]`)
- Input Validation
- XSS Prevention
- Secure State Management
**Beispiele**:
- Secure Component Patterns
- Common Security Pitfalls
- Security Checklist
### 2.3 Performance Guide
**Datei**: `docs/livecomponents/performance-guide-complete.md`
**Inhalte**:
- Caching Strategies (varyBy, SWR)
- Request Batching
- Debounce/Throttle Patterns
- Lazy Loading Best Practices
- Island Components für Performance
- Fragment Updates
- SSE Optimization
- Memory Management
**Beispiele**:
- Performance-Optimierte Components
- Benchmarking-Strategien
- Performance Checklist
### 2.4 Upload Guide
**Datei**: `docs/livecomponents/upload-guide-complete.md`
**Inhalte**:
- File Upload Basics
- Validation Patterns
- Chunked Uploads
- Progress Tracking
- Quarantine System
- Virus Scanning Integration
- Error Handling
- Security Considerations
**Beispiele**:
- Image Upload Component
- Large File Upload
- Multi-File Upload
### 2.5 DevTools/Debugging Guide
**Datei**: `docs/livecomponents/devtools-debugging-guide.md`
**Inhalte**:
- DevTools Overlay verwenden
- Component Inspector
- Action Logging
- Event Monitoring
- Network Timeline
- Performance Profiling
- Debugging-Tipps
- Common Issues & Solutions
### 2.6 API Reference
**Datei**: `docs/livecomponents/api-reference-complete.md`
**Inhalte**:
- Alle Contracts (LiveComponentContract, Pollable, Cacheable, etc.)
- Alle Attributes (`#[LiveComponent]`, `#[Action]`, `#[Island]`, etc.)
- Alle Controller-Endpoints
- Alle Value Objects
- JavaScript API
- Events API
**Format**:
- Vollständige Method-Signatures
- Parameter-Beschreibungen
- Return-Types
- Examples für jede Methode
---
## Phase 3: Middleware Support
### 3.1 Component Middleware Interface
**Ziel**: Interface für Component-Level Middleware
**Datei**: `src/Framework/LiveComponents/Middleware/ComponentMiddlewareInterface.php`
**Interface**:
```php
interface ComponentMiddlewareInterface
{
public function handle(
LiveComponentContract $component,
string $action,
ActionParameters $params,
callable $next
): ComponentUpdate;
}
```
### 3.2 Middleware Attribute
**Ziel**: Attribute zum Registrieren von Middleware auf Component-Level
**Datei**: `src/Framework/LiveComponents/Attributes/Middleware.php`
**Usage**:
```php
#[LiveComponent('user-profile')]
#[Middleware(LoggingMiddleware::class)]
#[Middleware(CachingMiddleware::class, priority: 10)]
final readonly class UserProfileComponent implements LiveComponentContract
{
}
```
### 3.3 Built-in Middleware
**Ziel**: Standard-Middleware für häufige Use Cases
**Middleware**:
1. **LoggingMiddleware** - Action-Logging
2. **CachingMiddleware** - Response-Caching
3. **RateLimitMiddleware** - Component-Level Rate Limiting
4. **ValidationMiddleware** - Input-Validation
5. **TransformMiddleware** - Request/Response Transformation
**Dateien**:
- `src/Framework/LiveComponents/Middleware/LoggingMiddleware.php`
- `src/Framework/LiveComponents/Middleware/CachingMiddleware.php`
- `src/Framework/LiveComponents/Middleware/RateLimitMiddleware.php`
- `src/Framework/LiveComponents/Middleware/ValidationMiddleware.php`
- `src/Framework/LiveComponents/Middleware/TransformMiddleware.php`
### 3.4 Middleware Pipeline
**Ziel**: Middleware-Pipeline für Component Actions
**Datei**: `src/Framework/LiveComponents/Middleware/ComponentMiddlewarePipeline.php`
**Features**:
- Middleware-Stack-Verwaltung
- Priority-basierte Ausführung
- Request/Response Transformation
- Error Handling in Middleware
**Architektur**:
```php
final readonly class ComponentMiddlewarePipeline
{
public function __construct(
private array $middlewares
) {}
public function process(
LiveComponentContract $component,
string $action,
ActionParameters $params
): ComponentUpdate {
// Execute middleware stack
}
}
```
### 3.5 Middleware Integration
**Ziel**: Integration in LiveComponentHandler
**Datei**: `src/Framework/LiveComponents/LiveComponentHandler.php`
**Änderungen**:
- Middleware-Discovery aus Component-Attributes
- Middleware-Pipeline vor Action-Execution
- Middleware-Configuration aus Metadata
### 3.6 Middleware Tests
**Ziel**: Umfassende Tests für Middleware-System
**Dateien**:
- `tests/Unit/Framework/LiveComponents/Middleware/ComponentMiddlewarePipelineTest.php`
- `tests/Unit/Framework/LiveComponents/Middleware/LoggingMiddlewareTest.php`
- `tests/Unit/Framework/LiveComponents/Middleware/CachingMiddlewareTest.php`
- `tests/Feature/Framework/LiveComponents/MiddlewareIntegrationTest.php`
---
## Implementierungsreihenfolge
### Sprint 1: Test Infrastructure (Woche 1-2)
1. LiveComponentTestCase Base Class
2. Snapshot Testing
3. Contract Compliance Tests
4. Security Tests (Basis)
### Sprint 2: Documentation Phase 1 (Woche 3)
1. End-to-End Guide
2. Security Guide
3. Performance Guide
### Sprint 3: Middleware Support (Woche 4-5)
1. ComponentMiddlewareInterface
2. Middleware Attribute
3. Middleware Pipeline
4. Built-in Middleware (Logging, Caching)
### Sprint 4: Documentation Phase 2 & Testing Completion (Woche 6)
1. Upload Guide
2. DevTools Guide
3. API Reference
4. E2E Tests
5. Security Tests (Vollständig)
---
## Akzeptanzkriterien
### Test Infrastructure
- [ ] LiveComponentTestCase mit allen Helpers implementiert
- [ ] Snapshot Testing funktional
- [ ] Contract Compliance Tests für alle Interfaces
- [ ] Security Tests für alle Security-Features
- [ ] E2E Tests für komplexe Szenarien
- [ ] Alle Tests dokumentiert mit Examples
### Documentation
- [ ] End-to-End Guide vollständig
- [ ] Security Guide vollständig
- [ ] Performance Guide vollständig
- [ ] Upload Guide vollständig
- [ ] DevTools Guide vollständig
- [ ] API Reference vollständig
- [ ] Alle Guides mit praktischen Examples
### Middleware Support
- [ ] ComponentMiddlewareInterface definiert
- [ ] Middleware Attribute implementiert
- [ ] Middleware Pipeline funktional
- [ ] Built-in Middleware implementiert (Logging, Caching, RateLimit)
- [ ] Integration in LiveComponentHandler
- [ ] Umfassende Tests
- [ ] Dokumentation mit Examples
---
## Technische Details
### Test Infrastructure
**Pest Integration**:
- Custom Expectations erweitern
- Helper-Functions in `tests/Pest.php`
- Test-Harness als Trait für Wiederverwendung
**Snapshot Format**:
- JSON-basiert für strukturierte Daten
- HTML-Snapshots mit Whitespace-Normalisierung
- Versionierte Snapshots für Breaking Changes
### Middleware Architecture
**Execution Order**:
1. Request Transformation (TransformMiddleware)
2. Validation (ValidationMiddleware)
3. Rate Limiting (RateLimitMiddleware)
4. Caching (CachingMiddleware)
5. Logging (LoggingMiddleware)
6. Action Execution
7. Response Transformation
**Priority System**:
- Higher priority = earlier execution
- Default priority: 100
- Built-in middleware: 50-150 range
### Documentation Structure
**Markdown Format**:
- Code-Beispiele mit Syntax-Highlighting
- TOC für Navigation
- Cross-References zwischen Guides
- Version-Info für Breaking Changes
---
## Erfolgsmetriken
### Test Infrastructure
- 90%+ Test Coverage für alle Core-Features
- Alle Security-Features getestet
- E2E Tests für kritische Flows
### Documentation
- Alle Guides vollständig
- Praktische Examples für alle Features
- Positive Developer Feedback
### Middleware Support
- Middleware-System funktional
- Built-in Middleware getestet
- Developer können Custom Middleware erstellen
---
## Risiken & Mitigation
### Test Infrastructure
- **Risiko**: Snapshot-Tests können bei Refactoring störend sein
- **Mitigation**: Update-Modus für einfaches Refactoring
### Documentation
- **Risiko**: Dokumentation kann schnell veralten
- **Mitigation**: Automatische Tests für Code-Examples, regelmäßige Reviews
### Middleware Support
- **Risiko**: Performance-Impact durch Middleware-Stack
- **Mitigation**: Caching, Lazy Loading, Performance-Tests
---
## Nächste Schritte
1. **Sprint Planning**: Detaillierte Task-Aufteilung
2. **Implementation Start**: Mit Test Infrastructure beginnen
3. **Documentation**: Parallel zur Implementation
4. **Middleware**: Nach Test Infrastructure

View File

@@ -0,0 +1,308 @@
# Island Directive
**Isolated Component Rendering** - Render heavy components separately for better performance.
## Overview
The `#[Island]` directive enables isolated rendering of LiveComponents, allowing them to be rendered separately from the main template flow. This is particularly useful for resource-intensive components that can slow down page rendering.
## Features
- **Isolated Rendering**: Components are rendered separately without template wrapper
- **Lazy Loading**: Optional lazy loading when component enters viewport
- **Independent Caching**: Islands can have their own caching strategies
- **Isolated Events**: Islands don't receive parent component events
## Basic Usage
### Simple Island Component
```php
use App\Framework\LiveComponents\Attributes\LiveComponent;
use App\Framework\LiveComponents\Attributes\Island;
#[LiveComponent('heavy-widget')]
#[Island]
final readonly class HeavyWidgetComponent implements LiveComponentContract
{
// Component implementation
}
```
### Lazy Island Component
```php
#[LiveComponent('metrics-dashboard')]
#[Island(isolated: true, lazy: true, placeholder: 'Loading dashboard...')]
final readonly class MetricsDashboardComponent implements LiveComponentContract
{
// Component implementation
}
```
## Configuration Options
### `isolated` (bool, default: `true`)
Whether the component should be rendered in isolation. When `true`, the component is rendered via the `/island` endpoint without template wrapper.
```php
#[Island(isolated: true)] // Isolated rendering (default)
#[Island(isolated: false)] // Normal rendering (not recommended)
```
### `lazy` (bool, default: `false`)
Whether the component should be lazy-loaded when entering the viewport. When `true`, a placeholder is generated and the component is loaded via IntersectionObserver.
```php
#[Island(lazy: true)] // Lazy load on viewport entry
#[Island(lazy: false)] // Load immediately (default)
```
### `placeholder` (string|null, default: `null`)
Custom placeholder text shown while the component is loading (only used when `lazy: true`).
```php
#[Island(lazy: true, placeholder: 'Loading widget...')]
```
## Template Usage
### In Templates
Island components are used the same way as regular LiveComponents:
```html
<x-heavy-widget id="user-123" />
```
When processed, lazy Islands generate a placeholder:
```html
<div data-island-component="true"
data-live-component-lazy="heavy-widget:user-123"
data-lazy-priority="normal"
data-lazy-threshold="0.1">
<div class="island-placeholder">Loading widget...</div>
</div>
```
## Island vs. Lazy Component
### Lazy Component (`data-live-component-lazy`)
- Loaded when entering viewport
- Part of normal template flow
- Shares state/events with parent components
- Uses `/lazy-load` endpoint
### Island Component (`data-island-component`)
- Rendered in isolation (separate request)
- No template wrapper (no layout/meta)
- Isolated event context (no parent events)
- Optional lazy loading on viewport entry
- Uses `/island` endpoint
## Performance Benefits
### Isolation
Islands reduce template processing overhead for heavy components by rendering them separately. This means:
- Heavy components don't block main page rendering
- Independent error handling (one island failure doesn't affect others)
- Separate caching strategies per island
### Lazy Loading
When combined with lazy loading, Islands provide:
- Reduced initial page load time
- Faster Time to Interactive (TTI)
- Better Core Web Vitals scores
### Example Performance Impact
```php
// Without Island: 500ms page load (heavy component blocks rendering)
// With Island: 200ms page load + 300ms island load (non-blocking)
```
## Use Cases
### Heavy Widgets
```php
#[LiveComponent('analytics-dashboard')]
#[Island(isolated: true, lazy: true, placeholder: 'Loading analytics...')]
final readonly class AnalyticsDashboardComponent implements LiveComponentContract
{
// Heavy component with complex data processing
}
```
### Third-Party Integrations
```php
#[LiveComponent('external-map')]
#[Island(isolated: true, lazy: true)]
final readonly class ExternalMapComponent implements LiveComponentContract
{
// Component that loads external resources
}
```
### Conditional Components
```php
#[LiveComponent('admin-panel')]
#[Island(isolated: true)]
final readonly class AdminPanelComponent implements LiveComponentContract
{
// Component only visible to admins
// Isolated to prevent template processing for non-admin users
}
```
## Technical Details
### Endpoint
Islands are rendered via:
```
GET /live-component/{id}/island
```
This endpoint:
- Renders component HTML without template wrapper
- Returns JSON with `html`, `state`, `csrf_token`
- Uses `ComponentRegistry.render()` directly (no wrapper)
### Frontend Integration
Islands are automatically detected and handled by `LazyComponentLoader`:
- Lazy islands: Loaded when entering viewport
- Non-lazy islands: Loaded immediately
- Isolated initialization: Islands don't receive parent events
### Event Isolation
Islands have isolated event contexts:
- No parent component events
- No shared state with parent components
- Independent SSE channels (if configured)
## Best Practices
1. **Use Islands for Heavy Components**: Only use `#[Island]` for components that significantly impact page load time
2. **Combine with Lazy Loading**: Use `lazy: true` for below-the-fold content
3. **Provide Placeholders**: Always provide meaningful placeholder text for better UX
4. **Test Isolation**: Verify that islands work correctly in isolation
5. **Monitor Performance**: Track island load times and optimize as needed
## Examples
### Complete Example
```php
<?php
declare(strict_types=1);
namespace App\Application\LiveComponents\Dashboard;
use App\Framework\LiveComponents\Attributes\LiveComponent;
use App\Framework\LiveComponents\Attributes\Island;
use App\Framework\LiveComponents\Contracts\LiveComponentContract;
use App\Framework\LiveComponents\ValueObjects\ComponentId;
use App\Framework\LiveComponents\ValueObjects\ComponentRenderData;
use App\Framework\LiveComponents\ValueObjects\ComponentState;
#[LiveComponent('metrics-dashboard')]
#[Island(isolated: true, lazy: true, placeholder: 'Loading metrics...')]
final readonly class MetricsDashboardComponent implements LiveComponentContract
{
public function __construct(
public ComponentId $id,
public ComponentState $state
) {
}
public function getRenderData(): ComponentRenderData
{
return new ComponentRenderData(
templatePath: 'livecomponent-metrics-dashboard',
data: [
'componentId' => $this->id->toString(),
'metrics' => $this->loadMetrics(),
]
);
}
private function loadMetrics(): array
{
// Heavy operation - isolated rendering prevents blocking
return [
'users' => 12345,
'orders' => 6789,
'revenue' => 123456.78,
];
}
}
```
## Migration Guide
### Converting Regular Component to Island
1. Add `#[Island]` attribute to component class
2. Test component rendering (should work identically)
3. Optionally enable lazy loading with `lazy: true`
4. Monitor performance improvements
### Backward Compatibility
- Components without `#[Island]` work exactly as before
- `#[Island]` is opt-in (no breaking changes)
- Existing lazy components continue to work
## Troubleshooting
### Island Not Loading
- Check browser console for errors
- Verify component is registered in ComponentRegistry
- Ensure `/island` endpoint is accessible
### Placeholder Not Showing
- Verify `lazy: true` is set
- Check `placeholder` parameter is provided
- Inspect generated HTML for `data-island-component` attribute
### Events Not Working
- Islands have isolated event contexts
- Use component-specific events, not parent events
- Check SSE channel configuration if using real-time updates
## See Also
- [Lazy Loading Guide](livecomponents-lazy-loading.md)
- [Component Caching](livecomponents-caching.md)
- [Performance Optimization](livecomponents-performance.md)

View File

@@ -0,0 +1,697 @@
# LiveComponents Performance Guide
**Complete performance optimization guide for LiveComponents covering caching, batching, middleware, and optimization techniques.**
---
## Table of Contents
1. [Caching Strategies](#caching-strategies)
2. [Request Batching](#request-batching)
3. [Middleware Performance](#middleware-performance)
4. [Debounce & Throttle](#debounce--throttle)
5. [Lazy Loading](#lazy-loading)
6. [Island Components](#island-components)
7. [Fragment Updates](#fragment-updates)
8. [SSE Optimization](#sse-optimization)
9. [Memory Management](#memory-management)
10. [Performance Checklist](#performance-checklist)
---
## Caching Strategies
### Component-Level Caching
Implement `Cacheable` interface for automatic caching:
```php
#[LiveComponent('user-stats')]
final readonly class UserStatsComponent implements LiveComponentContract, Cacheable
{
public function getCacheKey(): string
{
return 'user-stats:' . $this->state->userId;
}
public function getCacheTTL(): Duration
{
return Duration::fromMinutes(5);
}
public function shouldCache(): bool
{
return true;
}
public function getCacheTags(): array
{
return ['user-stats', 'user:' . $this->state->userId];
}
public function getVaryBy(): ?VaryBy
{
return VaryBy::userId();
}
public function getStaleWhileRevalidate(): ?Duration
{
return Duration::fromHours(1);
}
}
```
### Cache Key Variations
**Global Cache** (same for all users):
```php
public function getVaryBy(): ?VaryBy
{
return VaryBy::none(); // Same cache for everyone
}
```
**Per-User Cache**:
```php
public function getVaryBy(): ?VaryBy
{
return VaryBy::userId(); // Different cache per user
}
```
**Per-User Per-Locale Cache**:
```php
public function getVaryBy(): ?VaryBy
{
return VaryBy::userAndLocale(); // Different cache per user and locale
}
```
**With Feature Flags**:
```php
public function getVaryBy(): ?VaryBy
{
return VaryBy::userId()
->withFeatureFlags(['new-ui', 'dark-mode']);
}
```
### Stale-While-Revalidate (SWR)
Serve stale content while refreshing:
```php
public function getCacheTTL(): Duration
{
return Duration::fromMinutes(5); // Fresh for 5 minutes
}
public function getStaleWhileRevalidate(): ?Duration
{
return Duration::fromHours(1); // Serve stale for 1 hour while refreshing
}
```
**How it works**:
- 0-5min: Serve fresh cache
- 5min-1h: Serve stale cache + trigger background refresh
- >1h: Force fresh render
### Cache Invalidation
**By Tags**:
```php
// Invalidate all user-stats caches
$cache->invalidateTags(['user-stats']);
// Invalidate specific user's cache
$cache->invalidateTags(['user-stats', 'user:123']);
```
**Manual Invalidation**:
```php
$cacheKey = CacheKey::fromString('user-stats:123');
$cache->delete($cacheKey);
```
---
## Request Batching
### Client-Side Batching
Batch multiple operations in a single request:
```javascript
// Batch multiple actions
const response = await LiveComponent.executeBatch([
{
componentId: 'counter:demo',
method: 'increment',
params: { amount: 5 },
fragments: ['counter-display']
},
{
componentId: 'stats:user-123',
method: 'refresh'
},
{
componentId: 'notifications:user-123',
method: 'markAsRead',
params: { notificationId: 'abc' }
}
]);
```
### Server-Side Processing
The framework automatically processes batch requests:
```php
// BatchProcessor handles multiple operations
$batchRequest = new BatchRequest(...$operations);
$response = $batchProcessor->process($batchRequest);
// Returns:
// {
// "success": true,
// "results": [
// { "componentId": "counter:demo", "success": true, "html": "...", "fragments": {...} },
// { "componentId": "stats:user-123", "success": true, "html": "..." },
// { "componentId": "notifications:user-123", "success": true, "html": "..." }
// ],
// "totalOperations": 3,
// "successCount": 3,
// "failureCount": 0
// }
```
### Batch Benefits
- **Reduced HTTP Requests**: Multiple operations in one request
- **Lower Latency**: Single round-trip instead of multiple
- **Atomic Operations**: All succeed or all fail
- **Better Performance**: Especially on slow networks
---
## Middleware Performance
### Middleware Overview
Middleware allows you to intercept and transform component actions:
```php
#[LiveComponent('user-profile')]
#[Middleware(LoggingMiddleware::class)]
#[Middleware(CachingMiddleware::class, priority: 50)]
final readonly class UserProfileComponent implements LiveComponentContract
{
// All actions have Logging + Caching middleware
}
```
### Built-in Middleware
**LoggingMiddleware** - Logs actions with timing:
```php
#[Middleware(LoggingMiddleware::class, priority: 50)]
```
**CachingMiddleware** - Caches action responses:
```php
#[Middleware(CachingMiddleware::class, priority: 100)]
```
**RateLimitMiddleware** - Rate limits actions:
```php
#[Middleware(RateLimitMiddleware::class, priority: 200)]
```
### Custom Middleware
Create custom middleware for specific needs:
```php
final readonly class PerformanceMonitoringMiddleware implements ComponentMiddlewareInterface
{
public function handle(
LiveComponentContract $component,
string $action,
ActionParameters $params,
callable $next
): ComponentUpdate {
$startTime = microtime(true);
$startMemory = memory_get_usage();
$result = $next($component, $action, $params);
$duration = microtime(true) - $startTime;
$memoryUsed = memory_get_usage() - $startMemory;
// Log performance metrics
$this->metricsCollector->record($component::class, $action, $duration, $memoryUsed);
return $result;
}
}
```
### Middleware Priority
Higher priority = earlier execution:
```php
#[Middleware(LoggingMiddleware::class, priority: 50)] // Executes first
#[Middleware(CachingMiddleware::class, priority: 100)] // Executes second
#[Middleware(RateLimitMiddleware::class, priority: 200)] // Executes third
```
**Execution Order**:
1. RateLimitMiddleware (priority: 200)
2. CachingMiddleware (priority: 100)
3. LoggingMiddleware (priority: 50)
4. Action execution
### Middleware Performance Tips
**DO**:
- Use caching middleware for expensive operations
- Use logging middleware for debugging (disable in production)
- Keep middleware lightweight
- Use priority to optimize execution order
**DON'T**:
- Add unnecessary middleware
- Perform heavy operations in middleware
- Cache everything (be selective)
- Use middleware for business logic
---
## Debounce & Throttle
### Client-Side Debouncing
Debounce user input to reduce requests:
```javascript
let searchTimeout;
const searchInput = document.querySelector('[data-live-action="search"]');
searchInput.addEventListener('input', (e) => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
LiveComponent.executeAction('search:demo', 'search', {
query: e.target.value
});
}, 300); // 300ms debounce
});
```
### Client-Side Throttling
Throttle frequent actions:
```javascript
let lastExecution = 0;
const throttleDelay = 1000; // 1 second
function throttledAction() {
const now = Date.now();
if (now - lastExecution < throttleDelay) {
return; // Skip if too soon
}
lastExecution = now;
LiveComponent.executeAction('component:id', 'action', {});
}
```
### Server-Side Rate Limiting
Use `#[Action]` attribute with rate limit:
```php
#[Action(rateLimit: 10)] // 10 requests per minute
public function search(string $query): State
{
return $this->state->withResults($this->searchService->search($query));
}
```
---
## Lazy Loading
### Lazy Component Loading
Load components when entering viewport:
```php
// In template
{{ lazy_component('notification-center:user-123', [
'priority' => 'high',
'threshold' => '0.1',
'placeholder' => 'Loading notifications...'
]) }}
```
**Options**:
- `priority`: `'high'` | `'normal'` | `'low'`
- `threshold`: `'0.0'` to `'1.0'` (viewport intersection threshold)
- `placeholder`: Custom loading text
### Lazy Island Components
Use `#[Island]` for isolated lazy loading:
```php
#[LiveComponent('heavy-widget')]
#[Island(isolated: true, lazy: true, placeholder: 'Loading widget...')]
final readonly class HeavyWidgetComponent implements LiveComponentContract
{
// Component implementation
}
```
**Benefits**:
- Reduces initial page load time
- Loads only when needed
- Isolated rendering (no template overhead)
---
## Island Components
### Island Directive
Isolate resource-intensive components:
```php
#[LiveComponent('analytics-dashboard')]
#[Island(isolated: true, lazy: true)]
final readonly class AnalyticsDashboardComponent implements LiveComponentContract
{
// Heavy component with complex calculations
}
```
**Features**:
- **Isolated Rendering**: Separate from main template
- **Lazy Loading**: Load on viewport entry
- **Independent Updates**: No parent component re-renders
### When to Use Islands
**Use Islands for**:
- Heavy calculations
- External API calls
- Complex data visualizations
- Third-party widgets
- Below-the-fold content
**Don't use Islands for**:
- Simple components
- Above-the-fold content
- Components that need parent state
- Frequently updated components
---
## Fragment Updates
### Partial Rendering
Update only specific parts of a component:
```html
<!-- Template -->
<div data-lc-fragment="counter-display">
<h2>Count: {count}</h2>
</div>
<div data-lc-fragment="actions">
<button data-live-action="increment" data-lc-fragments="counter-display">
Increment
</button>
</div>
```
**Client-side**:
```javascript
// Only counter-display fragment is updated
LiveComponent.executeAction('counter:demo', 'increment', {}, {
fragments: ['counter-display']
});
```
### Fragment Benefits
- **Reduced DOM Updates**: Only update what changed
- **Better Performance**: Less re-rendering overhead
- **Smoother UX**: Faster perceived performance
- **Lower Bandwidth**: Smaller response payloads
---
## SSE Optimization
### Server-Sent Events
Real-time updates via SSE:
```php
#[LiveComponent('live-feed')]
final readonly class LiveFeedComponent implements LiveComponentContract, Pollable
{
public function getPollInterval(): int
{
return 5000; // Poll every 5 seconds
}
public function poll(): LiveComponentState
{
// Fetch latest data
$newItems = $this->feedService->getLatest();
return $this->state->withItems($newItems);
}
}
```
### SSE Configuration
**Heartbeat Interval**:
```env
LIVECOMPONENT_SSE_HEARTBEAT=15 # Seconds
```
**Connection Timeout**:
```env
LIVECOMPONENT_SSE_TIMEOUT=300 # Seconds
```
### SSE Best Practices
**DO**:
- Use SSE for real-time updates
- Set appropriate poll intervals
- Handle reconnection gracefully
- Monitor connection health
**DON'T**:
- Poll too frequently (< 1 second)
- Keep connections open indefinitely
- Ignore connection errors
- Use SSE for one-time operations
---
## Memory Management
### Component State Size
Keep component state minimal:
```php
// ✅ GOOD: Minimal state
final readonly class CounterState extends ComponentState
{
public function __construct(
public int $count = 0
) {}
}
// ❌ BAD: Large state
final readonly class CounterState extends ComponentState
{
public function __construct(
public int $count = 0,
public array $largeDataSet = [], // Don't store large data in state
public string $hugeString = '' // Don't store large strings
) {}
}
```
### State Cleanup
Clean up unused state:
```php
#[Action]
public function clearCache(): CounterState
{
// Remove cached data from state
return $this->state->withoutCache();
}
```
### Memory Monitoring
Monitor component memory usage:
```php
final readonly class MemoryMonitoringMiddleware implements ComponentMiddlewareInterface
{
public function handle($component, $action, $params, $next): ComponentUpdate
{
$startMemory = memory_get_usage();
$result = $next($component, $action, $params);
$endMemory = memory_get_usage();
if ($endMemory - $startMemory > 1024 * 1024) { // > 1MB
error_log("Memory spike in {$component::class}::{$action}: " . ($endMemory - $startMemory));
}
return $result;
}
}
```
---
## Performance Checklist
### Component Development
- [ ] Use caching for expensive operations
- [ ] Implement `Cacheable` interface where appropriate
- [ ] Use fragments for partial updates
- [ ] Use lazy loading for below-the-fold content
- [ ] Use islands for heavy components
- [ ] Keep component state minimal
- [ ] Use batch requests for multiple operations
- [ ] Debounce/throttle user input
- [ ] Set appropriate rate limits
### Middleware
- [ ] Use caching middleware for expensive actions
- [ ] Use logging middleware for debugging (disable in production)
- [ ] Keep middleware lightweight
- [ ] Set appropriate middleware priorities
- [ ] Monitor middleware performance
### Caching
- [ ] Configure cache TTL appropriately
- [ ] Use cache tags for grouped invalidation
- [ ] Use `VaryBy` for user-specific caching
- [ ] Use SWR for better perceived performance
- [ ] Monitor cache hit rates
### Testing
- [ ] Test component performance under load
- [ ] Monitor memory usage
- [ ] Test with slow network conditions
- [ ] Test batch request performance
- [ ] Test fragment update performance
---
## Performance Patterns
### Pattern 1: Cached Search Component
```php
#[LiveComponent('search')]
#[Middleware(CachingMiddleware::class, priority: 100)]
final readonly class SearchComponent implements LiveComponentContract, Cacheable
{
#[Action(rateLimit: 20)]
public function search(string $query): SearchState
{
if (strlen($query) < 3) {
return $this->state->withResults([]);
}
$results = $this->searchService->search($query);
return $this->state->withQuery($query)->withResults($results);
}
public function getCacheKey(): string
{
return 'search:' . md5($this->state->query);
}
public function getCacheTTL(): Duration
{
return Duration::fromMinutes(10);
}
public function getVaryBy(): ?VaryBy
{
return VaryBy::userId(); // Different results per user
}
}
```
### Pattern 2: Lazy-Loaded Dashboard
```php
#[LiveComponent('dashboard')]
#[Island(isolated: true, lazy: true, placeholder: 'Loading dashboard...')]
final readonly class DashboardComponent implements LiveComponentContract, Cacheable
{
public function getCacheKey(): string
{
return 'dashboard:' . $this->state->userId;
}
public function getCacheTTL(): Duration
{
return Duration::fromMinutes(5);
}
public function getStaleWhileRevalidate(): ?Duration
{
return Duration::fromHours(1); // Serve stale for 1 hour
}
}
```
### Pattern 3: Batch Operations
```javascript
// Client-side: Batch multiple updates
const updates = [
{ componentId: 'counter:1', method: 'increment', params: {} },
{ componentId: 'counter:2', method: 'increment', params: {} },
{ componentId: 'counter:3', method: 'increment', params: {} },
];
const response = await LiveComponent.executeBatch(updates);
```
---
## Next Steps
- [Security Guide](security-guide-complete.md) - Security best practices
- [End-to-End Guide](end-to-end-guide.md) - Complete development guide
- [API Reference](api-reference-complete.md) - Complete API documentation

View File

@@ -0,0 +1,756 @@
# LiveComponents Security Guide
**Complete security guide for LiveComponents covering CSRF protection, rate limiting, idempotency, authorization, and best practices.**
---
## Table of Contents
1. [CSRF Protection](#csrf-protection)
2. [Rate Limiting](#rate-limiting)
3. [Idempotency](#idempotency)
4. [Action Allow-List](#action-allow-list)
5. [Authorization](#authorization)
6. [Input Validation](#input-validation)
7. [XSS Prevention](#xss-prevention)
8. [Secure State Management](#secure-state-management)
9. [Security Checklist](#security-checklist)
---
## CSRF Protection
### How It Works
LiveComponents automatically protect all actions with CSRF tokens:
1. **Token Generation**: Unique token generated per component instance
2. **Token Validation**: Token validated on every action request
3. **Token Regeneration**: Token regenerated after each action
### Implementation
**Server-Side** (Automatic):
```php
// CSRF token is automatically generated and validated
#[Action]
public function increment(): CounterState
{
// CSRF validation happens automatically before this method is called
return $this->state->increment();
}
```
**Client-Side** (Automatic):
```html
<!-- CSRF token is automatically included in action requests -->
<button data-live-action="increment">Increment</button>
```
### Manual CSRF Token Access
If you need to access CSRF tokens manually:
```php
use App\Framework\LiveComponents\ComponentRegistry;
$registry = container()->get(ComponentRegistry::class);
$csrfToken = $registry->generateCsrfToken($componentId);
```
### CSRF Token Format
- **Length**: 32 hexadecimal characters
- **Format**: `[a-f0-9]{32}`
- **Scope**: Per component instance (not global)
### Best Practices
**DO**:
- Let the framework handle CSRF automatically
- Include CSRF meta tag in base layout: `<meta name="csrf-token" content="{csrf_token}">`
- Use HTTPS in production
**DON'T**:
- Disable CSRF protection
- Share CSRF tokens between components
- Store CSRF tokens in localStorage (use session)
---
## Rate Limiting
### Overview
Rate limiting prevents abuse by limiting the number of actions per time period.
### Configuration
**Global Rate Limit** (`.env`):
```env
LIVECOMPONENT_RATE_LIMIT=60 # 60 requests per minute per component
```
**Per-Action Rate Limit**:
```php
#[Action(rateLimit: 10)] // 10 requests per minute for this action
public function expensiveOperation(): State
{
// Implementation
}
```
### Rate Limit Headers
When rate limit is exceeded, response includes:
```
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1640995200
Retry-After: 30
```
### Client-Side Handling
```javascript
// Automatic retry after Retry-After header
LiveComponent.executeAction('counter:demo', 'increment')
.catch(error => {
if (error.status === 429) {
const retryAfter = error.headers['retry-after'];
console.log(`Rate limited. Retry after ${retryAfter} seconds`);
}
});
```
### Best Practices
**DO**:
- Set appropriate rate limits based on action cost
- Use higher limits for read operations, lower for write operations
- Monitor rate limit violations
**DON'T**:
- Set rate limits too low (hurts UX)
- Set rate limits too high (allows abuse)
- Ignore rate limit violations
---
## Idempotency
### Overview
Idempotency ensures that repeating the same action multiple times has the same effect as performing it once.
### Configuration
**Per-Action Idempotency**:
```php
#[Action(idempotencyTTL: 60)] // Idempotent for 60 seconds
public function processPayment(float $amount): State
{
// This action will return cached result if called again within 60 seconds
return $this->state->processPayment($amount);
}
```
### How It Works
1. **First Request**: Action executes, result cached with idempotency key
2. **Subsequent Requests**: Same idempotency key returns cached result
3. **After TTL**: Cache expires, action can execute again
### Idempotency Key
**Client-Side**:
```javascript
// Generate unique idempotency key
const idempotencyKey = `payment-${Date.now()}-${Math.random()}`;
LiveComponent.executeAction('payment:demo', 'processPayment', {
amount: 100.00,
idempotency_key: idempotencyKey
});
```
**Server-Side** (Automatic):
- Idempotency key extracted from request
- Key includes: component ID + action name + parameters
- Cached result returned if key matches
### Use Cases
**Good for**:
- Payment processing
- Order creation
- Email sending
- External API calls
**Not suitable for**:
- Incrementing counters
- Appending to lists
- Time-sensitive operations
### Best Practices
**DO**:
- Use idempotency for critical operations
- Set appropriate TTL (long enough to prevent duplicates, short enough to allow retries)
- Include idempotency key in client requests
**DON'T**:
- Use idempotency for operations that should execute multiple times
- Set TTL too long (prevents legitimate retries)
- Rely solely on idempotency for security
---
## Action Allow-List
### Overview
Only methods marked with `#[Action]` can be called from the client.
### Implementation
```php
#[LiveComponent('user-profile')]
final readonly class UserProfileComponent implements LiveComponentContract
{
// ✅ Can be called from client
#[Action]
public function updateProfile(array $data): State
{
return $this->state->updateProfile($data);
}
// ❌ Cannot be called from client (no #[Action] attribute)
public function internalMethod(): void
{
// This method is not exposed to the client
}
// ❌ Reserved methods cannot be actions
public function mount(): State
{
// Reserved method - cannot be called as action
}
}
```
### Reserved Methods
These methods cannot be actions:
- `mount()`
- `getRenderData()`
- `getId()`
- `getState()`
- `getData()`
- Methods starting with `_` (private convention)
### Security Benefits
- **Explicit API**: Only intended methods are callable
- **No Accidental Exposure**: Internal methods stay internal
- **Clear Intent**: `#[Action]` makes API surface explicit
### Best Practices
**DO**:
- Mark all public methods that should be callable with `#[Action]`
- Keep internal methods without `#[Action]`
- Use descriptive action names
**DON'T**:
- Mark internal methods with `#[Action]`
- Expose sensitive operations without proper authorization
- Use generic action names like `do()` or `execute()`
---
## Authorization
### Overview
Use `#[RequiresPermission]` to restrict actions to authorized users.
### Implementation
```php
#[LiveComponent('post-editor')]
final readonly class PostEditorComponent implements LiveComponentContract
{
#[Action]
#[RequiresPermission('posts.edit')]
public function updatePost(array $data): State
{
return $this->state->updatePost($data);
}
#[Action]
#[RequiresPermission('posts.delete')]
public function deletePost(): State
{
return $this->state->deletePost();
}
// Multiple permissions (user needs ALL)
#[Action]
#[RequiresPermission('posts.edit', 'posts.publish')]
public function publishPost(): State
{
return $this->state->publishPost();
}
}
```
### Permission Checking
**Custom Authorization Checker**:
```php
use App\Framework\LiveComponents\Security\AuthorizationCheckerInterface;
final readonly class CustomAuthorizationChecker implements AuthorizationCheckerInterface
{
public function hasPermission(string $permission): bool
{
// Check if current user has permission
return $this->user->hasPermission($permission);
}
public function hasAllPermissions(array $permissions): bool
{
foreach ($permissions as $permission) {
if (!$this->hasPermission($permission)) {
return false;
}
}
return true;
}
}
```
### Unauthorized Access
When user lacks required permission:
```json
{
"success": false,
"error": {
"code": "UNAUTHORIZED",
"message": "User does not have required permission: posts.edit"
}
}
```
### Best Practices
**DO**:
- Use authorization for sensitive operations
- Check permissions server-side (never trust client)
- Use specific permissions (not generic like 'admin')
- Log authorization failures
**DON'T**:
- Rely on client-side authorization checks
- Use overly broad permissions
- Skip authorization for write operations
- Expose permission names in error messages
---
## Input Validation
### Overview
Always validate input in actions before processing.
### Implementation
```php
#[Action]
public function updateProfile(array $data): State
{
// Validate input
$errors = [];
if (empty($data['name'])) {
$errors[] = 'Name is required';
}
if (isset($data['email']) && !filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
$errors[] = 'Invalid email address';
}
if (isset($data['age']) && ($data['age'] < 0 || $data['age'] > 150)) {
$errors[] = 'Age must be between 0 and 150';
}
if (!empty($errors)) {
throw new ValidationException('Validation failed', $errors);
}
// Process valid data
return $this->state->updateProfile($data);
}
```
### Type Safety
Use type hints for automatic validation:
```php
#[Action]
public function addAmount(int $amount): State
{
// PHP automatically validates $amount is an integer
if ($amount < 0) {
throw new \InvalidArgumentException('Amount must be positive');
}
return $this->state->addAmount($amount);
}
```
### Value Objects
Use Value Objects for complex validation:
```php
#[Action]
public function updateEmail(Email $email): State
{
// Email Value Object validates format automatically
return $this->state->updateEmail($email);
}
```
### Best Practices
**DO**:
- Validate all input in actions
- Use type hints for automatic validation
- Use Value Objects for complex data
- Return clear error messages
**DON'T**:
- Trust client input
- Skip validation for "internal" actions
- Expose internal validation details
- Use generic error messages
---
## XSS Prevention
### Overview
LiveComponents automatically escape output in templates.
### Template Escaping
**Automatic Escaping**:
```html
<!-- Automatically escaped -->
<div>{user_input}</div>
<!-- Safe HTML (if needed) -->
<div>{html_content|raw}</div>
```
### Best Practices
**DO**:
- Let the framework escape output automatically
- Use `|raw` filter only when necessary
- Validate HTML content before storing
- Use Content Security Policy (CSP)
**DON'T**:
- Disable automatic escaping
- Use `|raw` with user input
- Store unvalidated HTML
- Trust client-provided HTML
---
## Secure State Management
### Overview
Component state should never contain sensitive data.
### Sensitive Data
**DON'T Store**:
- Passwords
- API keys
- Credit card numbers
- Session tokens
- Private keys
**DO Store**:
- User IDs (not passwords)
- Display preferences
- UI state
- Non-sensitive configuration
### State Encryption
For sensitive state (if absolutely necessary):
```php
use App\Framework\Encryption\EncryptionService;
final readonly class SecureComponent implements LiveComponentContract
{
public function __construct(
private EncryptionService $encryption
) {
}
public function storeSensitiveData(string $data): State
{
$encrypted = $this->encryption->encrypt($data);
return $this->state->withEncryptedData($encrypted);
}
}
```
### Best Practices
**DO**:
- Keep state minimal
- Store only necessary data
- Use server-side storage for sensitive data
- Encrypt sensitive state if needed
**DON'T**:
- Store passwords or secrets in state
- Include sensitive data in state
- Trust client-provided state
- Expose internal implementation details
---
## Security Checklist
### Component Development
- [ ] All actions marked with `#[Action]`
- [ ] No sensitive data in component state
- [ ] Input validation in all actions
- [ ] Authorization checks for sensitive operations
- [ ] Rate limiting configured appropriately
- [ ] Idempotency for critical operations
- [ ] CSRF protection enabled (automatic)
- [ ] Error messages don't expose sensitive information
### Testing
- [ ] CSRF token validation tested
- [ ] Rate limiting tested
- [ ] Authorization tested
- [ ] Input validation tested
- [ ] XSS prevention tested
- [ ] Error handling tested
### Deployment
- [ ] HTTPS enabled
- [ ] CSRF protection enabled
- [ ] Rate limits configured
- [ ] Error reporting configured
- [ ] Security headers configured
- [ ] Content Security Policy configured
---
## Common Security Pitfalls
### 1. Trusting Client State
**WRONG**:
```php
#[Action]
public function updateBalance(float $newBalance): State
{
// Don't trust client-provided balance!
return $this->state->withBalance($newBalance);
}
```
**CORRECT**:
```php
#[Action]
public function addToBalance(float $amount): State
{
// Calculate new balance server-side
$newBalance = $this->state->balance + $amount;
return $this->state->withBalance($newBalance);
}
```
### 2. Skipping Authorization
**WRONG**:
```php
#[Action]
public function deleteUser(int $userId): State
{
// No authorization check!
$this->userService->delete($userId);
return $this->state;
}
```
**CORRECT**:
```php
#[Action]
#[RequiresPermission('users.delete')]
public function deleteUser(int $userId): State
{
$this->userService->delete($userId);
return $this->state;
}
```
### 3. Exposing Sensitive Data
**WRONG**:
```php
public function getRenderData(): ComponentRenderData
{
return new ComponentRenderData(
templatePath: 'user-profile',
data: [
'password' => $this->user->password, // ❌ Never expose passwords!
'api_key' => $this->user->apiKey, // ❌ Never expose API keys!
]
);
}
```
**CORRECT**:
```php
public function getRenderData(): ComponentRenderData
{
return new ComponentRenderData(
templatePath: 'user-profile',
data: [
'username' => $this->user->username,
'email' => $this->user->email,
// Only include safe, display data
]
);
}
```
---
## Security Patterns
### Pattern 1: Secure File Upload
```php
#[LiveComponent('file-uploader')]
final readonly class FileUploaderComponent implements LiveComponentContract, SupportsFileUpload
{
public function validateUpload(UploadedFile $file): array
{
$errors = [];
// Check file type
$allowedTypes = ['image/jpeg', 'image/png'];
if (!in_array($file->getMimeType(), $allowedTypes)) {
$errors[] = 'Invalid file type';
}
// Check file size
if ($file->getSize() > 5 * 1024 * 1024) {
$errors[] = 'File too large';
}
// Check file extension
$extension = pathinfo($file->getName(), PATHINFO_EXTENSION);
$allowedExtensions = ['jpg', 'jpeg', 'png'];
if (!in_array(strtolower($extension), $allowedExtensions)) {
$errors[] = 'Invalid file extension';
}
return $errors;
}
#[Action]
#[RequiresPermission('files.upload')]
public function handleUpload(UploadedFile $file): State
{
// Additional server-side validation
$errors = $this->validateUpload($file);
if (!empty($errors)) {
throw new ValidationException('Upload validation failed', $errors);
}
// Process upload securely
$path = $this->storage->store($file);
return $this->state->withUploadedFile($path);
}
}
```
### Pattern 2: Secure Payment Processing
```php
#[LiveComponent('payment')]
final readonly class PaymentComponent implements LiveComponentContract
{
#[Action]
#[RequiresPermission('payments.process')]
#[Action(rateLimit: 5, idempotencyTTL: 300)] // 5 per minute, 5 min idempotency
public function processPayment(
float $amount,
string $paymentMethod,
string $idempotencyKey
): State {
// Validate amount
if ($amount <= 0 || $amount > 10000) {
throw new ValidationException('Invalid amount');
}
// Validate payment method
$allowedMethods = ['credit_card', 'paypal'];
if (!in_array($paymentMethod, $allowedMethods)) {
throw new ValidationException('Invalid payment method');
}
// Process payment (idempotent)
$transaction = $this->paymentService->process(
$amount,
$paymentMethod,
$idempotencyKey
);
return $this->state->withTransaction($transaction);
}
}
```
---
## Next Steps
- [Performance Guide](performance-guide-complete.md) - Optimization techniques
- [End-to-End Guide](end-to-end-guide.md) - Complete development guide
- [API Reference](api-reference-complete.md) - Complete API documentation

View File

@@ -11,9 +11,10 @@ This guide covers the integrated UI features available in LiveComponents, includ
1. [Tooltip System](#tooltip-system)
2. [Loading States & Skeleton Loading](#loading-states--skeleton-loading)
3. [UI Helper System](#ui-helper-system)
4. [Notification Component](#notification-component)
5. [Dialog & Modal Integration](#dialog--modal-integration)
6. [Best Practices](#best-practices)
4. [Event-Based UI Integration](#event-based-ui-integration)
5. [Notification Component](#notification-component)
6. [Dialog & Modal Integration](#dialog--modal-integration)
7. [Best Practices](#best-practices)
---
@@ -338,6 +339,332 @@ uiHelper.hideNotification('component-id');
---
## Event-Based UI Integration
### Overview
The Event-Based UI Integration system allows LiveComponents to trigger UI components (Toasts, Modals) by dispatching events from PHP actions. The JavaScript `UIEventHandler` automatically listens to these events and displays the appropriate UI components.
**Features**:
- Automatic UI component display from PHP events
- Type-safe event payloads
- Toast queue management
- Modal stack management
- Zero JavaScript code required in components
### Architecture
- **PHP Side**: Components dispatch events via `ComponentEventDispatcher`
- **JavaScript Side**: `UIEventHandler` listens to events and displays UI components
- **Integration**: Events are automatically dispatched after action execution
### Basic Usage with UIHelper
The `UIHelper` class provides convenient methods for dispatching UI events:
```php
<?php
declare(strict_types=1);
namespace App\Application\LiveComponents\Product;
use App\Framework\LiveComponents\Attributes\Action;
use App\Framework\LiveComponents\Attributes\LiveComponent;
use App\Framework\LiveComponents\ComponentEventDispatcher;
use App\Framework\LiveComponents\Contracts\LiveComponentContract;
use App\Framework\LiveComponents\UI\UIHelper;
use App\Framework\LiveComponents\ValueObjects\ComponentId;
#[LiveComponent('product-form')]
final readonly class ProductFormComponent implements LiveComponentContract
{
public function __construct(
public ComponentId $id,
public ProductFormState $state
) {
}
#[Action]
public function save(?ComponentEventDispatcher $events = null): ProductFormState
{
// ... save logic ...
// Show success toast
(new UIHelper($events))->successToast('Product saved successfully!');
return $this->state->withSaved();
}
#[Action]
public function delete(int $productId, ?ComponentEventDispatcher $events = null): ProductFormState
{
// Show confirmation dialog with action
(new UIHelper($events))->confirmDelete(
$this->id,
"product #{$productId}",
'doDelete',
['id' => $productId]
);
return $this->state;
}
#[Action]
public function doDelete(int $id): ProductFormState
{
// This action is automatically called when user confirms
// ... delete logic ...
return $this->state->withoutProduct($id);
}
}
```
### Toast Events
#### Show Toast
```php
use App\Framework\LiveComponents\UI\UIHelper;
#[Action]
public function save(?ComponentEventDispatcher $events = null): State
{
// Basic success toast
(new UIHelper($events))->successToast('Saved successfully!');
// Toast with custom duration
(new UIHelper($events))->infoToast('File uploaded', 7000);
// Using fluent interface for multiple toasts
(new UIHelper($events))
->infoToast('Processing...')
->successToast('Done!');
return $this->state;
}
```
**Toast Types**:
- `info` - Blue, informational message
- `success` - Green, success message
- `warning` - Orange, warning message
- `error` - Red, error message
**Positions**:
- `top-right` (default)
- `top-left`
- `bottom-right`
- `bottom-left`
#### Hide Toast
```php
#[Action]
public function dismissNotification(?ComponentEventDispatcher $events = null): State
{
$this->hideToast($events, 'global');
return $this->state;
}
```
### Modal Events
#### Show Modal
```php
use App\Framework\LiveComponents\UI\UIHelper;
use App\Framework\LiveComponents\Events\UI\Options\ModalOptions;
use App\Framework\LiveComponents\Events\UI\Enums\ModalSize;
#[Action]
public function showEditForm(int $userId, ?ComponentEventDispatcher $events = null): State
{
$formHtml = $this->renderEditForm($userId);
(new UIHelper($events))->modal(
$this->id,
'Edit User',
$formHtml,
ModalOptions::create()
->withSize(ModalSize::Large)
->withButtons([
['text' => 'Save', 'class' => 'btn-primary', 'action' => 'save'],
['text' => 'Cancel', 'class' => 'btn-secondary', 'action' => 'close']
])
);
return $this->state;
}
```
#### Show Confirmation Dialog
```php
use App\Framework\LiveComponents\UI\UIHelper;
#[Action]
public function requestDelete(int $id, ?ComponentEventDispatcher $events = null): State
{
(new UIHelper($events))->confirmDelete(
$this->id,
"item #{$id}",
'doDelete',
['id' => $id]
);
return $this->state;
}
```
**Note**: Confirmation dialogs return a result via the `modal:confirm:result` event. You can listen to this event in JavaScript to handle the confirmation:
```javascript
document.addEventListener('modal:confirm:result', (e) => {
const { componentId, confirmed } = e.detail;
if (confirmed) {
// User confirmed - execute action
LiveComponentManager.getInstance()
.executeAction(componentId, 'confirmDelete', { id: 123 });
}
});
```
#### Show Alert Dialog
```php
use App\Framework\LiveComponents\UI\UIHelper;
#[Action]
public function showError(?ComponentEventDispatcher $events = null): State
{
(new UIHelper($events))->alertError(
$this->id,
'Error',
'An error occurred while processing your request.'
);
return $this->state;
}
```
#### Close Modal
```php
use App\Framework\LiveComponents\UI\UIHelper;
#[Action]
public function closeModal(?ComponentEventDispatcher $events = null): State
{
(new UIHelper($events))->closeModal($this->id);
return $this->state;
}
```
### Manual Event Dispatching
If you prefer to dispatch events manually (without UIHelper):
```php
use App\Framework\LiveComponents\ComponentEventDispatcher;
use App\Framework\LiveComponents\Events\UI\ToastShowEvent;
#[Action]
public function save(?ComponentEventDispatcher $events = null): State
{
// ... save logic ...
if ($events !== null) {
// Option 1: Using Event classes (recommended)
$events->dispatchEvent(ToastShowEvent::success('Saved successfully!'));
// Option 2: Direct instantiation
$events->dispatchEvent(new ToastShowEvent(
message: 'Saved successfully!',
type: 'success',
duration: 5000
));
}
return $this->state;
}
```
### JavaScript Event Handling
The `UIEventHandler` automatically listens to these events and displays UI components. You can also listen to events manually:
```javascript
// Listen to toast events
document.addEventListener('toast:show', (e) => {
const { message, type, duration, position } = e.detail;
console.log(`Toast: ${message} (${type})`);
});
// Listen to modal events
document.addEventListener('modal:show', (e) => {
const { componentId, title, content } = e.detail;
console.log(`Modal shown: ${title}`);
});
// Listen to confirmation results
document.addEventListener('modal:confirm:result', (e) => {
const { componentId, confirmed } = e.detail;
if (confirmed) {
// Handle confirmation
}
});
```
### Toast Queue Management
The `ToastQueue` automatically manages multiple toasts:
- Maximum 5 toasts per position (configurable)
- Automatic stacking with spacing
- Oldest toast removed when limit reached
- Auto-dismiss with queue management
### Modal Stack Management
The `ModalManager` handles modal stacking using native `<dialog>` element:
- **Native `<dialog>` element**: Uses `showModal()` for true modal behavior
- **Automatic backdrop**: Native `::backdrop` pseudo-element
- **Automatic focus management**: Browser handles focus trapping
- **Automatic ESC handling**: Native `cancel` event
- **Z-index management**: Manual stacking for nested modals
- **Stack tracking**: Manages multiple open modals
#### Native `<dialog>` Benefits
The `<dialog>` element provides:
- **True modal behavior**: Blocks background interaction via `showModal()`
- **Native backdrop**: Automatic overlay with `::backdrop` pseudo-element
- **Automatic focus trapping**: Browser handles focus management
- **Automatic ESC handling**: Native `cancel` event
- **Better accessibility**: Native ARIA attributes and semantics
- **Better performance**: Native browser implementation
- **Wide browser support**: Chrome 37+, Firefox 98+, Safari 15.4+
The system uses native `<dialog>` features for optimal modal behavior and accessibility.
### Best Practices
1. **Use UIHelper**: Prefer the UIHelper class over manual event dispatching for type safety and convenience
2. **Component IDs**: Always provide component IDs for modals to enable proper management
3. **Toast Duration**: Use appropriate durations (5s for success, longer for errors)
4. **Modal Sizes**: Choose appropriate sizes (small for alerts, large for forms)
5. **Error Handling**: Always show user-friendly error messages
```php
// Good: Clear, user-friendly toast
(new UIHelper($events))->successToast('Product saved successfully!');
// Bad: Technical error message
(new UIHelper($events))->errorToast('SQL Error: INSERT failed');
```
---
## Notification Component
### Overview
@@ -519,18 +846,23 @@ final class UserSettings extends LiveComponent
### Modal with LiveComponent Content
```php
use App\Framework\LiveComponents\UI\UIHelper;
use App\Framework\LiveComponents\Events\UI\Options\ModalOptions;
use App\Framework\LiveComponents\Events\UI\Enums\ModalSize;
#[Action]
public function showUserModal(int $userId): void
public function showUserModal(int $userId, ?ComponentEventDispatcher $events = null): void
{
$userComponent = UserDetailsComponent::mount(
ComponentId::generate('user-details'),
userId: $userId
);
$this->uiHelper->showModal(
title: 'User Details',
content: $userComponent->render(),
size: 'medium'
(new UIHelper($events))->modal(
$this->id,
'User Details',
$userComponent->render(),
ModalOptions::create()->withSize(ModalSize::Medium)
);
}
```