Files
michaelschiemer/docs/livecomponents/attributes-reference.md
Michael Schiemer fc3d7e6357 feat(Production): Complete production deployment infrastructure
- 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.
2025-10-25 19:18:37 +02:00

34 KiB

LiveComponents Attributes Reference

Complete reference for all LiveComponents attributes with examples and best practices.

Table of Contents


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: true only for user input fields
  • Use lazy: true for 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, not fragment-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-empty
  • email - Valid email format
  • min:n - Minimum length/value
  • max:n - Maximum length/value
  • numeric - Numeric value
  • alphanumeric - Alphanumeric characters only
  • unique:table,column - Unique in database
  • exists:table,column - Exists in database
  • confirmed - Match confirmation field (password_confirmation)
  • regex:pattern - Match regex pattern
  • in:foo,bar,baz - Value in list
  • url - Valid URL format
  • date - 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 errorFragment to update only error section
  • Don't trust client-side validation alone

Gotchas:

  • Validation errors prevent action execution
  • Errors automatically populate $errors property
  • 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:

  1. @RateLimit - Check rate limits first
  2. @Authorize - Check permissions
  3. @Validated - Validate parameters
  4. @Idempotent - Check for duplicate requests
  5. @Cached - Check cache (if applicable)
  6. Action Execution - Execute action logic
  7. @Fragment - Update specific fragments
  8. @Optimistic - Apply optimistic updates
  9. @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

  1. Use @Fragment for components >5KB or with independent sections
  2. Use @Optimistic for non-critical user interactions (likes, votes)
  3. Use @RateLimit on all public endpoints and expensive operations
  4. Use @Authorize for all privileged actions
  5. Use @Idempotent for CREATE/DELETE operations
  6. Use @Encrypted for PII and financial data
  7. Use @Cached for expensive queries (>100ms)
  8. Use @Validated for all user input
  9. Combine attributes for defense-in-depth security
  10. Test attribute behavior with integration tests

For more examples and advanced patterns, see: