ordermind/logical-permissions
A Laravel package for expressive, logic-based permission checks. Define roles and permissions with AND/OR/NOT rules, then authorize users against complex conditions using a clean API. Ideal for apps needing flexible access control beyond simple role checks.
This is a plain PHP library, not a Laravel package — there is no service provider, facade, config file, or migration to publish. Treat it as a pure dependency and wire it into Laravel yourself.
composer require ordermind/logical-permissions-php
Where to look first:
type + permissions keys) and the built-in gates.src/ — specifically the LogicalPermissions class (the single entry point) and the logic-gate classes. Confirm the exact method names and the custom-gate callback signature there before you write wrappers; the surface area is small enough to read in ten minutes.First use case: the "admin OR owner, but not suspended" check that everyone writes badly inside a policy. Instead of nested ifs:
$permission = [
'type' => 'and',
'permissions' => [
['type' => 'not', 'permissions' => ['suspended']],
['type' => 'or', 'permissions' => ['admin', 'owner']],
],
];
$logicalPermissions->checkPermission($permission, $context);
The value proposition is that the permission tree is data — you can store it in config, in a database column, or return it from a policy — rather than control flow. If your rules never get deeper than two levels, plain Laravel Gate closures are simpler and you should keep them.
Bind it as a singleton and register custom gates at boot.
// app/Providers/AppServiceProvider.php
$this->app->singleton(LogicalPermissions::class, function () {
$lp = new LogicalPermissions();
$lp->setDefaultLogicGate('and');
// Custom gates: majority vote, permission-level thresholds, time windows…
$lp->addLogicGate('at_least_two', function (array $permissions, array $context, callable $check) {
$passed = count(array_filter($permissions, fn ($p) => $check($p, $context)));
return $passed >= 2;
});
return $lp;
});
Bridge to Laravel's Gate so @can, middleware, and policies keep working. The library should be an implementation detail, not something controllers call directly:
Gate::before(fn ($user) => $user->isSuperAdmin() ? true : null);
Gate::define('edit-post', function (User $user, Post $post) {
$tree = config("permissions.posts.edit"); // array, not code
return app(LogicalPermissions::class)->checkPermission($tree, [
'user' => $user,
'post' => $post,
'permissions' => $user->permissionFlags(), // resolves names to booleans
]);
});
Resolve permission names to booleans in one place. The library evaluates structure; you decide what "admin" means. A single permissionFlags() method or a PermissionContext value object that maps names to booleans keeps the tree declarative and testable — the permission array becomes a pure function of the context, which makes unit tests trivial and free of actingAs().
Store trees in config, validate them at boot. config/permissions.php returning nested arrays is the sweet spot: reviewable in PRs, cacheable with php artisan config:cache, and diffable. If you must store trees in the database (per-tenant rules, admin-editable), write a recursive validator that asserts every type is a registered gate and every leaf is a string, and run it on save — otherwise a bad row becomes a runtime exception in production.
Custom gates are the real extension point. not/and/or covers maybe 80% of cases; the remaining 20% (quorum approvals, "any two of these five roles", time-of-day restrictions, tenant-scoped overrides) is where a custom gate pays for itself versus hand-written policy methods.
Keep policies thin. A policy method that reads return $this->lp->checkPermission($tree, $ctx); is ideal. Resist the urge to mix imperative checks and tree checks in the same method — reviewers can't reason about the combination.
config/logical-permissions.php, no auto-discovery. Every convenience in this list is code you own and must test.type falls back to whatever the library defaults to — and mixed behaviour between "list of permissions" and "single permission" is a subtle source of bugs. Set it once at boot and document it.not gate guarding a dangerous action. Verify the bypass semantics against the README before relying on it, and prefer Laravel's Gate::before for the super-admin case so the bypass is visible in one place instead of buried in the library.php artisan command or a test that iterates config('permissions')) so malformed config fails CI instead of production.if statement. Trees come from config or validated admin input only.(tree, user) pair.Gate closures are better reviewed, better documented, and better supported — this library's whole value is turning access rules into data, and if you don't need that, you're paying an integration cost for nothing.How can I help you explore Laravel packages today?