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
class Dashboard extends Component
{
use UseAuth, UseNotifications;
protected array $composables = [
'useAuth',
'useNotifications',
];
}
<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
<!-- 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>
<!-- 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