PHP Composables
Reusable units of server-side logic that expose data and actions directly to your Vue templates under clean namespaces. Inspired by Vue's Composition API, but executed entirely on the server with full access to auth, database and services.
Creating a Composable
A composable is a PHP trait with a use* method that returns an array of data and closures (actions). Generate one with Artisan:
php artisan make:livue-composable Auth
<?php
namespace App\LiVue\Composables;
trait UseAuth
{
public function useAuth(): array
{
$user = auth()->user();
return [
// Data (available in template)
'user' => $user,
'name' => $user?->name ?? 'Guest',
'isAuthenticated' => auth()->check(),
'isAdmin' => $user?->hasRole('admin') ?? false,
// Actions (callable from template)
'logout' => function() {
auth()->logout();
$this->redirect('/login');
},
];
}
}
Using in a Component
Add the trait to your component. All use* methods are automatically discovered — no need to declare a $composables array. The method name minus the use prefix becomes the namespace in the template:
class Dashboard extends Component
{
use UseAuth, UseNotifications;
}
Tip — You can still use the $composables array, the #[Composable] attribute, or the getComposables() method for explicit control. Auto-discovery simply removes the need for boilerplate when you just want all use* methods to be active.
<nav>
<span>Welcome, {{ auth.name }}</span>
<span v-if="auth.isAdmin">Admin</span>
<span>{{ notifications.count }} unread</span>
<button @click="auth.logout()">Logout</button>
</nav>
Composable State
Use composableState() to maintain private persistent state that does not require public properties on the component:
trait UseCart
{
public function useCart(): array
{
// Private 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'] = [],
];
}
}
<div v-if="cart.isEmpty">Your cart is empty</div>
<div v-for="(item, index) in cart.items" :key="index">
{{ item.name }} - {{ item.price }} EUR
<button @click="cart.remove(index)">Remove</button>
</div>
<p>Total: {{ cart.total.toFixed(2) }} EUR ({{ cart.count }} items)</p>
<button @click="cart.add(42)">Add Product</button>
<button @click="cart.clear()">Clear Cart</button>
Global Composables
Register composables globally from a service provider with Component::use(). Global composables are available to all components without needing traits or declarations.
Closure-based
use LiVue\Component;
public function boot(): void
{
Component::use('auth', function () {
$user = auth()->user();
return [
'name' => $user?->name ?? 'Guest',
'isAdmin' => $user?->hasRole('admin') ?? false,
'logout' => function () {
auth()->logout();
$this->redirect('/login');
},
];
});
}
The closure is bound to the component instance at runtime, so $this refers to the current component.
Class-based (mixin pattern)
Group related composables into a class. Each use* method must return a closure:
class AuthComposables
{
public function useAuth()
{
return function (): array {
return [
'user' => auth()->user(),
'logout' => fn () => auth()->logout(),
];
};
}
public function usePermissions()
{
return function (): array {
return [
'can' => fn (string $p) => auth()->user()?->can($p) ?? false,
];
};
}
}
// In AppServiceProvider::boot()
Component::use(new AuthComposables());
// or
Component::use(AuthComposables::class);
Precedence — Local composables (defined on the component) always take precedence over global composables with the same namespace.
Naming Convention
The template namespace is derived from the method name by removing the use prefix and converting to camelCase:
| Method | Namespace | Template access |
|---|---|---|
useAuth() |
auth |
auth.name, auth.logout() |
useShoppingCart() |
shoppingCart |
shoppingCart.items |
useNotifications() |
notifications |
notifications.count |
Security
Composables are re-executed on every AJAX request, so authorization checks and data are always fresh. Only closures explicitly returned are callable from the client — the server never exposes internal methods.
| Rule | Description |
|---|---|
| Action whitelist | Only closures explicitly returned in the array are callable from the client |
| HMAC checksum | All composable data is protected by the snapshot HMAC — tampered requests are rejected |
| Re-execution | Composables run on every request, ensuring authorization is always verified |
| Blocked methods | use, getGlobalComposables, flushGlobalComposables are not callable from the client |