fix: DockerSecretsResolver - don't normalize absolute paths like /var/www/html/...
Some checks failed
Deploy Application / deploy (push) Has been cancelled
Some checks failed
Deploy Application / deploy (push) Has been cancelled
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
877
docs/livecomponents/end-to-end-guide.md
Normal file
877
docs/livecomponents/end-to-end-guide.md
Normal 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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
308
docs/livecomponents/island-directive.md
Normal file
308
docs/livecomponents/island-directive.md
Normal 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)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
697
docs/livecomponents/performance-guide-complete.md
Normal file
697
docs/livecomponents/performance-guide-complete.md
Normal 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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
756
docs/livecomponents/security-guide-complete.md
Normal file
756
docs/livecomponents/security-guide-complete.md
Normal 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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user