helvetiapps/live-controls
Live Controls is a Laravel package that adds reusable, reactive UI controls to Livewire applications, helping you build interactive buttons, toggles, and form widgets with less markup and minimal setup. Ideal for quickly wiring up dynamic interfaces without writing custom components from scratch.
Install and wire up (about 5 minutes)
composer require spatie/laravel-permission
php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider"
php artisan migrate
That publishes the package config and the role/permission pivot migrations. Next:
use Spatie\Permission\Traits\HasPermissions;
class User extends Authenticatable
{
use HasPermissions;
}
$role = Role::create(['name' => 'editor']);
$permission = Permission::create(['name' => 'edit articles']);
$role->givePermissionTo($permission);
$user->assignRole('editor');
app/Http/Kernel.php ($routeMiddleware) and gate a route:
Route::put('/articles/{article}', 'ArticleController@update')
->middleware('permission:edit articles');
Where to look first: config/laravel-permission.php (table names, cache key/expiry, exception behaviour), then Spatie\Permission\Models\{Role,Permission} and Spatie\Permission\Traits\HasPermissions. Those three files tell you 90% of what the package does.
First real use case: replace ad-hoc is_admin boolean columns. Map your existing admin flag to a super-admin role, then use Gate::before so super-admins bypass every check — you get granular permissions for everyone else without touching existing code paths.
About v1.8.23: this is a patch release. HasPermissions::fetchPermissions() used to throw when the consuming model did not declare a $permissionTable property; it now falls back to the package config. Nothing to migrate, no signature changes. If you previously added protected $permissionTable = 'permissions'; to your User model as a workaround, you can drop it — and users who never added it will simply stop getting exceptions.
Seeding is the backbone. Put roles and permissions in a seeder and run it in every environment, including tests. Use firstOrCreate / givePermissionTo so the seeder is idempotent, and call app(PermissionRegistrar::class)->forgetCachedPermissions() at the end — otherwise a cached permission map will shadow what you just created.
foreach (['admin', 'editor', 'viewer'] as $name) {
Role::firstOrCreate(['name' => $name]);
}
Permission::firstOrCreate(['name' => 'publish articles']);
Role::findByName('editor')->givePermissionTo('publish articles');
Route middleware is the cheapest enforcement. middleware('role:admin') and middleware('permission:edit articles') cover most CRUD guards. Use | for OR semantics (permission:edit articles|publish articles) and remember the middleware strings are permission names, so keep names slug-like and stable.
Policies and form requests for anything non-trivial. Route middleware can't see the model instance. Once ownership or record state matters, hand off to a policy:
public function update(User $user, Article $article)
{
return $user->hasPermissionTo('edit articles') && $article->user_id === $user->id;
}
Then $this->authorize('update', $article) in the controller or Gate::allows(...) in a form request.
Prefer roles in code, permissions in data. Check hasRole() for coarse branch points ("show the admin nav"), check hasPermissionTo() for actual capabilities. This lets you re-shape what an editor can do without editing PHP.
Direct permissions vs. role permissions. $user->givePermissionTo('publish articles') is handy for one-off grants and exceptions, but it makes auditing harder. Standardise on role-based grants and reserve direct grants for break-glass scenarios, documented in the seeder.
Testing. Use RefreshDatabase, run the permission seeder in setUp(), and clear the registrar cache. Then the suite reads like $this->actingAs($editor)->put(...)->assertStatus(200). Add one test per role boundary — a 403 test for each protected route catches the vast majority of permission regressions.
Expose abilities to the frontend. For an SPA, return the checks the UI needs rather than duplicating a role matrix in JavaScript:
return [
'can' => [
'publish' => $user->hasPermissionTo('publish articles'),
],
];
Custom models via extension points. If you need extra columns, subclass Spatie\Permission\Models\Role / Permission, point the package config at your classes, and keep the $permissionTable / $roleTable properties aligned with the pivot table names in the migrations.
spatie.permission.cache. Any runtime mutation (assigning a role in a controller, creating a permission in an admin panel) needs a forgetCachedPermissions() call afterwards, or the change won't take effect until the TTL expires. If a check "randomly" fails right after you granted something, this is why.config:cache during deploy. If your permission/role table names live in config, a stale cached config after a deploy is a classic source of "works locally, 403 in production." Rebuild config in your deploy script, and never call env() outside the config file.fetchPermissions() now falls back to config when $permissionTable is absent — which means your migration table name, the config value, and any $permissionTable override must all agree. Pick one source of truth; if you override the property on the model, treat it as authoritative and drop the config value for that table from your mental model.hasPermissionTo('typo') throws PermissionDoesNotExist rather than returning false. Wrap it (or use a try/catch helper) in user-facing code — a typo should be a 403, not a 500.can() is shadowed. The trait provides its own permission-aware can(). If you have Laravel gates with the same string names as permissions, you can get confusing double-resolution. Name gates and permissions distinctly.Gate::before, not by stuffing every permission into the role. It's one line in AuthServiceProvider and avoids N pivot rows.roles and permissions when rendering lists of users with their abilities. Cheaper still: compute hasPermissionTo() once and pass a boolean into the view.edit articles vs Edit Articles are two different permissions.@foreach bodies for large collections; hoist the check, or you'll re-hit the cache hundreds of times per request.composer update, verify your fetchPermissions()-dependent flows still behave. No config republish, no migration, no trait signature change.How can I help you explore Laravel packages today?