- Add comprehensive health check system with multiple endpoints - Add Prometheus metrics endpoint - Add production logging configurations (5 strategies) - Add complete deployment documentation suite: * QUICKSTART.md - 30-minute deployment guide * DEPLOYMENT_CHECKLIST.md - Printable verification checklist * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference * production-logging.md - Logging configuration guide * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation * README.md - Navigation hub * DEPLOYMENT_SUMMARY.md - Executive summary - Add deployment scripts and automation - Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment - Update README with production-ready features All production infrastructure is now complete and ready for deployment.
1028 lines
23 KiB
Markdown
1028 lines
23 KiB
Markdown
# Advanced LiveComponents Features
|
|
|
|
**Deep Dive into Advanced LiveComponents Functionality**
|
|
|
|
This guide explores advanced features for building sophisticated, high-performance LiveComponents applications.
|
|
|
|
---
|
|
|
|
## Table of Contents
|
|
|
|
1. [Fragment-Based Rendering](#fragment-based-rendering)
|
|
2. [Request Batching](#request-batching)
|
|
3. [Server-Sent Events (SSE)](#server-sent-events-sse)
|
|
4. [Optimistic UI Updates](#optimistic-ui-updates)
|
|
5. [Chunked File Uploads](#chunked-file-uploads)
|
|
6. [Component Communication](#component-communication)
|
|
7. [State Management Patterns](#state-management-patterns)
|
|
8. [Custom Actions](#custom-actions)
|
|
|
|
---
|
|
|
|
## Fragment-Based Rendering
|
|
|
|
### Overview
|
|
|
|
Fragment rendering enables updating specific parts of a component without re-rendering the entire component DOM tree.
|
|
|
|
**Benefits**:
|
|
- 70-90% reduction in DOM updates
|
|
- Preserved focus and scroll position
|
|
- Improved perceived performance
|
|
- Lower bandwidth usage
|
|
|
|
### Basic Fragment Usage
|
|
|
|
```html
|
|
<!-- Component template with fragments -->
|
|
<div data-component-id="{component_id}">
|
|
<!-- Static header - never updates -->
|
|
<header>
|
|
<h1>Product Dashboard</h1>
|
|
</header>
|
|
|
|
<!-- Fragment 1: Product list -->
|
|
<div data-lc-fragment="product-list">
|
|
<for items="products" as="product">
|
|
<div class="product-card">
|
|
<h3>{product.name}</h3>
|
|
<p>{product.price}</p>
|
|
</div>
|
|
</for>
|
|
</div>
|
|
|
|
<!-- Fragment 2: Shopping cart -->
|
|
<div data-lc-fragment="cart">
|
|
<p>Items: {cartCount}</p>
|
|
<p>Total: {cartTotal}</p>
|
|
</div>
|
|
|
|
<!-- Fragment 3: User preferences -->
|
|
<div data-lc-fragment="preferences">
|
|
<!-- Rarely updated -->
|
|
<form data-lc-submit="savePreferences">
|
|
<!-- Form fields -->
|
|
</form>
|
|
</div>
|
|
</div>
|
|
```
|
|
|
|
### Server-Side Fragment Control
|
|
|
|
```php
|
|
use App\Framework\LiveComponents\Attributes\Fragment;
|
|
|
|
final class ProductDashboard extends LiveComponent
|
|
{
|
|
#[LiveProp]
|
|
public array $products = [];
|
|
|
|
#[LiveProp]
|
|
public int $cartCount = 0;
|
|
|
|
#[LiveProp]
|
|
public float $cartTotal = 0.0;
|
|
|
|
// Update only product list fragment
|
|
#[LiveAction]
|
|
#[Fragment('product-list')]
|
|
public function filterProducts(string $category): void
|
|
{
|
|
$this->products = $this->productService->filterByCategory($category);
|
|
// Cart and preferences remain unchanged
|
|
}
|
|
|
|
// Update only cart fragment
|
|
#[LiveAction]
|
|
#[Fragment('cart')]
|
|
public function addToCart(string $productId): void
|
|
{
|
|
$this->cart->add($productId);
|
|
$this->cartCount = $this->cart->count();
|
|
$this->cartTotal = $this->cart->total();
|
|
// Product list and preferences remain unchanged
|
|
}
|
|
|
|
// Update multiple fragments
|
|
#[LiveAction]
|
|
#[Fragment(['product-list', 'cart'])]
|
|
public function clearCart(): void
|
|
{
|
|
$this->cart->clear();
|
|
$this->cartCount = 0;
|
|
$this->cartTotal = 0.0;
|
|
$this->loadRecommendedProducts(); // Affects product list
|
|
}
|
|
}
|
|
```
|
|
|
|
### Nested Fragments
|
|
|
|
```html
|
|
<!-- Nested fragment pattern -->
|
|
<div data-lc-fragment="dashboard">
|
|
<div class="header">
|
|
<h2>Analytics Dashboard</h2>
|
|
</div>
|
|
|
|
<!-- Child fragment 1 -->
|
|
<div data-lc-fragment="user-stats">
|
|
<p>Active Users: {activeUsers}</p>
|
|
<p>New Signups: {newSignups}</p>
|
|
</div>
|
|
|
|
<!-- Child fragment 2 -->
|
|
<div data-lc-fragment="revenue-stats">
|
|
<p>Today's Revenue: {todayRevenue}</p>
|
|
<p>Monthly Revenue: {monthlyRevenue}</p>
|
|
</div>
|
|
</div>
|
|
```
|
|
|
|
```php
|
|
// Update parent fragment (updates everything)
|
|
#[LiveAction]
|
|
#[Fragment('dashboard')]
|
|
public function refreshAll(): void
|
|
{
|
|
$this->updateUserStats();
|
|
$this->updateRevenueStats();
|
|
}
|
|
|
|
// Update specific child fragment
|
|
#[LiveAction]
|
|
#[Fragment('user-stats')]
|
|
public function refreshUserStats(): void
|
|
{
|
|
$this->activeUsers = $this->analyticsService->getActiveUsers();
|
|
$this->newSignups = $this->analyticsService->getNewSignups();
|
|
}
|
|
```
|
|
|
|
### Dynamic Fragment Names
|
|
|
|
```html
|
|
<!-- Dynamic fragments per item -->
|
|
<div class="todo-list">
|
|
<for items="todos" as="todo">
|
|
<div data-lc-fragment="todo-{todo.id}" class="todo-item">
|
|
<input
|
|
type="checkbox"
|
|
{todo.completed ? 'checked' : ''}
|
|
data-lc-action="toggleTodo"
|
|
data-lc-params='{"id": "{todo.id}"}'
|
|
/>
|
|
<span>{todo.title}</span>
|
|
</div>
|
|
</for>
|
|
</div>
|
|
```
|
|
|
|
```php
|
|
#[LiveAction]
|
|
#[Fragment] // Fragment name computed dynamically
|
|
public function toggleTodo(string $id): void
|
|
{
|
|
$todo = $this->findTodo($id);
|
|
$todo->toggle();
|
|
|
|
// Set fragment name dynamically
|
|
$this->setFragmentName("todo-{$id}");
|
|
}
|
|
```
|
|
|
|
### Fragment Events
|
|
|
|
```javascript
|
|
// Listen for fragment updates
|
|
window.addEventListener('livecomponent:fragment-updated', (e) => {
|
|
const { fragmentName, duration, nodesChanged } = e.detail;
|
|
|
|
console.log(`Fragment "${fragmentName}" updated:`);
|
|
console.log(`- Duration: ${duration}ms`);
|
|
console.log(`- Nodes changed: ${nodesChanged}`);
|
|
|
|
// Trigger custom logic after specific fragment updates
|
|
if (fragmentName === 'cart') {
|
|
updateCartBadge();
|
|
}
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## Request Batching
|
|
|
|
### Overview
|
|
|
|
Request batching automatically combines multiple rapid actions into a single HTTP request for network efficiency.
|
|
|
|
**Benefits**:
|
|
- 80-95% reduction in HTTP requests
|
|
- Lower server load
|
|
- Better mobile performance
|
|
- Reduced latency
|
|
|
|
### Automatic Batching
|
|
|
|
```javascript
|
|
// Configure batching globally
|
|
LiveComponent.configure({
|
|
batchSize: 10, // Max actions per batch
|
|
batchDebounce: 50 // Wait 50ms for more actions
|
|
});
|
|
```
|
|
|
|
**How it works**:
|
|
```
|
|
User clicks button 1 → Queue action
|
|
User clicks button 2 → Queue action } 50ms debounce window
|
|
User clicks button 3 → Queue action
|
|
↓
|
|
Single batched request with 3 actions
|
|
```
|
|
|
|
### Manual Batch Control
|
|
|
|
```javascript
|
|
// Manually flush batch immediately
|
|
LiveComponent.flushBatch('component-id');
|
|
|
|
// Pause batching temporarily
|
|
LiveComponent.pauseBatching();
|
|
|
|
// Resume batching
|
|
LiveComponent.resumeBatching();
|
|
```
|
|
|
|
### Batch-Aware Action Design
|
|
|
|
```php
|
|
// Actions designed for batching efficiency
|
|
final class ProductGrid extends LiveComponent
|
|
{
|
|
#[LiveAction]
|
|
public function quickView(string $productId): void
|
|
{
|
|
// Fast, stateless - perfect for batching
|
|
$this->activeProductId = $productId;
|
|
}
|
|
|
|
#[LiveAction]
|
|
public function addToWishlist(string $productId): void
|
|
{
|
|
// Independent operation - batches well
|
|
$this->wishlist->add($productId);
|
|
}
|
|
|
|
#[LiveAction]
|
|
public function updateFilters(array $filters): void
|
|
{
|
|
// Depends on state - still batches, processed in order
|
|
$this->filters = $filters;
|
|
$this->applyFilters();
|
|
}
|
|
}
|
|
```
|
|
|
|
### Batch Response Handling
|
|
|
|
```javascript
|
|
// Monitor batch efficiency
|
|
window.addEventListener('livecomponent:batch-sent', (e) => {
|
|
const { actionsCount, payloadSize } = e.detail;
|
|
const requestsSaved = actionsCount - 1;
|
|
const efficiency = (requestsSaved / actionsCount) * 100;
|
|
|
|
console.log(`Batch sent: ${actionsCount} actions`);
|
|
console.log(`Requests saved: ${requestsSaved}`);
|
|
console.log(`Efficiency: ${efficiency.toFixed(1)}%`);
|
|
});
|
|
|
|
// Handle batch response
|
|
window.addEventListener('livecomponent:batch-completed', (e) => {
|
|
const { results, duration } = e.detail;
|
|
|
|
// Process individual action results
|
|
results.forEach((result, index) => {
|
|
if (!result.success) {
|
|
console.error(`Action ${index} failed:`, result.error);
|
|
}
|
|
});
|
|
});
|
|
```
|
|
|
|
### Conditional Batching
|
|
|
|
```php
|
|
use App\Framework\LiveComponents\Attributes\NoBatch;
|
|
|
|
final class CriticalAction extends LiveComponent
|
|
{
|
|
// Disable batching for critical actions
|
|
#[LiveAction]
|
|
#[NoBatch]
|
|
public function processPayment(): void
|
|
{
|
|
// Always sent as standalone request
|
|
$this->paymentService->charge($this->amount);
|
|
}
|
|
|
|
// Allow batching (default)
|
|
#[LiveAction]
|
|
public function updateQuantity(string $productId, int $qty): void
|
|
{
|
|
$this->cart->updateQuantity($productId, $qty);
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Server-Sent Events (SSE)
|
|
|
|
### Overview
|
|
|
|
Server-Sent Events enable real-time, server-to-client updates without polling.
|
|
|
|
**Benefits**:
|
|
- Real-time updates without polling
|
|
- Lower bandwidth than WebSockets for one-way communication
|
|
- Automatic reconnection
|
|
- HTTP/2 multiplexing support
|
|
|
|
### Basic SSE Usage
|
|
|
|
```php
|
|
final class LiveDashboard extends LiveComponent
|
|
{
|
|
#[LiveProp]
|
|
public int $activeUsers = 0;
|
|
|
|
#[LiveProp]
|
|
public array $recentActivity = [];
|
|
|
|
public function mount(): void
|
|
{
|
|
// Enable SSE for this component
|
|
$this->enableSse();
|
|
|
|
// Subscribe to specific channels
|
|
$this->subscribeTo([
|
|
'user.activity',
|
|
'system.notifications',
|
|
"team.{$this->teamId}.updates"
|
|
]);
|
|
}
|
|
|
|
public function unmount(): void
|
|
{
|
|
// Clean up SSE connection
|
|
$this->disableSse();
|
|
}
|
|
}
|
|
```
|
|
|
|
### Broadcasting Updates
|
|
|
|
```php
|
|
use App\Framework\LiveComponents\SSE\SseBroadcaster;
|
|
|
|
final class UserActivityService
|
|
{
|
|
public function __construct(
|
|
private readonly SseBroadcaster $broadcaster
|
|
) {}
|
|
|
|
public function recordLogin(User $user): void
|
|
{
|
|
$this->repository->recordLogin($user);
|
|
|
|
// Broadcast to all listening components
|
|
$this->broadcaster->broadcast('user.activity', [
|
|
'type' => 'login',
|
|
'user_id' => $user->id,
|
|
'username' => $user->name,
|
|
'timestamp' => time()
|
|
]);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Client-Side SSE Handling
|
|
|
|
```javascript
|
|
// Listen for SSE updates
|
|
window.addEventListener('livecomponent:sse-message', (e) => {
|
|
const { channel, data } = e.detail;
|
|
|
|
console.log(`SSE message on "${channel}":`, data);
|
|
|
|
// Handle specific event types
|
|
if (data.type === 'login') {
|
|
showNotification(`${data.username} logged in`);
|
|
}
|
|
});
|
|
|
|
// Connection status
|
|
window.addEventListener('livecomponent:sse-connected', (e) => {
|
|
console.log('SSE connected:', e.detail.componentId);
|
|
showConnectionStatus('connected');
|
|
});
|
|
|
|
window.addEventListener('livecomponent:sse-disconnected', (e) => {
|
|
console.warn('SSE disconnected:', e.detail.reason);
|
|
showConnectionStatus('disconnected');
|
|
});
|
|
```
|
|
|
|
### SSE with Automatic Component Updates
|
|
|
|
```php
|
|
// Server automatically triggers component re-render
|
|
public function broadcastUpdate(string $message): void
|
|
{
|
|
$this->broadcaster->broadcast("component.{$this->id}.update", [
|
|
'trigger_render' => true, // Automatic re-render
|
|
'message' => $message
|
|
]);
|
|
}
|
|
```
|
|
|
|
### Connection Pooling
|
|
|
|
```php
|
|
use App\Framework\LiveComponents\SSE\SseConnectionPool;
|
|
|
|
final class SseOptimizedComponent extends LiveComponent
|
|
{
|
|
public function mount(): void
|
|
{
|
|
// Reuse existing connection pool
|
|
$this->ssePool->subscribe(
|
|
componentId: $this->id,
|
|
channels: ['global.updates']
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Optimistic UI Updates
|
|
|
|
### Overview
|
|
|
|
Optimistic UI updates the interface immediately before server confirmation for instant perceived performance.
|
|
|
|
**Benefits**:
|
|
- <50ms perceived latency
|
|
- Better user experience
|
|
- Higher engagement
|
|
- Network-independent responsiveness
|
|
|
|
### Simple Optimistic Updates
|
|
|
|
```html
|
|
<!-- Optimistic toggle -->
|
|
<button
|
|
data-lc-action="toggleLike"
|
|
data-optimistic="true"
|
|
>
|
|
{isLiked ? '❤️ Liked' : '🤍 Like'}
|
|
</button>
|
|
|
|
<!-- Optimistic counter -->
|
|
<button
|
|
data-lc-action="incrementCounter"
|
|
data-optimistic="increment"
|
|
>
|
|
Count: {count}
|
|
</button>
|
|
```
|
|
|
|
### Server-Side Optimistic Configuration
|
|
|
|
```php
|
|
use App\Framework\LiveComponents\Attributes\Optimistic;
|
|
|
|
final class PostInteractions extends LiveComponent
|
|
{
|
|
#[LiveProp]
|
|
public bool $isLiked = false;
|
|
|
|
#[LiveProp]
|
|
public int $likeCount = 0;
|
|
|
|
#[LiveAction]
|
|
#[Optimistic(property: 'isLiked', operation: 'toggle')]
|
|
#[Optimistic(property: 'likeCount', operation: 'increment')]
|
|
public function toggleLike(): void
|
|
{
|
|
// Client already updated UI optimistically
|
|
// Server just persists the change
|
|
|
|
$this->isLiked = !$this->isLiked;
|
|
$this->likeCount += $this->isLiked ? 1 : -1;
|
|
|
|
$this->postService->toggleLike($this->postId, $this->isLiked);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Custom Optimistic Operations
|
|
|
|
```php
|
|
#[LiveAction]
|
|
#[Optimistic(property: 'status', operation: 'custom', handler: 'optimisticStatusChange')]
|
|
public function changeStatus(string $newStatus): void
|
|
{
|
|
$this->status = $newStatus;
|
|
$this->statusService->update($this->id, $newStatus);
|
|
}
|
|
|
|
// Custom optimistic handler
|
|
public function optimisticStatusChange(string $newStatus): mixed
|
|
{
|
|
// Return optimistic value
|
|
return match($newStatus) {
|
|
'pending' => 'Processing...',
|
|
'approved' => 'Approved ✓',
|
|
'rejected' => 'Rejected ✗',
|
|
default => $newStatus
|
|
};
|
|
}
|
|
```
|
|
|
|
### Error Handling & Rollback
|
|
|
|
```php
|
|
use App\Framework\LiveComponents\Exceptions\OptimisticConflictException;
|
|
|
|
#[LiveAction]
|
|
#[Optimistic(property: 'quantity', operation: 'set')]
|
|
public function updateQuantity(int $newQuantity): void
|
|
{
|
|
// Check for conflicts
|
|
$currentStock = $this->inventoryService->getStock($this->productId);
|
|
|
|
if ($newQuantity > $currentStock) {
|
|
// Rollback optimistic update
|
|
throw new OptimisticConflictException(
|
|
"Insufficient stock. Available: {$currentStock}"
|
|
);
|
|
}
|
|
|
|
$this->quantity = $newQuantity;
|
|
$this->cartService->updateQuantity($this->productId, $newQuantity);
|
|
}
|
|
```
|
|
|
|
### Client-Side Rollback Handling
|
|
|
|
```javascript
|
|
// Handle optimistic rollback
|
|
window.addEventListener('livecomponent:optimistic-rollback', (e) => {
|
|
const { action, error, stateBeforeOptimistic } = e.detail;
|
|
|
|
// Show error to user
|
|
showNotification(error, 'error');
|
|
|
|
// Optional: Custom recovery
|
|
if (action === 'updateQuantity') {
|
|
highlightField('quantity-input');
|
|
}
|
|
});
|
|
```
|
|
|
|
### Conditional Optimistic Updates
|
|
|
|
```php
|
|
#[LiveAction]
|
|
#[Optimistic(property: 'votes', operation: 'increment', condition: 'canVote')]
|
|
public function upvote(): void
|
|
{
|
|
if (!$this->canVote()) {
|
|
throw new UnauthorizedException('Already voted');
|
|
}
|
|
|
|
$this->votes++;
|
|
$this->voteService->record($this->postId, $this->userId);
|
|
}
|
|
|
|
private function canVote(): bool
|
|
{
|
|
return !$this->hasVoted($this->userId);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Chunked File Uploads
|
|
|
|
### Overview
|
|
|
|
Chunked uploads split large files into smaller pieces for reliable, resumable uploads with progress tracking.
|
|
|
|
**Benefits**:
|
|
- Upload large files (GB+)
|
|
- Pause/resume support
|
|
- Progress tracking
|
|
- Retry failed chunks
|
|
- Validation before upload
|
|
|
|
### Basic Upload Configuration
|
|
|
|
```html
|
|
<form>
|
|
<input
|
|
type="file"
|
|
data-lc-upload="handleFileUpload"
|
|
data-chunk-size="1048576"
|
|
data-max-file-size="104857600"
|
|
accept=".pdf,.jpg,.png"
|
|
/>
|
|
|
|
<div class="upload-progress" data-upload-progress>
|
|
<div class="progress-bar" style="width: {uploadProgress}%"></div>
|
|
<span>{uploadProgress}%</span>
|
|
<button data-lc-action="pauseUpload">Pause</button>
|
|
<button data-lc-action="resumeUpload">Resume</button>
|
|
<button data-lc-action="cancelUpload">Cancel</button>
|
|
</div>
|
|
</form>
|
|
```
|
|
|
|
### Server-Side Upload Handling
|
|
|
|
```php
|
|
use App\Framework\LiveComponents\Attributes\LiveAction;
|
|
use App\Framework\LiveComponents\Upload\ChunkUploadHandler;
|
|
|
|
final class FileUploadComponent extends LiveComponent
|
|
{
|
|
#[LiveProp]
|
|
public float $uploadProgress = 0.0;
|
|
|
|
#[LiveProp]
|
|
public ?string $uploadId = null;
|
|
|
|
#[LiveProp]
|
|
public bool $uploadPaused = false;
|
|
|
|
#[LiveAction]
|
|
public function handleFileUpload(
|
|
string $uploadId,
|
|
int $chunkIndex,
|
|
int $totalChunks,
|
|
string $filename,
|
|
string $chunkData
|
|
): void {
|
|
$this->uploadId = $uploadId;
|
|
|
|
// Store chunk
|
|
$this->chunkUploadHandler->storeChunk(
|
|
uploadId: $uploadId,
|
|
chunkIndex: $chunkIndex,
|
|
data: base64_decode($chunkData)
|
|
);
|
|
|
|
// Update progress
|
|
$this->uploadProgress = ($chunkIndex + 1) / $totalChunks * 100;
|
|
|
|
// Last chunk - assemble file
|
|
if ($chunkIndex === $totalChunks - 1) {
|
|
$finalPath = $this->chunkUploadHandler->assembleFile(
|
|
uploadId: $uploadId,
|
|
filename: $filename,
|
|
totalChunks: $totalChunks
|
|
);
|
|
|
|
$this->processUploadedFile($finalPath);
|
|
}
|
|
}
|
|
|
|
#[LiveAction]
|
|
public function pauseUpload(): void
|
|
{
|
|
$this->uploadPaused = true;
|
|
}
|
|
|
|
#[LiveAction]
|
|
public function resumeUpload(): void
|
|
{
|
|
$this->uploadPaused = false;
|
|
}
|
|
|
|
#[LiveAction]
|
|
public function cancelUpload(): void
|
|
{
|
|
if ($this->uploadId) {
|
|
$this->chunkUploadHandler->cleanup($this->uploadId);
|
|
$this->uploadId = null;
|
|
$this->uploadProgress = 0.0;
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Client-Side Upload Progress
|
|
|
|
```javascript
|
|
// Track upload progress
|
|
window.addEventListener('livecomponent:upload-progress', (e) => {
|
|
const { uploadId, progress, chunkIndex, totalChunks } = e.detail;
|
|
|
|
console.log(`Upload ${uploadId}: ${progress}%`);
|
|
console.log(`Chunk ${chunkIndex + 1} of ${totalChunks}`);
|
|
|
|
// Update UI
|
|
updateProgressBar(progress);
|
|
});
|
|
|
|
// Upload complete
|
|
window.addEventListener('livecomponent:upload-complete', (e) => {
|
|
const { uploadId, filename, fileSize } = e.detail;
|
|
|
|
showNotification(`Upload complete: ${filename}`);
|
|
});
|
|
|
|
// Upload error
|
|
window.addEventListener('livecomponent:upload-error', (e) => {
|
|
const { uploadId, error, chunkIndex } = e.detail;
|
|
|
|
console.error(`Upload failed at chunk ${chunkIndex}:`, error);
|
|
|
|
// Retry logic
|
|
if (e.detail.retryable) {
|
|
retryUpload(uploadId, chunkIndex);
|
|
}
|
|
});
|
|
```
|
|
|
|
### Resumable Uploads
|
|
|
|
```php
|
|
final class ResumableUploadHandler
|
|
{
|
|
public function resumeUpload(string $uploadId): UploadState
|
|
{
|
|
// Get upload state from storage
|
|
$state = $this->uploadStateRepository->find($uploadId);
|
|
|
|
if (!$state) {
|
|
throw new UploadNotFoundException($uploadId);
|
|
}
|
|
|
|
// Return chunks that still need uploading
|
|
$uploadedChunks = $this->getUploadedChunks($uploadId);
|
|
$remainingChunks = array_diff(
|
|
range(0, $state->totalChunks - 1),
|
|
$uploadedChunks
|
|
);
|
|
|
|
return new UploadState(
|
|
uploadId: $uploadId,
|
|
filename: $state->filename,
|
|
totalChunks: $state->totalChunks,
|
|
uploadedChunks: $uploadedChunks,
|
|
remainingChunks: $remainingChunks
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Component Communication
|
|
|
|
### Parent-Child Communication
|
|
|
|
```php
|
|
// Parent component
|
|
final class Dashboard extends LiveComponent
|
|
{
|
|
#[LiveProp]
|
|
public array $childData = [];
|
|
|
|
#[LiveAction]
|
|
public function updateChild(array $data): void
|
|
{
|
|
$this->childData = $data;
|
|
|
|
// Broadcast update to child component
|
|
$this->emit('child-component-id', 'dataUpdated', $data);
|
|
}
|
|
}
|
|
|
|
// Child component
|
|
final class Widget extends LiveComponent
|
|
{
|
|
public function mount(): void
|
|
{
|
|
// Listen for parent events
|
|
$this->on('dataUpdated', fn($data) => $this->handleUpdate($data));
|
|
}
|
|
|
|
private function handleUpdate(array $data): void
|
|
{
|
|
$this->processData($data);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Sibling Communication
|
|
|
|
```php
|
|
// Component A
|
|
#[LiveAction]
|
|
public function notifySibling(string $message): void
|
|
{
|
|
// Broadcast to specific component
|
|
$this->broadcast('component-b-id', 'messageReceived', [
|
|
'message' => $message,
|
|
'sender' => $this->id
|
|
]);
|
|
}
|
|
|
|
// Component B
|
|
public function mount(): void
|
|
{
|
|
$this->on('messageReceived', function($data) {
|
|
$this->handleMessage($data['message'], $data['sender']);
|
|
});
|
|
}
|
|
```
|
|
|
|
### Global Events
|
|
|
|
```php
|
|
// Broadcast to all components
|
|
final class GlobalNotification extends LiveComponent
|
|
{
|
|
#[LiveAction]
|
|
public function sendNotification(string $message): void
|
|
{
|
|
$this->broadcastGlobal('notification.new', [
|
|
'message' => $message,
|
|
'timestamp' => time()
|
|
]);
|
|
}
|
|
}
|
|
|
|
// Any component can listen
|
|
public function mount(): void
|
|
{
|
|
$this->onGlobal('notification.new', function($data) {
|
|
$this->notifications[] = $data;
|
|
});
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## State Management Patterns
|
|
|
|
### Computed Properties
|
|
|
|
```php
|
|
final class ShoppingCart extends LiveComponent
|
|
{
|
|
#[LiveProp]
|
|
public array $items = [];
|
|
|
|
// Computed property (not serialized)
|
|
public function getSubtotal(): float
|
|
{
|
|
return array_reduce(
|
|
$this->items,
|
|
fn($total, $item) => $total + ($item['price'] * $item['quantity']),
|
|
0.0
|
|
);
|
|
}
|
|
|
|
public function getTax(): float
|
|
{
|
|
return $this->getSubtotal() * 0.19;
|
|
}
|
|
|
|
public function getTotal(): float
|
|
{
|
|
return $this->getSubtotal() + $this->getTax();
|
|
}
|
|
}
|
|
```
|
|
|
|
### Derived State
|
|
|
|
```php
|
|
final class FilteredList extends LiveComponent
|
|
{
|
|
#[LiveProp]
|
|
public array $allItems = [];
|
|
|
|
#[LiveProp]
|
|
public string $searchTerm = '';
|
|
|
|
#[LiveProp]
|
|
public array $filters = [];
|
|
|
|
// Derived state
|
|
private array $filteredItems = [];
|
|
|
|
public function updated(string $property, mixed $oldValue, mixed $newValue): void
|
|
{
|
|
if (in_array($property, ['searchTerm', 'filters'])) {
|
|
$this->recalculateFilteredItems();
|
|
}
|
|
}
|
|
|
|
private function recalculateFilteredItems(): void
|
|
{
|
|
$this->filteredItems = array_filter(
|
|
$this->allItems,
|
|
fn($item) => $this->matchesFilters($item)
|
|
);
|
|
}
|
|
|
|
public function render(): string
|
|
{
|
|
return $this->view('filtered-list', [
|
|
'items' => $this->filteredItems
|
|
]);
|
|
}
|
|
}
|
|
```
|
|
|
|
### State Persistence
|
|
|
|
```php
|
|
use App\Framework\LiveComponents\Attributes\Persisted;
|
|
|
|
final class UserPreferences extends LiveComponent
|
|
{
|
|
#[LiveProp]
|
|
#[Persisted(storage: 'session', key: 'user.preferences')]
|
|
public array $preferences = [];
|
|
|
|
#[LiveAction]
|
|
public function updatePreference(string $key, mixed $value): void
|
|
{
|
|
$this->preferences[$key] = $value;
|
|
// Automatically persisted to session
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Custom Actions
|
|
|
|
### Action Middleware
|
|
|
|
```php
|
|
use App\Framework\LiveComponents\Middleware\ActionMiddleware;
|
|
|
|
final class LoggingMiddleware implements ActionMiddleware
|
|
{
|
|
public function before(
|
|
LiveComponent $component,
|
|
string $action,
|
|
array $params
|
|
): void {
|
|
$this->logger->info("Action starting: {$action}", [
|
|
'component' => get_class($component),
|
|
'params' => $params
|
|
]);
|
|
}
|
|
|
|
public function after(
|
|
LiveComponent $component,
|
|
string $action,
|
|
mixed $result
|
|
): void {
|
|
$this->logger->info("Action completed: {$action}");
|
|
}
|
|
}
|
|
```
|
|
|
|
### Custom Action Attributes
|
|
|
|
```php
|
|
use App\Framework\LiveComponents\Attributes\CustomAction;
|
|
|
|
#[CustomAction(
|
|
name: 'tracked',
|
|
handler: TrackingActionHandler::class
|
|
)]
|
|
#[LiveAction]
|
|
public function importantAction(): void
|
|
{
|
|
// Action automatically tracked by TrackingActionHandler
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
**Next**: [Best Practices](best-practices/component-design.md) →
|