Files
michaelschiemer/src/Framework/LiveComponents/docs/COMPONENT-CREATION-GUIDE.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

29 KiB

LiveComponents End-to-End Creation Guide

Complete guide for creating LiveComponents from scratch - from concept to production deployment.

Table of Contents


Overview

A LiveComponent is a server-rendered, interactive UI component with automatic state synchronization. Components are:

  • Server-rendered: HTML generated on the server
  • Stateful: Maintain state across interactions
  • Interactive: Respond to user actions without full page reloads
  • Framework-compliant: Readonly classes, Value Objects, Composition

When to Use LiveComponents

Use LiveComponents for:

  • Interactive forms with validation
  • Real-time dashboards
  • Shopping carts
  • Search interfaces with filters
  • File uploads with progress
  • Paginated lists
  • Wizards/multi-step forms

Don't use LiveComponents for:

  • Static content (use regular templates)
  • Simple links (use standard HTML)
  • High-frequency updates (use WebSockets directly)
  • Complex client-side logic (use dedicated JavaScript)

Quick Start

Minimal Component (5 minutes)

<?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;

// 1. Mark class with #[LiveComponent] attribute
#[LiveComponent('counter')]
// 2. Implement LiveComponentContract
final readonly class CounterComponent implements LiveComponentContract
{
    // 3. Constructor: ComponentId + State
    public function __construct(
        public ComponentId $id,
        public CounterState $state
    ) {}

    // 4. Define actions with #[Action]
    #[Action]
    public function increment(): CounterState
    {
        return $this->state->increment();
    }

    #[Action]
    public function decrement(): CounterState
    {
        return $this->state->decrement();
    }

    // 5. Render method returns template path
    public function render(): string
    {
        return 'components/counter';
    }
}

State Value Object

<?php

declare(strict_types=1);

namespace App\Application\LiveComponents\Counter;

use App\Framework\LiveComponents\Contracts\ComponentData;
use DateTimeImmutable;

// State must implement ComponentData
final readonly class CounterState implements ComponentData
{
    public function __construct(
        public int $count = 0,
        public ?DateTimeImmutable $lastUpdate = null
    ) {}

    // Immutable state updates
    public function increment(): self
    {
        return new self(
            count: $this->count + 1,
            lastUpdate: new DateTimeImmutable()
        );
    }

    public function decrement(): self
    {
        return new self(
            count: max(0, $this->count - 1), // Prevent negative
            lastUpdate: new DateTimeImmutable()
        );
    }

    // Required by ComponentData
    public function toArray(): array
    {
        return [
            'count' => $this->count,
            'lastUpdate' => $this->lastUpdate?->format('Y-m-d H:i:s')
        ];
    }
}

Template

<!-- resources/views/components/counter.view.php -->
<div data-lc-component="counter" data-lc-id="{component_id}">
    <h2>Counter: {count}</h2>

    <button data-lc-action="increment">+1</button>
    <button data-lc-action="decrement">-1</button>

    <if condition="lastUpdate">
        <p>Last update: {lastUpdate}</p>
    </if>
</div>

Usage in Controller

use App\Framework\LiveComponents\ComponentRegistry;
use App\Framework\LiveComponents\ValueObjects\ComponentId;
use App\Framework\View\ViewResult;

public function show(ComponentRegistry $registry): ViewResult
{
    $component = $registry->resolve(
        ComponentId::fromString('counter:main'),
        initialData: ['count' => 0]
    );

    return new ViewResult('pages/counter', [
        'component' => $component
    ]);
}

That's it! Component is now interactive. Actions automatically sync state to the server.


Step-by-Step Tutorial

Let's build a Shopping Cart Component from scratch.

Step 1: Define the Domain

<?php
// src/Application/LiveComponents/ShoppingCart/CartItem.php

namespace App\Application\LiveComponents\ShoppingCart;

use App\Framework\Core\ValueObjects\Price;

final readonly class CartItem
{
    public function __construct(
        public string $productId,
        public string $name,
        public Price $price,
        public int $quantity
    ) {
        if ($quantity < 1) {
            throw new \InvalidArgumentException('Quantity must be at least 1');
        }
    }

    public function getTotal(): Price
    {
        return $this->price->multiply($this->quantity);
    }

    public function withQuantity(int $quantity): self
    {
        return new self(
            $this->productId,
            $this->name,
            $this->price,
            $quantity
        );
    }

    public function toArray(): array
    {
        return [
            'product_id' => $this->productId,
            'name' => $this->name,
            'price' => $this->price->toDecimal(),
            'quantity' => $this->quantity,
            'total' => $this->getTotal()->toDecimal()
        ];
    }
}

Step 2: Create State Value Object

<?php
// src/Application/LiveComponents/ShoppingCart/ShoppingCartState.php

namespace App\Application\LiveComponents\ShoppingCart;

use App\Framework\LiveComponents\Contracts\ComponentData;
use App\Framework\Core\ValueObjects\Price;

final readonly class ShoppingCartState implements ComponentData
{
    /** @param array<CartItem> $items */
    public function __construct(
        public array $items = [],
        public ?string $couponCode = null,
        public int $discountPercentage = 0
    ) {}

    public function addItem(CartItem $item): self
    {
        // Check if item already exists
        foreach ($this->items as $existingItem) {
            if ($existingItem->productId === $item->productId) {
                // Update quantity
                return $this->updateQuantity(
                    $item->productId,
                    $existingItem->quantity + $item->quantity
                );
            }
        }

        // Add new item
        return new self(
            items: [...$this->items, $item],
            couponCode: $this->couponCode,
            discountPercentage: $this->discountPercentage
        );
    }

    public function removeItem(string $productId): self
    {
        return new self(
            items: array_values(array_filter(
                $this->items,
                fn(CartItem $item) => $item->productId !== $productId
            )),
            couponCode: $this->couponCode,
            discountPercentage: $this->discountPercentage
        );
    }

    public function updateQuantity(string $productId, int $quantity): self
    {
        if ($quantity < 1) {
            return $this->removeItem($productId);
        }

        $updatedItems = array_map(
            fn(CartItem $item) => $item->productId === $productId
                ? $item->withQuantity($quantity)
                : $item,
            $this->items
        );

        return new self(
            items: $updatedItems,
            couponCode: $this->couponCode,
            discountPercentage: $this->discountPercentage
        );
    }

    public function applyCoupon(string $couponCode, int $discountPercentage): self
    {
        return new self(
            items: $this->items,
            couponCode: $couponCode,
            discountPercentage: $discountPercentage
        );
    }

    public function getSubtotal(): Price
    {
        $total = 0;
        foreach ($this->items as $item) {
            $total += $item->getTotal()->cents;
        }

        return Price::fromCents($total);
    }

    public function getDiscount(): Price
    {
        if ($this->discountPercentage === 0) {
            return Price::fromCents(0);
        }

        $subtotal = $this->getSubtotal();
        $discountAmount = ($subtotal->cents * $this->discountPercentage) / 100;

        return Price::fromCents((int) $discountAmount);
    }

    public function getTotal(): Price
    {
        return $this->getSubtotal()->subtract($this->getDiscount());
    }

    public function getItemCount(): int
    {
        return array_reduce(
            $this->items,
            fn(int $sum, CartItem $item) => $sum + $item->quantity,
            0
        );
    }

    public function isEmpty(): bool
    {
        return empty($this->items);
    }

    public function toArray(): array
    {
        return [
            'items' => array_map(fn(CartItem $item) => $item->toArray(), $this->items),
            'coupon_code' => $this->couponCode,
            'discount_percentage' => $this->discountPercentage,
            'subtotal' => $this->getSubtotal()->toDecimal(),
            'discount' => $this->getDiscount()->toDecimal(),
            'total' => $this->getTotal()->toDecimal(),
            'item_count' => $this->getItemCount(),
            'is_empty' => $this->isEmpty()
        ];
    }
}

Step 3: Create Component

<?php
// src/Application/LiveComponents/ShoppingCart/ShoppingCartComponent.php

namespace App\Application\LiveComponents\ShoppingCart;

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\ComponentEventDispatcher;
use App\Framework\LiveComponents\ValueObjects\EventPayload;
use App\Framework\Core\ValueObjects\Price;

#[LiveComponent('shopping-cart')]
final readonly class ShoppingCartComponent implements LiveComponentContract
{
    public function __construct(
        public ComponentId $id,
        public ShoppingCartState $state,
        private CouponService $couponService // Injected service
    ) {}

    #[Action(rateLimit: 30, rateLimitWindow: 60)] // 30 updates per minute
    public function addItem(
        string $product_id,
        string $name,
        int $price_cents,
        int $quantity = 1,
        ?ComponentEventDispatcher $events = null
    ): ShoppingCartState {
        $item = new CartItem(
            productId: $product_id,
            name: $name,
            price: Price::fromCents($price_cents),
            quantity: $quantity
        );

        $newState = $this->state->addItem($item);

        // Dispatch event for analytics
        $events?->dispatch('cart:item-added', EventPayload::fromArray([
            'product_id' => $product_id,
            'quantity' => $quantity,
            'total_items' => $newState->getItemCount()
        ]));

        return $newState;
    }

    #[Action]
    public function removeItem(
        string $product_id,
        ?ComponentEventDispatcher $events = null
    ): ShoppingCartState {
        $newState = $this->state->removeItem($product_id);

        $events?->dispatch('cart:item-removed', EventPayload::fromArray([
            'product_id' => $product_id,
            'total_items' => $newState->getItemCount()
        ]));

        return $newState;
    }

    #[Action]
    public function updateQuantity(
        string $product_id,
        int $quantity
    ): ShoppingCartState {
        return $this->state->updateQuantity($product_id, $quantity);
    }

    #[Action(rateLimit: 5, rateLimitWindow: 300)] // 5 coupon attempts per 5 min
    public function applyCoupon(
        string $coupon_code,
        ?ComponentEventDispatcher $events = null
    ): ShoppingCartState {
        // Validate coupon via service
        $coupon = $this->couponService->validate($coupon_code);

        if (!$coupon->isValid()) {
            $events?->dispatch('cart:coupon-invalid', EventPayload::fromArray([
                'coupon_code' => $coupon_code,
                'reason' => $coupon->getInvalidReason()
            ]));

            // Return current state unchanged
            return $this->state;
        }

        $newState = $this->state->applyCoupon(
            $coupon_code,
            $coupon->getDiscountPercentage()
        );

        $events?->dispatch('cart:coupon-applied', EventPayload::fromArray([
            'coupon_code' => $coupon_code,
            'discount_percentage' => $coupon->getDiscountPercentage(),
            'discount_amount' => $newState->getDiscount()->toDecimal()
        ]));

        return $newState;
    }

    #[Action(
        rateLimit: 3,
        rateLimitWindow: 300,
        idempotencyTTL: 3600
    )]
    public function checkout(?ComponentEventDispatcher $events = null): ShoppingCartState
    {
        if ($this->state->isEmpty()) {
            $events?->dispatch('cart:checkout-failed', EventPayload::fromArray([
                'reason' => 'empty_cart'
            ]));

            return $this->state;
        }

        // Process checkout (would integrate with payment service)
        $events?->dispatch('cart:checkout-initiated', EventPayload::fromArray([
            'total' => $this->state->getTotal()->toDecimal(),
            'item_count' => $this->state->getItemCount()
        ]));

        return $this->state;
    }

    public function render(): string
    {
        return 'components/shopping-cart';
    }
}

Step 4: Create Template

<!-- resources/views/components/shopping-cart.view.php -->
<div
    data-lc-component="shopping-cart"
    data-lc-id="{component_id}"
    class="shopping-cart"
>
    <h2>Shopping Cart ({item_count} items)</h2>

    <if condition="is_empty">
        <p class="empty-message">Your cart is empty</p>
    </if>

    <if condition="!is_empty">
        <!-- Cart Items (Fragment for partial updates) -->
        <div data-lc-fragment="cart-items">
            <for items="items" as="item">
                <div class="cart-item">
                    <h3>{item.name}</h3>
                    <p>Price: €{item.price}</p>

                    <div class="quantity-control">
                        <button
                            data-lc-action="updateQuantity"
                            data-lc-params='{"product_id": "{item.product_id}", "quantity": {item.quantity - 1}}'
                        >-</button>

                        <span>{item.quantity}</span>

                        <button
                            data-lc-action="updateQuantity"
                            data-lc-params='{"product_id": "{item.product_id}", "quantity": {item.quantity + 1}}'
                        >+</button>
                    </div>

                    <p>Total: €{item.total}</p>

                    <button
                        data-lc-action="removeItem"
                        data-lc-params='{"product_id": "{item.product_id}"}'
                        class="remove-btn"
                    >Remove</button>
                </div>
            </for>
        </div>

        <!-- Cart Summary (Fragment for partial updates) -->
        <div data-lc-fragment="cart-summary" class="cart-summary">
            <div>
                <span>Subtotal:</span>
                <span>€{subtotal}</span>
            </div>

            <if condition="discount_percentage > 0">
                <div class="discount">
                    <span>Discount ({discount_percentage}%):</span>
                    <span>-€{discount}</span>
                </div>
            </if>

            <div class="total">
                <span>Total:</span>
                <span>€{total}</span>
            </div>
        </div>

        <!-- Coupon Form -->
        <form data-lc-action="applyCoupon" class="coupon-form">
            <input type="text" name="coupon_code" placeholder="Coupon code" />
            <button type="submit">Apply</button>
        </form>

        <!-- Checkout Button -->
        <button
            data-lc-action="checkout"
            class="checkout-btn"
            data-lc-confirm="Proceed to checkout?"
        >
            Checkout (€{total})
        </button>
    </if>
</div>

Step 5: JavaScript Integration (Optional)

// resources/js/modules/shopping-cart.js
import { LiveComponent } from './live-component.js';

const cartComponent = new LiveComponent('shopping-cart:main');

// Listen to events
cartComponent.on('cart:item-added', (data) => {
    console.log('Item added:', data.product_id);

    // Show notification
    showNotification(`Added ${data.quantity} item(s) to cart`);

    // Update cart badge
    updateCartBadge(data.total_items);
});

cartComponent.on('cart:coupon-applied', (data) => {
    showNotification(
        `Coupon applied! You save ${data.discount_percentage}%`,
        'success'
    );
});

cartComponent.on('cart:coupon-invalid', (data) => {
    showNotification(
        `Invalid coupon: ${data.reason}`,
        'error'
    );
});

cartComponent.on('cart:checkout-initiated', (data) => {
    // Redirect to checkout page
    window.location.href = `/checkout?total=${data.total}`;
});

// Fragment updates for better performance
cartComponent.on('action:updateQuantity', async (event) => {
    // Only update cart-items and cart-summary fragments
    await cartComponent.executeAction(
        'updateQuantity',
        event.params,
        { fragments: ['cart-items', 'cart-summary'] }
    );
});

Step 6: Testing

<?php
// tests/Feature/LiveComponents/ShoppingCartComponentTest.php

use function Pest\LiveComponents\mountComponent;
use function Pest\LiveComponents\callAction;

describe('ShoppingCartComponent', function () {
    it('starts with empty cart', function () {
        $component = mountComponent('shopping-cart:test');

        expect($component['state'])->toHaveStateKey('is_empty', true);
        expect($component['state'])->toHaveStateKey('item_count', 0);
    });

    it('adds items to cart', function () {
        $component = mountComponent('shopping-cart:test');

        $result = callAction($component, 'addItem', [
            'product_id' => 'product-123',
            'name' => 'Test Product',
            'price_cents' => 1999,
            'quantity' => 2
        ]);

        expect($result['state'])->toHaveStateKey('item_count', 2);
        expect($result['state'])->toHaveStateKey('subtotal', '39.98');
        expect($result['events'])->toHaveDispatchedEvent('cart:item-added');
    });

    it('applies valid coupon', function () {
        $component = mountComponent('shopping-cart:test');

        // Add item first
        $component = callAction($component, 'addItem', [
            'product_id' => 'product-123',
            'name' => 'Test Product',
            'price_cents' => 10000,
            'quantity' => 1
        ]);

        // Apply coupon
        $result = callAction($component, 'applyCoupon', [
            'coupon_code' => 'SAVE20'
        ]);

        expect($result['state'])->toHaveStateKey('discount_percentage', 20);
        expect($result['state'])->toHaveStateKey('discount', '20.00');
        expect($result['state'])->toHaveStateKey('total', '80.00');
        expect($result['events'])->toHaveDispatchedEvent('cart:coupon-applied');
    });

    it('enforces rate limiting on checkout', function () {
        $component = mountComponent('shopping-cart:test');

        // Add item
        $component = callAction($component, 'addItem', [
            'product_id' => 'product-123',
            'name' => 'Test Product',
            'price_cents' => 10000
        ]);

        // Try to checkout 4 times (limit is 3)
        callAction($component, 'checkout');
        callAction($component, 'checkout');
        callAction($component, 'checkout');

        // 4th attempt should be rate limited
        expect(fn() => callAction($component, 'checkout'))
            ->toThrow(RateLimitExceededException::class);
    });
});

Component Architecture

Required Elements

Every LiveComponent must have:

  1. #[LiveComponent] attribute with unique name
  2. LiveComponentContract implementation
  3. ComponentId + State in constructor
  4. State as ComponentData Value Object
  5. render() method returning template path

Optional Elements

  • #[Action] methods for interactivity
  • Service injection via constructor
  • Event dispatching for side effects
  • Pollable interface for auto-refresh
  • Fragment markers for partial updates

Component Lifecycle

1. Resolve (ComponentRegistry)
   ├─ Create instance via DI Container
   ├─ Inject ComponentId
   ├─ Inject initial State
   └─ Inject Services

2. Render (LiveComponentRenderer)
   ├─ Call render() for template path
   ├─ Pass state data to template
   ├─ Process template (placeholders, conditionals, loops)
   └─ Return HTML

3. Action Execution (LiveComponentHandler)
   ├─ Validate CSRF token
   ├─ Check rate limits
   ├─ Check idempotency
   ├─ Bind parameters
   ├─ Execute action method
   ├─ Create new State from return value
   ├─ Dispatch events
   └─ Return ComponentUpdate

4. Re-render
   ├─ Create new component instance with new State
   ├─ Render updated HTML
   ├─ Extract fragments (if requested)
   └─ Return response (html/fragments + state + events)

Advanced Patterns

Service Injection

#[LiveComponent('user-profile')]
final readonly class UserProfileComponent implements LiveComponentContract
{
    public function __construct(
        public ComponentId $id,
        public UserProfileState $state,
        // Inject services via constructor
        private UserRepository $userRepository,
        private AvatarService $avatarService,
        private PermissionChecker $permissions
    ) {}

    #[Action]
    public function updateAvatar(UploadedFile $avatar): UserProfileState
    {
        $url = $this->avatarService->upload($avatar);

        return $this->state->withAvatarUrl($url);
    }
}

Event Dispatching

#[Action]
public function completeOrder(
    ?ComponentEventDispatcher $events = null
): OrderState {
    $order = $this->orderService->complete($this->state->orderId);

    // Dispatch domain event
    $events?->dispatch('order:completed', EventPayload::fromArray([
        'order_id' => $order->id,
        'total' => $order->total->toDecimal(),
        'customer_id' => $order->customerId
    ]));

    // Other systems can listen to this event
    // - Send confirmation email
    // - Update inventory
    // - Notify warehouse
    // - Track analytics

    return $this->state->withOrder($order);
}

Polling for Real-Time Updates

use App\Framework\LiveComponents\Contracts\Pollable;

#[LiveComponent('order-status')]
final readonly class OrderStatusComponent implements
    LiveComponentContract,
    Pollable
{
    #[Action]
    public function poll(): OrderStatusState
    {
        // Fetch latest order status
        $order = $this->orderRepository->find($this->state->orderId);

        return $this->state->withStatus($order->status);
    }

    public function getPollInterval(): int
    {
        return 5000; // Poll every 5 seconds
    }
}

Fragment-Based Updates

// Template with fragments
<div data-lc-component="product-list">
    <!-- Only update this part -->
    <div data-lc-fragment="product-grid">
        <for items="products" as="product">
            <!-- Product cards -->
        </for>
    </div>

    <!-- Only update this part -->
    <div data-lc-fragment="pagination">
        <!-- Pagination controls -->
    </div>
</div>

// JavaScript
liveComponent.executeAction('changePage', { page: 2 }, {
    fragments: ['product-grid', 'pagination'] // Only update these
});

DTOs for Complex Parameters

// Define DTO
final readonly class UpdateProfileRequest
{
    public function __construct(
        public string $name,
        public Email $email,
        public ?string $bio = null,
        public bool $newsletter = false
    ) {}
}

// Use in action
#[Action]
public function updateProfile(UpdateProfileRequest $request): UserProfileState
{
    // $request is automatically instantiated from action parameters
    return $this->state->update(
        name: $request->name,
        email: $request->email,
        bio: $request->bio,
        newsletter: $request->newsletter
    );
}

Testing

See tests/Feature/LiveComponents/CounterComponentTest.php for examples.

Testing Helpers

use function Pest\LiveComponents\mountComponent;
use function Pest\LiveComponents\callAction;
use function Pest\LiveComponents\callActionWithFragments;

// Mount component
$component = mountComponent('counter:test', ['count' => 0]);

// Execute action
$result = callAction($component, 'increment');

// Execute action with fragments
$result = callActionWithFragments(
    $component,
    'updateGrid',
    params: ['page' => 2],
    fragments: ['product-grid', 'pagination']
);

Custom Expectations

// HTML assertions
expect($component['html'])->toContainHtml('Count: 5');

// State assertions
expect($component['state'])->toHaveState(['count' => 5]);
expect($component['state'])->toHaveStateKey('count', 5);

// Event assertions
expect($result['events'])->toHaveDispatchedEvent('counter:changed');
expect($result['events'])->toHaveDispatchedEventWithData('counter:changed', [
    'old_value' => 4,
    'new_value' => 5
]);

// Fragment assertions
expect($result['html'])->toHaveFragment('cart-summary', '€99.99');

Deployment

Production Checklist

  • All actions have #[Action] attribute
  • Rate limits configured for critical actions
  • Idempotency TTL set for state-changing actions
  • CSRF protection enabled (default)
  • Events logged for analytics
  • Error handling implemented
  • Tests passing
  • Templates optimized (fragments for large updates)
  • JavaScript client configured
  • Monitoring/alerting set up

Performance Tips

  1. Use fragments for partial updates
  2. Configure polling interval based on data freshness needs
  3. Add caching via #[Action(cache: true)]
  4. Optimize templates - minimize DOM size
  5. Lazy load components not immediately visible

Security Tips

  1. Never trust client input - validate in actions
  2. Use DTOs for complex parameters
  3. Set rate limits on all actions
  4. Enable idempotency for critical operations
  5. Log security events for monitoring

Troubleshooting

Component Not Found

Error: "Component 'my-component' not found"

Causes:

  • Missing #[LiveComponent] attribute
  • Typo in component name
  • Component class not in Application/LiveComponents/ directory
  • Discovery cache stale

Solutions:

# Clear discovery cache
php console.php discovery:clear

# Verify component is registered
php console.php mcp:server
# Then use analyze_codebase to find LiveComponents

Action Not Callable

Error: "Action 'myAction' not found"

Causes:

  • Missing #[Action] attribute
  • Method is private/protected
  • Method is static
  • Reserved method name (render, toArray, etc.)

Solutions:

// ✅ Correct
#[Action]
public function myAction(): StateType

// ❌ Wrong - missing attribute
public function myAction(): StateType

// ❌ Wrong - static
#[Action]
public static function myAction(): StateType

// ❌ Wrong - private
#[Action]
private function myAction(): StateType

State Not Updating

Causes:

  • Action not returning new state
  • State mutation instead of new instance
  • Caching issues

Solutions:

// ❌ Wrong - mutation (doesn't work with readonly)
public function increment(): void
{
    $this->state->count++; // Can't mutate readonly
}

// ✅ Correct - return new state
public function increment(): CounterState
{
    return $this->state->increment(); // Returns new instance
}

Events Not Dispatching

Causes:

  • Missing ComponentEventDispatcher parameter
  • Not calling dispatch() method
  • Events not handled in JavaScript

Solutions:

// ✅ Correct
#[Action]
public function save(?ComponentEventDispatcher $events = null): StateType
{
    $events?->dispatch('saved', EventPayload::fromArray(['id' => 123]));
    return $newState;
}

// JavaScript
liveComponent.on('saved', (data) => {
    console.log('Saved:', data.id);
});

Next Steps

  • Read Security Guide for CSRF/Rate Limiting/Idempotency
  • Read Performance Guide for optimization strategies
  • Read API Reference for complete attribute/contract documentation
  • Explore example components in src/Application/LiveComponents/
  • Run tests: ./vendor/bin/pest tests/Feature/LiveComponents/

Summary

LiveComponents provide:

Server-rendered interactivity without JavaScript complexity Type-safe state management with Value Objects Built-in security (CSRF, Rate Limiting, Idempotency) Framework-compliant (Readonly, Immutable, Composition) Easy testing with Pest helpers Production-ready with monitoring and error handling

Start simple with a counter, then build more complex components as you learn the patterns.