BETA We're building something new — all help is welcome! Contribute →
EXCLUSIVE

PHP Composables

A revolutionary pattern to encapsulate reusable PHP logic and expose it directly in Vue templates.

Why Composables?

PHP Composables solve code duplication by letting you write reusable logic once and use it everywhere with a clean, namespaced API in your templates.

Without Composables

// Duplicated in every component
public string $userName = '';
public bool $isAdmin = false;

public function logout() { ... }

// Template: scattered access
<p>{{ userName }}</p>
<button @click="livue.call('logout')">

With Composables

// Define once, reuse everywhere
use UseAuth;
protected array $composables = ['useAuth'];

// Template: namespaced access
<p>{{ auth.name }}</p>
<button @click="auth.logout()">

Creating a Composable

A composable is a PHP trait with a use* method that returns data and actions:

trait UseAuth
{
    public function useAuth(): array
    {
        $user = auth()->user();

        return [
            // Data (reactive in templates)
            'user' => $user,
            'name' => $user?->name ?? 'Guest',
            'email' => $user?->email,
            'isAuthenticated' => auth()->check(),
            'isAdmin' => $user?->hasRole('admin') ?? false,

            // Actions (callable from templates)
            'logout' => function() {
                auth()->logout();
                $this->redirect('/login');
            },

            'can' => fn(string $permission) => $user?->can($permission) ?? false,
        ];
    }
}

Using in Components

Component
class Dashboard extends Component
{
    use UseAuth, UseNotifications;

    protected array $composables = [
        'useAuth',
        'useNotifications',
    ];
}
Template
<nav>
    <span>Welcome, {{ auth.name }}</span>

    <span v-if="auth.isAdmin">
        Admin
    </span>

    <span>
        {{ notifications.count }} new
    </span>

    <button @click="auth.logout()">
        Logout
    </button>
</nav>

Persistent State

Use $this->composableState() for state that persists across requests:

trait UseCart
{
    public function useCart(): array
    {
        // State persists between requests
        $state = $this->composableState('cart', [
            'items' => [],
        ]);

        $items = collect($state['items']);

        return [
            'items' => $state['items'],
            'count' => $items->sum('qty'),
            'total' => $items->sum(fn($i) => $i['price'] * $i['qty']),
            'isEmpty' => $items->isEmpty(),

            'add' => function(int $productId, int $qty = 1) use ($state) {
                $product = Product::findOrFail($productId);
                $state['items'] = [...$state['items'], [
                    'product_id' => $productId,
                    'name' => $product->name,
                    'price' => $product->price,
                    'qty' => $qty,
                ]];
            },

            'remove' => function(int $index) use ($state) {
                $items = $state['items'];
                unset($items[$index]);
                $state['items'] = array_values($items);
            },

            'clear' => fn() => $state['items'] = [],
        ];
    }
}

Template Syntax

Accessing Data
<!-- Simple values -->
<p>{{ auth.name }}</p>
<p>{{ cart.total }} EUR</p>

<!-- Conditionals -->
<div v-if="auth.isAdmin">
    Admin Panel
</div>

<!-- Loops -->
<div v-for="item in cart.items">
    {{ item.name }}
</div>
Calling Actions
<!-- Direct call (recommended) -->
<button @click="cart.add(123)">
    Add to Cart
</button>

<!-- With parameters -->
<button @click="cart.add(product.id, 2)">
    Add 2
</button>

<!-- Alternative via livue.call -->
<button @click="livue.call('cart.add', [123])">

Key Benefits

Reusable

Write once, use in any component. Just add the trait and register.

Encapsulated

State can be private to the trait. No public properties needed on component.

Namespaced

Clean API in templates: auth.*, cart.*, prefs.*

Secure

Re-executed on every request. Auth checks always fresh.

Type Support

Return Eloquent Models, Carbon, Collections - all serialized automatically.

Portable

Copy traits between projects. Self-contained and dependency-free.

Naming Convention

The namespace is derived from the method name by removing the use prefix:

Method Namespace Template Usage
useAuth() auth auth.name, auth.logout()
useShoppingCart() shoppingCart shoppingCart.items
useUserPrefs() userPrefs userPrefs.theme

Override with #[Composable(as: 'cart')] for custom namespaces.

Registration Methods

1. Via $composables array (recommended)

protected array $composables = ['useAuth', 'useCart'];

2. Via #[Composable] attribute

#[Composable]
public function useMetrics(): array { ... }

#[Composable(as: 'time')]  // Custom namespace
public function useTimestamp(): array { ... }

3. Via getComposables() (conditional)

protected function getComposables(): array
{
    $composables = ['useAuth'];

    if (auth()->user()?->isAdmin()) {
        $composables[] = 'useAdmin';
    }

    return $composables;
}

Real-World Example: E-commerce Cart

<div class="cart">
    <div v-if="cart.isEmpty" class="empty">
        Your cart is empty
    </div>

    <div v-else>
        <div v-for="(item, index) in cart.items" :key="index">
            <span>{{ item.name }}</span>
            <span>{{ item.price }} x {{ item.qty }}</span>
            <button @click="cart.remove(index)">Remove</button>
        </div>

        <div class="total">
            Total: {{ cart.total.toFixed(2) }} EUR
        </div>

        <button @click="cart.clear()">Clear Cart</button>
    </div>
</div>

Security

  • Whitelist only - Only declared actions are callable from client
  • Re-executed per request - Auth checks run every time
  • No closure serialization - Closures never sent to client
  • HMAC protected - Memo integrity verified via checksum