mediapress/bouncer
Role and permission-based authorization for Laravel. Bouncer lets you define abilities, assign roles and permissions, and check access anywhere in your app with clean, testable code—no bloated configuration or complex setup required.
mediapress/bouncer is a fork of silber/bouncer (Eloquent roles & abilities), so day-to-day usage follows upstream Bouncer's API. The important difference is distribution: it is not on Packagist, so you pull it from the Bitbucket VCS repository.
1. Add the repository and require the package.
{
"repositories": [
{ "type": "vcs", "url": "https://bitbucket.org/mediaclickTR/bouncer.git" }
],
"require": {
"mediapress/bouncer": "dev-master#<commit-sha>"
}
}
Pin a commit or tag rather than tracking dev-master — see the Gotchas section for why this matters here. If the repo is private, the CI/build machine needs credentials (SSH deploy key via [email protected]:mediaclickTR/bouncer.git, an app password in the URL, or COMPOSER_AUTH in the environment).
2. Confirm the namespace before writing any code.
Forks of Bouncer usually keep the Silber\Bouncer namespace so they are drop-in replacements, but do not assume it:
composer show mediapress/bouncer
grep -r "namespace" vendor/mediapress/bouncer/src/ | head
Everything below uses Silber\Bouncer\...; substitute the fork's namespace if it differs.
3. Publish config/migrations and migrate. The provider FQCN is whatever you found in step 2 (typically Silber\Bouncer\BouncerServiceProvider).
php artisan vendor:publish --provider="Silber\Bouncer\BouncerServiceProvider" --tag=config
php artisan vendor:publish --provider="Silber\Bouncer\BouncerServiceProvider" --tag=migrations
php artisan migrate
This creates the abilities, roles, assigned_roles and permissions tables (the last one holds entity-scoped grants and forbidden flags).
4. Give your User model the trait.
use Silber\Bouncer\Database\HasRolesAndAbilities;
class User extends Authenticatable
{
use HasRolesAndAbilities;
}
5. First use case — bootstrap an admin and check it.
// tinker or a seeder
Bouncer::allow('admin')->to('view-users');
Bouncer::assign('admin')->to($user);
$user->isAn('admin'); // true
$user->can('view-users'); // true, also wired into Gate/@can
If that returns true, the package is installed correctly and you can build on top of it.
Seed the permission graph, don't build it in controllers. Bouncer's allow/assign calls are idempotent firstOrCreate under the hood, so a single seeder can be re-run safely:
// database/seeders/BouncerSeeder.php
public function run(): void
{
Bouncer::allow('admin')->to(['view-users', 'edit-users', 'delete-users']);
Bouncer::allow('editor')->to('edit', Post::class); // the whole model type
Bouncer::allow('editor')->to('publish', Post::class);
Bouncer::assign('admin')->to(User::where('email', '[email protected]')->first());
}
Use string constants or a backed enum for ability and role names, and reference them everywhere — a typo silently creates a new ability rather than failing.
Two distinct kinds of grants — know which you want.
Bouncer::allow($user)->to('edit', Post::class); // edit ANY post
Bouncer::allow($user)->to('edit', $post); // edit THIS post only
Bouncer::allow($user)->to('edit-post'); // a plain, entity-less ability
Checks must match the shape of the grant: $user->can('edit', $post), $user->can('edit', Post::class), $user->can('edit-post'). Post::class also works as a string in can() for type-wide grants.
Ownership. For "authors can edit their own posts", Bouncer supports ownership-scoped abilities:
Bouncer::allow($user)->toOwn('update', Post::class);
$user->can('update', $postAuthoredByThem); // true
$user->can('update', $someoneElsesPost); // false
Verify the ownership column convention against the fork's source (upstream defaults to user_id on the model) before relying on it for a custom schema.
Explicit denials beat grants. Use forbid for exceptions layered on top of roles:
Bouncer::forbid($user)->to('delete', $post);
Bouncer::unforbid($user)->to('delete', $post);
disallow is different — it removes an explicit allow for that user; it does not create a deny that overrides a role.
Wire it into the framework, then use the framework everywhere. Once the trait and Gate integration are in place, prefer the standard Laravel surfaces over calling Bouncer directly in controllers:
// routes
Route::delete('/posts/{post}', [PostController::class, 'destroy'])
->middleware('can:delete,post');
// controller
$this->authorize('edit', $post);
// Blade
@can('edit', $post) <a href="...">Edit</a> @endcan
Multi-tenancy via scope. Set the scope in middleware before any authorization runs:
public function handle($request, Closure $next)
{
Bouncer::scope()->to($request->user()->tenant_id);
return $next($request);
}
For intentional cross-tenant work (admin impersonation, scheduled jobs), wrap it:
Bouncer::scope()->onceTo($tenantId, fn () => /* ... */);
Testing. In setUp(), add Bouncer::dontCache(); after parent::setUp(), then seed only what the test needs:
Bouncer::allow('editor')->to('edit', Post::class);
Bouncer::assign('editor')->to($user = User::factory()->create());
$this->actingAs($user)->get("/posts/{$post->id}/edit")->assertOk();
Roles vs. abilities vs. policies. Keep Bouncer for coarse role/ability checks and Laravel policies for logic that needs application data ("can this post be edited after it's published?"). A policy can call $user->can('edit', $post) and then apply business rules; that composes cleanly and avoids stuffing conditional logic into the permission tables.
silber/bouncer version it forked from (git log, git diff) so you know what the fork actually changes — often it's just a Laravel-version bump. If the fork's changes are trivial, use upstream and avoid the VCS dependency entirely. Pin the commit SHA in composer.json either way.DB::table('permissions'), another process, a queue worker) won't be visible until Bouncer::refresh() or Bouncer::refreshFor($user). In Octane, Horizon, or long-running workers, either disable the cache in config or refresh explicitly at the start of each job.Bouncer::dontCache() in the base TestCase::setUp(); otherwise a grant made in one test can bleed into the next through a stale in-memory cache.Bouncer::allow('admn')->to('edit-users') creates a role named admn and grants it, and nothing errors. Wrap names in constants and add a test asserting the expected set of roles/abilities exists after seeding.can('edit-post', $post) looks plausible but doesn't match to('edit', $post). When a check unexpectedly fails, log both the grant and the check and confirm the ability name and the entity argument are identical in form.roles, abilities, assigned_roles, permissions. If you already have similarly named tables, publish the migrations and rename via the config tables keys before running migrate — renaming afterwards means hand-written data migration.Bouncer::scope() is unset (global) there. Any place that authorizes outside a request must set the scope itself or you'll get cross-tenant true results.forbid vs disallow confusion. forbid is a durable deny that wins over a role grant; disallow just deletes a specific allow. If a user "still has access after disallow," they're inheriting it from a role — use forbid, or retract the role with Bouncer::retract('admin')->from($user).$user->getRoles() and $user->getAbilities() show what the user actually resolves to (after cache); Bouncer::role()->where('name', 'admin')->first()->abilities shows the role-side grants. php artisan tinker is usually faster than reading the tables by hand.user_model and models keys let you swap in your own Role/Ability subclasses. If you need custom role metadata (e.g., a tenant_id on roles), subclass and point the config at it rather than adding columns through a separate migration that the package's queries won't see.How can I help you explore Laravel packages today?