gepur-it/access_manager
A Laravel package for managing application access control, with tools for defining permissions, roles, and access rules through a simple, configurable API. It helps centralize authorization logic and keep access policies maintainable.
Install and publish
composer require gepur-it/access_manager
php artisan vendor:publish --provider="GepurIt\AccessManager\AccessManagerServiceProvider"
php artisan migrate
The publish step is the important one — this package ships migrations and a config file, and you almost always end up editing both. Publish them in the same commit so the schema you deploy matches what your code expects.
Pin the version and record what you actually got
The package is at v5.1.0 as of this writing. That release only touched the Composer constraint (PHP >=7.3), so it is not a feature release — but it does confirm the constraint file is still being edited while the PHP code may not be. Commit your composer.lock and note the resolved version in the PR description. When you come back in six months, "which version of access_manager is this code written against" is the first question you'll ask.
Where to look first
vendor/gepur-it/access_manager/composer.json — read the illuminate/* constraint before anything else. The PHP constraint tells you nothing about Laravel support; this file does. If it says ^7.0|^8.0, the package will refuse to install on Laravel 10 and you need to know that before you write a single line of code.config/access_manager.php — this is the source of truth. Look for the user model class, the table names, the middleware alias, and any cache TTL. Nearly every "why isn't this working" question resolves here.User model (add it before you touch anything else).role_id vs role, pivot table names) rather than guessing from the README. The README has drifted from the code.Attach the trait
// app/Models/User.php
use GepurIt\AccessManager\Traits\HasAccess; // confirm exact namespace in vendor/
class User extends Authenticatable
{
use HasAccess;
}
First use case: gate a route, not a view
Start with middleware on one route group rather than sprinkling checks across controllers. It gives you a single place to verify the whole chain (user → role → permission) is wired correctly:
Route::middleware(['auth', 'access:orders.manage'])->group(function () {
Route::resource('orders', OrderController::class);
});
Seed one role, one permission, assign it to your own user, hit the route, and confirm a 403 for a user without it. If that works, everything else is variations on the theme.
Seed roles and permissions as data, not code
The single biggest maintainability decision with any RBAC package is whether permissions live in a seeder/config or in an admin UI. For a package this small, keep them in a seeder checked into the repo and make role assignment the only runtime-editable thing:
// database/seeders/AccessSeeder.php
$roles = [
'admin' => ['orders.view', 'orders.manage', 'users.manage'],
'manager' => ['orders.view', 'orders.manage'],
'viewer' => ['orders.view'],
];
This makes permission names greppable, reviewable in PRs, and diffable across environments. Runtime permission creation on a package this small is a debugging liability.
Centralise permission names as constants
String literals in middleware, Blade, and policies drift. One class:
final class Perm
{
public const ORDERS_VIEW = 'orders.view';
public const ORDERS_MANAGE = 'orders.manage';
}
Then 'access:' . Perm::ORDERS_MANAGE in routes. Costs nothing, prevents typos that silently 403 in production.
Layer the checks by cost
Use middleware for route-level gates, Blade directives for UI affordances, and direct checks only inside controllers for row-level logic:
@access(\App\Support\Perm::ORDERS_MANAGE)
<a href="{{ route('orders.create') }}">New order</a>
@endaccess
Hide the button and protect the route. Never rely on the Blade directive alone — it's cosmetic.
Pair with Policies for ownership
RBAC answers "can this role do X at all"; it doesn't answer "can this user edit this order". Keep those separate:
public function update(User $user, Order $order): bool
{
return $user->hasAccess(Perm::ORDERS_MANAGE)
&& ($user->isAdmin() || $order->user_id === $user->id);
}
Trying to encode ownership into permission names is how RBAC setups rot.
Eager-load roles in list views
Any admin screen listing users and their roles will N+1 unless you eager-load the relationship the trait defines. Do it in the repository/query, not the Blade template.
A permissive PHP constraint is not a compatibility guarantee
v5.1.0 relaxes the requirement to php >=7.3 — no upper bound. That means Composer will happily install this package on PHP 8.3 or 8.4 even though nobody has run the test suite there, and it will also install it on a 7.3 box that may not have the Laravel version you're on. Do not read "it installed cleanly" as "it works." The constraint is a floor, not a support matrix.
Practical mitigations:
php artisan migrate and one middleware assertion on your actual PHP + Laravel pair before committing to the package.config.platform.php in composer.json to your real production version so local resolution matches CI and prod.$dates, implicit nullable return types, strftime, dynamic properties) — the code predates 8.1.Laravel version compatibility is still the real risk
The v5.1.0 note says nothing about illuminate/*. The original release targeted Laravel 7/8, and the PHP constraint change doesn't alter that. On Laravel 9+ expect friction: doctrine/dbal removal, the $dates → $casts migration, container binding changes, and PHP 8.1 deprecations in return types. If composer require refuses outright, that's the loud, good failure — you know immediately. If it installs but the middleware throws, the fix is usually two or three lines in a forked service provider. Fork it into app/Support/ or a private VCS repo rather than waiting on upstream — the project is small enough that you are effectively the maintainer for your copy.
Cache invalidation is the #1 production bug
RBAC packages cache the permission map per user. When you assign a role via a seeder, tinker, or admin panel, the old map persists until TTL expiry or an explicit flush. Symptom: "I gave them the role, they still get 403." Fix: call the package's cache-clear method (check the trait/provider for flushCache/clearCache) inside whatever service assigns roles, and always flush in test setUp().
Check the cache driver in production
If the package uses Cache::remember with the default driver, CACHE_DRIVER=array in a queue worker means each worker computes its own map — fine functionally, but role changes mid-job won't apply. Use redis or database for the cache store if you're mutating roles at runtime.
Watch for name collisions
If you already have spatie/laravel-permission, or ever plan to, don't install both. Table names (roles, permissions, role_user) and trait method names (hasRole, hasPermission, assignRole) collide directly. Pick one. For a new project on Laravel 10/11, Spatie is the safer call; take this package only if you're inheriting a codebase already on it or want something deliberately minimal.
Migrations are published, so they're yours
Once published, php artisan migrate:fresh uses your copy. That's a feature — add indexes on the pivot table's user_id and role_id columns, since every request does a lookup there. The stock migration may not have them.
Extension points worth knowing
Testing
Use RefreshDatabase plus a seeded permission set, and build factory states (->forRole('manager')) so tests read as intent rather than setup. Add one test asserting a permission-less user gets 403 on every protected route — it's cheap and catches the entire class of "someone removed the middleware" regressions. Run this suite against the lowest PHP version you claim to support, not just your local one — the >=7.3 floor means Composer will let you deploy somewhere the code has never executed.
How can I help you explore Laravel packages today?