Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Bouncer Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

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.

Implementation Patterns

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.

Gotchas and Tips

  • Fork risk is the headline issue. Zero dependents and no Packagist listing means no ecosystem pressure to keep it current with Laravel releases. Diff it against the upstream 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.
  • Caching will make you doubt your own code. Bouncer caches a user's roles/abilities for the request. Any write done outside the package's API (raw 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.
  • Cache + tests = flaky tests. Call 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.
  • Role/ability name typos fail silently by design. 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.
  • Mismatched ability shapes are the most common "why is this false". 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.
  • Migration conflicts. The package ships migrations that create 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.
  • Scope leaks in non-HTTP entry points. Console commands, jobs, and notifications never pass through middleware, so 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).
  • Debugging toolkit. $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.
  • Extension point: the config's 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.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels
wazza/laravel-db-encryption
hokny/laravel-generator
vonsogt/backup
jengo/queues
jeremykenedy/laravel-observability
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums