- 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.
34 KiB
LiveComponents Attributes Reference
Complete reference for all LiveComponents attributes with examples and best practices.
Table of Contents
- Component Attributes
- Security Attributes
- Performance Attributes
- Validation Attributes
- State Management Attributes
Component Attributes
@LiveProp
Purpose: Mark a component property as reactive - automatically synchronized between server and client.
Signature:
#[LiveProp(
writable: bool = false, // Allow client-side updates
lazy: bool = false, // Load on first access
computed: bool = false, // Computed from other props
dehydrate: ?callable = null, // Custom serialization
hydrate: ?callable = null // Custom deserialization
)]
Basic Usage:
final class Counter extends LiveComponent
{
#[LiveProp]
public int $count = 0;
#[LiveProp(writable: true)]
public string $searchQuery = '';
#[LiveProp(lazy: true)]
public array $expensiveData = [];
#[LiveAction]
public function increment(): void
{
$this->count++;
}
}
Writable Props (Two-Way Binding):
final class SearchForm extends LiveComponent
{
#[LiveProp(writable: true)]
public string $query = '';
#[LiveProp(writable: true)]
public array $filters = [];
#[LiveAction]
public function search(): void
{
// $this->query automatically updated from client
$this->results = $this->searchService->search($this->query, $this->filters);
}
}
HTML Template:
<div data-lc-component="SearchForm">
<!-- Automatically synced to server on change -->
<input
type="text"
data-lc-model="query"
placeholder="Search..."
/>
<!-- Current value: {{ query }} -->
</div>
Lazy Props (Load on Demand):
final class Dashboard extends LiveComponent
{
#[LiveProp]
public string $userId;
#[LiveProp(lazy: true)]
public array $analytics = [];
#[LiveAction]
public function loadAnalytics(): void
{
// Only load when explicitly requested
$this->analytics = $this->analyticsService->getForUser($this->userId);
}
}
Computed Props:
final class ShoppingCart extends LiveComponent
{
#[LiveProp]
public array $items = [];
#[LiveProp(computed: true)]
public function total(): Money
{
return array_reduce(
$this->items,
fn($total, $item) => $total->add($item->price),
Money::zero()
);
}
}
Custom Serialization:
final class DateRangePicker extends LiveComponent
{
#[LiveProp(
dehydrate: [self::class, 'dehydrateDate'],
hydrate: [self::class, 'hydrateDate']
)]
public \DateTimeImmutable $startDate;
public static function dehydrateDate(\DateTimeImmutable $date): string
{
return $date->format('Y-m-d');
}
public static function hydrateDate(string $date): \DateTimeImmutable
{
return new \DateTimeImmutable($date);
}
}
Best Practices:
- ✅ Use
writable: trueonly for user input fields - ✅ Use
lazy: truefor expensive data that's not always needed - ✅ Keep props serializable (primitives, arrays, or custom dehydrate/hydrate)
- ❌ Don't make service dependencies
#[LiveProp] - ❌ Don't expose sensitive data as LiveProps
Gotchas:
- Writable props validate on server before updating
- Computed props are read-only on client
- Lazy props won't be available until explicitly loaded
@LiveAction
Purpose: Mark a method as a server-side action that can be triggered from the client.
Signature:
#[LiveAction(
name: ?string = null, // Custom action name
debounce: ?int = null, // Debounce in milliseconds
throttle: ?int = null, // Throttle in milliseconds
confirm: ?string = null // Confirmation message
)]
Basic Usage:
final class TodoList extends LiveComponent
{
#[LiveProp]
public array $todos = [];
#[LiveAction]
public function addTodo(string $text): void
{
$this->todos[] = [
'id' => uniqid(),
'text' => $text,
'done' => false
];
}
#[LiveAction]
public function toggleTodo(string $id): void
{
foreach ($this->todos as &$todo) {
if ($todo['id'] === $id) {
$todo['done'] = !$todo['done'];
}
}
}
#[LiveAction]
public function removeTodo(string $id): void
{
$this->todos = array_filter(
$this->todos,
fn($todo) => $todo['id'] !== $id
);
}
}
HTML Template:
<div data-lc-component="TodoList">
<ul>
<for items="todos" as="todo">
<li>
<input
type="checkbox"
data-lc-action="toggleTodo"
data-lc-params='{"id": "{{ todo.id }}"}'
{checked:todo.done}
/>
{{ todo.text }}
<button
data-lc-action="removeTodo"
data-lc-params='{"id": "{{ todo.id }}"}'
>
Delete
</button>
</li>
</for>
</ul>
</div>
Debounced Actions (Search Input):
final class SearchComponent extends LiveComponent
{
#[LiveProp(writable: true)]
public string $query = '';
#[LiveProp]
public array $results = [];
#[LiveAction(debounce: 300)]
public function search(): void
{
// Only executes 300ms after user stops typing
$this->results = $this->searchService->search($this->query);
}
}
Throttled Actions (Rate Limited):
final class AnalyticsTracker extends LiveComponent
{
#[LiveAction(throttle: 1000)]
public function trackEvent(string $eventName): void
{
// Maximum once per second
$this->analyticsService->track($eventName);
}
}
Confirmation Actions:
final class UserManager extends LiveComponent
{
#[LiveAction(confirm: 'Are you sure you want to delete this user?')]
public function deleteUser(string $userId): void
{
$this->userService->delete($userId);
}
}
Custom Action Names:
final class PaymentForm extends LiveComponent
{
#[LiveAction(name: 'submit-payment')]
public function processPayment(array $paymentData): void
{
$this->paymentService->process($paymentData);
}
}
Best Practices:
- ✅ Use debounce for search/autocomplete
- ✅ Use throttle for tracking/analytics
- ✅ Use confirm for destructive actions
- ✅ Keep actions focused (single responsibility)
- ❌ Don't perform long-running operations (use jobs)
- ❌ Don't return data (update LiveProps instead)
Gotchas:
- Actions automatically re-render component by default
- Debounce/throttle applied client-side before request
- Confirmation shows native browser confirm dialog
@Fragment
Purpose: Mark an action to update only specific fragments of the component (partial rendering).
Signature:
#[Fragment(
string|array $names // Fragment name(s) to update
)]
Basic Usage:
final class Dashboard extends LiveComponent
{
#[LiveProp]
public array $stats = [];
#[LiveProp]
public array $notifications = [];
#[LiveAction]
#[Fragment('stats')]
public function refreshStats(): void
{
// Only re-renders the 'stats' fragment
$this->stats = $this->statsService->getLatest();
}
#[LiveAction]
#[Fragment('notifications')]
public function refreshNotifications(): void
{
// Only re-renders the 'notifications' fragment
$this->notifications = $this->notificationService->getUnread();
}
#[LiveAction]
#[Fragment(['stats', 'notifications'])]
public function refreshAll(): void
{
// Re-renders both fragments
$this->stats = $this->statsService->getLatest();
$this->notifications = $this->notificationService->getUnread();
}
}
HTML Template:
<div data-lc-component="Dashboard">
<div data-lc-fragment="stats">
<h2>Statistics</h2>
<for items="stats" as="stat">
<div>{{ stat.label }}: {{ stat.value }}</div>
</for>
<button data-lc-action="refreshStats">Refresh Stats</button>
</div>
<div data-lc-fragment="notifications">
<h2>Notifications</h2>
<for items="notifications" as="notification">
<div>{{ notification.message }}</div>
</for>
<button data-lc-action="refreshNotifications">Refresh Notifications</button>
</div>
<button data-lc-action="refreshAll">Refresh Everything</button>
</div>
Nested Fragments:
final class ProductList extends LiveComponent
{
#[LiveProp]
public array $products = [];
#[LiveProp]
public array $filters = [];
#[LiveAction]
#[Fragment('product-list')]
public function applyFilter(string $category): void
{
$this->filters['category'] = $category;
$this->products = $this->productService->search($this->filters);
}
#[LiveAction]
#[Fragment('product-item')]
public function toggleFavorite(string $productId): void
{
$this->productService->toggleFavorite($productId);
}
}
HTML Template with Nested Fragments:
<div data-lc-component="ProductList">
<div data-lc-fragment="product-list">
<for items="products" as="product">
<div data-lc-fragment="product-item" data-product-id="{{ product.id }}">
<h3>{{ product.name }}</h3>
<button
data-lc-action="toggleFavorite"
data-lc-params='{"productId": "{{ product.id }}"}'
>
{favorite:product.isFavorite}
</button>
</div>
</for>
</div>
</div>
Performance Benefits:
// Without fragments (full re-render)
#[LiveAction]
public function updateSmallPart(): void
{
// Re-renders entire 10KB template
}
// With fragments (partial re-render)
#[LiveAction]
#[Fragment('small-part')]
public function updateSmallPart(): void
{
// Only re-renders 500 bytes
// 95% network reduction!
}
Best Practices:
- ✅ Use fragments for large components with independent sections
- ✅ Name fragments semantically (
user-profile, notfragment-1) - ✅ Keep fragments independent (minimal cross-dependencies)
- ✅ Use nested fragments for list items
- ❌ Don't create too many small fragments (overhead)
- ❌ Don't use fragments for tiny components (< 1KB)
Gotchas:
- Fragment names must match exactly between PHP and HTML
- Nested fragments require unique identifiers (e.g.,
data-product-id) - Missing fragment in template causes full re-render
Security Attributes
@RateLimit
Purpose: Limit the rate at which an action can be executed to prevent abuse.
Signature:
#[RateLimit(
int $requests, // Number of allowed requests
int $window, // Time window in seconds
string $key = 'ip' // Rate limit key ('ip', 'user', 'component')
)]
IP-Based Rate Limiting:
final class ContactForm extends LiveComponent
{
#[LiveAction]
#[RateLimit(requests: 3, window: 3600, key: 'ip')]
public function submitForm(array $formData): void
{
// Max 3 submissions per hour per IP address
$this->emailService->send($formData);
}
}
User-Based Rate Limiting:
final class ApiExplorer extends LiveComponent
{
#[LiveAction]
#[RateLimit(requests: 100, window: 60, key: 'user')]
public function executeQuery(string $query): void
{
// Max 100 queries per minute per authenticated user
$this->results = $this->apiService->query($query);
}
}
Component-Based Rate Limiting:
final class ExpensiveReport extends LiveComponent
{
#[LiveAction]
#[RateLimit(requests: 5, window: 300, key: 'component')]
public function generateReport(): void
{
// Max 5 reports per 5 minutes for this component instance
$this->report = $this->reportService->generate();
}
}
Multiple Rate Limits:
final class PaymentForm extends LiveComponent
{
#[LiveAction]
#[RateLimit(requests: 3, window: 60, key: 'ip')]
#[RateLimit(requests: 10, window: 3600, key: 'user')]
public function processPayment(array $paymentData): void
{
// IP: Max 3 per minute
// User: Max 10 per hour
$this->paymentService->process($paymentData);
}
}
Best Practices:
- ✅ Use
key: 'ip'for public endpoints (contact forms, registrations) - ✅ Use
key: 'user'for authenticated endpoints - ✅ Use
key: 'component'for expensive operations - ✅ Set generous limits to avoid false positives
- ❌ Don't rate limit critical user flows too aggressively
Gotchas:
- Rate limit exceeded returns 429 Too Many Requests
- Client automatically shows retry-after countdown
- Rate limits are per-action (not per-component)
@Authorize
Purpose: Restrict action execution to users with specific roles or permissions.
Signature:
#[Authorize(
array $roles = [], // Required roles (any match)
array $permissions = [], // Required permissions (all match)
bool $requireAll = false // Require all roles (default: any)
)]
Role-Based Authorization:
final class UserManagement extends LiveComponent
{
#[LiveAction]
#[Authorize(roles: ['admin'])]
public function deleteUser(string $userId): void
{
// Only admins can delete users
$this->userService->delete($userId);
}
#[LiveAction]
#[Authorize(roles: ['admin', 'moderator'])]
public function banUser(string $userId): void
{
// Admins OR moderators can ban
$this->userService->ban($userId);
}
}
Permission-Based Authorization:
final class DocumentEditor extends LiveComponent
{
#[LiveAction]
#[Authorize(permissions: ['documents.edit'])]
public function saveDocument(string $content): void
{
$this->documentService->save($content);
}
#[LiveAction]
#[Authorize(permissions: ['documents.delete'])]
public function deleteDocument(string $documentId): void
{
$this->documentService->delete($documentId);
}
}
Combined Authorization:
final class BillingPanel extends LiveComponent
{
#[LiveAction]
#[Authorize(
roles: ['admin', 'billing'],
permissions: ['billing.process-refund'],
requireAll: true
)]
public function processRefund(string $orderId): void
{
// Must have (admin OR billing role) AND billing.process-refund permission
$this->billingService->refund($orderId);
}
}
Dynamic Authorization:
final class ProjectEditor extends LiveComponent
{
#[LiveProp]
public string $projectId;
#[LiveAction]
public function saveProject(array $data): void
{
// Custom authorization logic
if (!$this->authService->canEditProject($this->projectId)) {
throw new UnauthorizedException('You cannot edit this project');
}
$this->projectService->save($this->projectId, $data);
}
}
Best Practices:
- ✅ Use roles for broad access control
- ✅ Use permissions for fine-grained control
- ✅ Combine roles and permissions for complex requirements
- ✅ Show/hide UI elements based on authorization
- ❌ Don't rely solely on client-side authorization
Gotchas:
- Authorization failure returns 403 Forbidden
- Unauthorized actions don't execute (state unchanged)
- Client-side UI should match server-side authorization
@Idempotent
Purpose: Ensure an action can be safely retried without duplicate side effects.
Signature:
#[Idempotent(
string $keyParam = 'idempotencyKey', // Parameter name for idempotency key
int $ttl = 3600 // TTL in seconds (default: 1 hour)
)]
Basic Usage:
final class OrderForm extends LiveComponent
{
#[LiveAction]
#[Idempotent]
public function submitOrder(string $idempotencyKey, array $orderData): void
{
// Duplicate requests with same idempotencyKey return cached result
$order = $this->orderService->create($orderData);
$this->orderId = $order->id;
}
}
HTML Template:
<div data-lc-component="OrderForm">
<form data-lc-action="submitOrder">
<!-- Idempotency key automatically generated -->
<input type="hidden" name="idempotencyKey" data-lc-idempotency />
<button type="submit">Submit Order</button>
</form>
</div>
Custom Key Parameter:
final class PaymentProcessor extends LiveComponent
{
#[LiveAction]
#[Idempotent(keyParam: 'transactionId', ttl: 7200)]
public function processPayment(string $transactionId, array $paymentData): void
{
// Uses 'transactionId' as idempotency key
// Cached for 2 hours
$this->paymentService->process($transactionId, $paymentData);
}
}
Use Cases:
- ✅ Order submissions
- ✅ Payment processing
- ✅ Email sending
- ✅ API calls to external services
- ✅ Database writes
Best Practices:
- ✅ Use for all non-idempotent operations (CREATE, DELETE)
- ✅ Use longer TTL for critical operations (payments: 24 hours)
- ✅ Generate idempotency keys client-side (UUID v4)
- ❌ Don't use for read-only operations (unnecessary overhead)
Gotchas:
- Idempotent actions return cached response on retry
- TTL determines how long duplicates are prevented
- Client must provide idempotency key (automatic with
data-lc-idempotency)
@Encrypted
Purpose: Automatically encrypt and decrypt sensitive LiveProp values.
Signature:
#[Encrypted(
string $algorithm = 'aes-256-gcm' // Encryption algorithm
)]
Basic Usage:
final class PaymentForm extends LiveComponent
{
#[LiveProp]
public string $orderId;
#[LiveProp]
#[Encrypted]
public string $creditCardNumber = '';
#[LiveProp]
#[Encrypted]
public string $cvv = '';
#[LiveAction]
public function processPayment(): void
{
// Properties automatically decrypted here
$this->paymentService->charge(
$this->creditCardNumber,
$this->cvv
);
}
}
Multiple Encrypted Props:
final class UserProfile extends LiveComponent
{
#[LiveProp]
public string $userId;
#[LiveProp]
#[Encrypted]
public string $ssn = '';
#[LiveProp]
#[Encrypted]
public string $bankAccount = '';
#[LiveProp]
#[Encrypted]
public array $sensitiveNotes = [];
}
Best Practices:
- ✅ Encrypt all PII (personally identifiable information)
- ✅ Encrypt financial data (credit cards, bank accounts)
- ✅ Encrypt authentication tokens
- ✅ Use in combination with HTTPS
- ❌ Don't encrypt non-sensitive data (performance overhead)
- ❌ Don't rely solely on encryption (validate and sanitize)
Gotchas:
- Encrypted props increase payload size (~30%)
- Encryption/decryption adds ~5ms latency
- Requires LIVECOMPONENT_ENCRYPTION_KEY in .env
Performance Attributes
@Optimistic
Purpose: Configure optimistic UI updates for instant perceived performance.
Signature:
#[Optimistic(
bool $enabled = true, // Enable optimistic updates
array $updateProps = [], // Props to update optimistically
?string $rollbackOn = null // Rollback condition
)]
Basic Usage:
final class LikeButton extends LiveComponent
{
#[LiveProp]
public int $likes = 0;
#[LiveProp]
public bool $isLiked = false;
#[LiveAction]
#[Optimistic(updateProps: ['likes', 'isLiked'])]
public function toggleLike(): void
{
// Client updates UI immediately
// Server confirms asynchronously
if ($this->isLiked) {
$this->likes--;
$this->isLiked = false;
} else {
$this->likes++;
$this->isLiked = true;
}
}
}
HTML Template:
<div data-lc-component="LikeButton">
<button
data-lc-action="toggleLike"
data-lc-optimistic='{"likes": {{ likes + 1 }}, "isLiked": true}'
>
❤️ {{ likes }} {isLiked ? 'Unlike' : 'Like'}
</button>
</div>
Conditional Rollback:
final class InventoryManager extends LiveComponent
{
#[LiveProp]
public int $stock = 100;
#[LiveAction]
#[Optimistic(
updateProps: ['stock'],
rollbackOn: 'stock < 0'
)]
public function reserveItem(int $quantity): void
{
if ($this->stock < $quantity) {
throw new InsufficientStockException();
}
$this->stock -= $quantity;
}
}
Best Practices:
- ✅ Use for user interactions (likes, votes, favorites)
- ✅ Use for non-critical updates
- ✅ Provide visual feedback for rollbacks
- ❌ Don't use for financial transactions
- ❌ Don't use for destructive actions
Gotchas:
- Optimistic updates happen instantly (<50ms)
- Server validation can trigger rollback
- Network failures automatically rollback
@Cached
Purpose: Cache action results to improve performance for expensive operations.
Signature:
#[Cached(
int $ttl = 3600, // Cache TTL in seconds
string $key = 'auto', // Cache key strategy
array $tags = [] // Cache tags for invalidation
)]
Basic Usage:
final class ReportViewer extends LiveComponent
{
#[LiveProp]
public string $reportId;
#[LiveProp]
public array $reportData = [];
#[LiveAction]
#[Cached(ttl: 1800)]
public function loadReport(): void
{
// Expensive operation cached for 30 minutes
$this->reportData = $this->reportService->generate($this->reportId);
}
}
Tagged Caching:
final class ProductCatalog extends LiveComponent
{
#[LiveProp]
public string $categoryId;
#[LiveProp]
public array $products = [];
#[LiveAction]
#[Cached(ttl: 3600, tags: ['products', 'category:{categoryId}'])]
public function loadProducts(): void
{
$this->products = $this->productService->getByCategory($this->categoryId);
}
#[LiveAction]
public function invalidateCache(): void
{
// Invalidate all product caches
$this->cache->invalidateTags(['products']);
}
}
Custom Cache Keys:
final class UserDashboard extends LiveComponent
{
#[LiveProp]
public string $userId;
#[LiveAction]
#[Cached(ttl: 600, key: 'user:{userId}:dashboard')]
public function loadDashboard(): void
{
// Cache key: "user:123:dashboard"
$this->dashboardData = $this->dashboardService->getForUser($this->userId);
}
}
Best Practices:
- ✅ Cache expensive database queries
- ✅ Cache external API calls
- ✅ Use tags for batch invalidation
- ✅ Set appropriate TTL based on data freshness needs
- ❌ Don't cache user-specific sensitive data
- ❌ Don't cache highly dynamic data
Gotchas:
- Cache invalidation is eventual (not immediate)
- Tagged invalidation requires cache driver support
- Cache misses execute full action
@NoBatch
Purpose: Disable automatic request batching for an action.
Signature:
#[NoBatch]
Basic Usage:
final class FileUploader extends LiveComponent
{
#[LiveAction]
#[NoBatch]
public function uploadFile(UploadedFile $file): void
{
// File uploads should not be batched
$this->uploadedFileId = $this->uploadService->store($file);
}
#[LiveAction]
public function deleteFile(string $fileId): void
{
// Regular action - can be batched
$this->uploadService->delete($fileId);
}
}
Use Cases:
- ✅ File uploads
- ✅ Real-time operations
- ✅ Time-sensitive actions
- ✅ Operations requiring immediate response
Best Practices:
- ✅ Use for operations that can't be delayed
- ✅ Use for operations with large payloads
- ❌ Don't overuse (batching improves performance)
Gotchas:
- Disables batching only for this specific action
- Other actions in the same component can still batch
- Increases number of HTTP requests
Validation Attributes
@Validated
Purpose: Automatically validate action parameters before execution.
Signature:
#[Validated(
array $rules = [], // Validation rules per parameter
?string $errorFragment = null // Fragment to update on validation error
)]
Basic Usage:
final class RegistrationForm extends LiveComponent
{
#[LiveProp]
public array $errors = [];
#[LiveAction]
#[Validated(
rules: [
'email' => ['required', 'email', 'unique:users'],
'password' => ['required', 'min:8', 'confirmed'],
'username' => ['required', 'alphanumeric', 'min:3', 'max:20']
],
errorFragment: 'form-errors'
)]
public function register(string $email, string $password, string $username): void
{
// Parameters automatically validated
$user = $this->authService->register($email, $password, $username);
$this->redirect('/dashboard');
}
}
HTML Template:
<div data-lc-component="RegistrationForm">
<form data-lc-action="register">
<div>
<input type="email" name="email" required />
<if condition="errors.email">
<span class="error">{{ errors.email }}</span>
</if>
</div>
<div>
<input type="password" name="password" required />
<if condition="errors.password">
<span class="error">{{ errors.password }}</span>
</if>
</div>
<div>
<input type="text" name="username" required />
<if condition="errors.username">
<span class="error">{{ errors.username }}</span>
</if>
</div>
<button type="submit">Register</button>
</form>
<div data-lc-fragment="form-errors">
<if condition="errors">
<div class="alert alert-error">
Please correct the errors above.
</div>
</if>
</div>
</div>
Available Validation Rules:
required- Field must be present and non-emptyemail- Valid email formatmin:n- Minimum length/valuemax:n- Maximum length/valuenumeric- Numeric valuealphanumeric- Alphanumeric characters onlyunique:table,column- Unique in databaseexists:table,column- Exists in databaseconfirmed- Match confirmation field (password_confirmation)regex:pattern- Match regex patternin:foo,bar,baz- Value in listurl- Valid URL formatdate- Valid date format
Custom Validation:
final class ProductForm extends LiveComponent
{
#[LiveAction]
public function saveProduct(array $productData): void
{
// Custom validation logic
$validator = new ProductValidator();
if (!$validator->validate($productData)) {
$this->errors = $validator->getErrors();
return; // Stop execution
}
$this->productService->save($productData);
}
}
Best Practices:
- ✅ Validate all user input
- ✅ Use appropriate rules for each field type
- ✅ Provide clear error messages
- ✅ Use
errorFragmentto update only error section - ❌ Don't trust client-side validation alone
Gotchas:
- Validation errors prevent action execution
- Errors automatically populate
$errorsproperty - Validation runs before authorization checks
State Management Attributes
@Persisted
Purpose: Automatically persist component state across requests/sessions.
Signature:
#[Persisted(
string $storage = 'session', // Storage type ('session', 'cookie', 'database')
int $ttl = 3600, // TTL in seconds
array $props = [] // Specific props to persist (default: all LiveProps)
)]
Session Persistence:
final class ShoppingCart extends LiveComponent
{
#[LiveProp]
#[Persisted(storage: 'session')]
public array $items = [];
#[LiveAction]
public function addItem(string $productId, int $quantity): void
{
// Cart persists across page navigations
$this->items[] = [
'product_id' => $productId,
'quantity' => $quantity
];
}
}
Cookie Persistence:
final class ThemeSwitcher extends LiveComponent
{
#[LiveProp]
#[Persisted(storage: 'cookie', ttl: 31536000)]
public string $theme = 'light';
#[LiveAction]
public function toggleTheme(): void
{
// Theme persists for 1 year
$this->theme = $this->theme === 'light' ? 'dark' : 'light';
}
}
Database Persistence:
final class DraftEditor extends LiveComponent
{
#[LiveProp]
public string $documentId;
#[LiveProp]
#[Persisted(storage: 'database', ttl: 86400)]
public string $draftContent = '';
#[LiveAction]
public function autosave(string $content): void
{
// Draft persists for 24 hours
$this->draftContent = $content;
}
}
Selective Persistence:
final class FilterPanel extends LiveComponent
{
#[LiveProp]
public array $filters = [];
#[LiveProp]
public array $results = [];
#[Persisted(storage: 'session', props: ['filters'])]
public function mount(): void
{
// Only 'filters' persisted, not 'results'
}
}
Best Practices:
- ✅ Use session for temporary state (shopping carts)
- ✅ Use cookies for user preferences (theme, locale)
- ✅ Use database for long-term drafts
- ✅ Set appropriate TTL based on data sensitivity
- ❌ Don't persist large amounts of data
- ❌ Don't persist sensitive data in cookies
Gotchas:
- Session storage cleared on logout
- Cookie storage limited to 4KB
- Database storage requires additional queries
Combining Attributes
Multiple Attributes on Single Action:
final class PaymentProcessor extends LiveComponent
{
#[LiveAction]
#[RateLimit(requests: 3, window: 3600, key: 'user')]
#[Authorize(permissions: ['payments.process'])]
#[Idempotent(ttl: 86400)]
#[Validated(rules: [
'amount' => ['required', 'numeric', 'min:1'],
'currency' => ['required', 'in:USD,EUR,GBP']
])]
#[Fragment('payment-result')]
public function processPayment(
string $idempotencyKey,
float $amount,
string $currency
): void {
// Secure, rate-limited, idempotent, validated payment processing
$this->paymentService->charge($amount, $currency);
}
}
Execution Order:
- @RateLimit - Check rate limits first
- @Authorize - Check permissions
- @Validated - Validate parameters
- @Idempotent - Check for duplicate requests
- @Cached - Check cache (if applicable)
- Action Execution - Execute action logic
- @Fragment - Update specific fragments
- @Optimistic - Apply optimistic updates
- @Persisted - Persist state changes
Summary Table
| Attribute | Purpose | Performance Impact | Use Case |
|---|---|---|---|
@LiveProp |
Reactive properties | Low | State synchronization |
@LiveAction |
Server actions | Low | User interactions |
@Fragment |
Partial updates | High (70%+ reduction) | Large components |
@RateLimit |
Prevent abuse | Low | Public endpoints |
@Authorize |
Access control | Low | Restricted actions |
@Idempotent |
Duplicate prevention | Low | Critical operations |
@Encrypted |
Data protection | Medium (+30% payload) | Sensitive data |
@Optimistic |
Instant UI | High (<50ms latency) | Interactive UIs |
@Cached |
Response caching | High (avoid queries) | Expensive ops |
@NoBatch |
Disable batching | Negative (more requests) | File uploads |
@Validated |
Input validation | Low | User input |
@Persisted |
State persistence | Medium | User preferences |
Debugging Attributes
Check Attribute Configuration:
// Client-side debugging
const component = LiveComponent.getComponent('component-id');
console.log(component.config.attributes);
// Output:
{
rateLimit: { requests: 60, window: 60 },
fragments: ['user-profile', 'notifications'],
optimistic: { enabled: true, updateProps: ['likes'] },
cached: { ttl: 3600, tags: ['users'] }
}
Server-side Debugging:
// Get action metadata
$reflection = new \ReflectionMethod(MyComponent::class, 'myAction');
$attributes = $reflection->getAttributes(LiveAction::class);
foreach ($attributes as $attribute) {
var_dump($attribute->getArguments());
}
Best Practices Summary
- Use
@Fragmentfor components >5KB or with independent sections - Use
@Optimisticfor non-critical user interactions (likes, votes) - Use
@RateLimiton all public endpoints and expensive operations - Use
@Authorizefor all privileged actions - Use
@Idempotentfor CREATE/DELETE operations - Use
@Encryptedfor PII and financial data - Use
@Cachedfor expensive queries (>100ms) - Use
@Validatedfor all user input - Combine attributes for defense-in-depth security
- Test attribute behavior with integration tests
For more examples and advanced patterns, see: