Download the PHP package elpandape/warden without Composer

On this page you can find all versions of the php package elpandape/warden. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.

FAQ

After the download, you have to make one include require_once('vendor/autoload.php');. After that you have to import the classes with use statements.

Example:
If you use only one package a project is not needed. But if you use more then one package, without a project it is not possible to import the classes with use statements.

In general, it is recommended to use always a project to download your libraries. In an application normally there is more than one library needed.
Some PHP packages are not free to download and because of that hosted in private repositories. In this case some credentials are needed to access such packages. Please use the auth.json textarea to insert credentials, if a package is coming from a private repository. You can look here for more information.

  • Some hosting areas are not accessible by a terminal or SSH. Then it is not possible to use Composer.
  • To use Composer is sometimes complicated. Especially for beginners.
  • Composer needs much resources. Sometimes they are not available on a simple webspace.
  • If you are using private repositories you don't need to share your credentials. You can set up everything on our site and then you provide a simple download link to your team member.
  • Simplify your Composer build process. Use our own command line tool to download the vendor folder as binary. This makes your build process faster and you don't need to expose your credentials for private repositories.
Please rate this library. Is it a good library?

Informations about the package warden

Warden

Warden

Roles & permissions for Laravel
Instance-level grants, explicit forbids, ownership, multi-tenancy, and ABAC.
Authorization that explains itself.

Packagist Version Total Downloads PHP 8.4+ Laravel 13+


๐Ÿ“– Table of Contents


โœจ Features

Feature Description
๐ŸŽฏ Laravel's Gate, zero learning curve can(), @can, authorize() โ€” works out of the box.
๐Ÿ”’ Explicit forbids A forbid() beats every grant. Distinguishes "denied" from "not granted."
๐Ÿ“Š whereCan() query scope The only package that can answer "over which rows?" as an Eloquent scope.
๐Ÿ” explain() debugging Know why a check resolved the way it did โ€” including "explicitly forbidden."
๐Ÿ—๏ธ ABAC constraints where('status', 'published') on grants โ€” evaluated on every check.
๐Ÿ  Ownership toOwn(Post::class) โ€” grant only what the user owns, resolved by attribute or closure.
๐ŸŽฏ Scoped roles assign('editor')->on($org) โ€” same role, different contexts.
โณ Temporary access until($moment) on a grant or an assignment โ€” it stops authorizing on its own.
๐Ÿช† Nested roles A role inside a role lends its grants, off by default and switchable live.
๐Ÿข Multi-tenancy Tenant-scoped rows with global fallback, injectable resolver, exception-safe onceTo().
๐Ÿ’พ Smart caching O(1) invalidation, versioned payloads, anti-stampede locking, Octane-safe.
๐Ÿ“ก Typed events Every write dispatches a typed event with hydrated models โ€” never raw IDs.
๐Ÿ”ข Enum support BackedEnum accepted everywhere a name string is.
๐Ÿงช Testing helpers Warden::fake(), WithPermissions trait, artisan commands.
๐Ÿ”„ Migration path warden:upgrade + Rector set for silber/bouncer users.

๐Ÿ“‹ Requirements

Requirement Version
PHP ^8.4
Laravel ^13.0

๐Ÿš€ Installation

warden:install publishes the config, the migration, and runs it. You can also publish individually:

Then add the concern to your authority model(s):

๐Ÿ”„ Coming from silber/bouncer? This package conflicts with it by design (same default tables). Run php artisan warden:upgrade to migrate the schema in place. See MIGRATING-FROM-BOUNCER.md.

โฌ†๏ธ Already on warden 1.x? 2.0 adds a column and a unique index to permissions. Publish and run the upgrade migration โ€” vendor:publish --tag=warden-migrations-v2 then migrate โ€” before the first write. See UPGRADE.md.


โšก Quick Start


๐Ÿ” Checking Permissions

Nothing to learn โ€” it's Laravel's Gate.

Grant vs Check Matrix

Grant โ†“ / Check โ†’ can('edit') can('edit', Post::class) can('edit', $post)
to('edit') โœ… โ€” โ€”
to('edit', Post::class) โ€” โœ… โœ…
to('edit', $post) โ€” โ€” โœ… that one
to('edit', '*') โ€” โœ… โœ…
to('*') โœ… โ€” โ€”
toManage(Post::class) โ€” โœ… โœ…
everything() โœ… โœ… โœ…

๐Ÿ“Œ Rules:

  • forbid() beats every Warden grant.
  • By default, Warden answers after your policies โ€” policies always win.
  • Set warden.gate.run_before_policies to make forbids veto everything.
  • Checks with more than one argument are left to your policies.
  • Guests and non-model arguments are never answered by Warden.
  • Warden::can() inside a policy recurses through the Gate. Ask the resolver directly โ€” app(Contracts\Resolver::class) โ€” when a policy needs Warden's own answer.
  • With warden.gate.register off, Warden abstains from every Gate answer, so a loose permission with no policy behind it reads as denied by every route through the Gate โ€” $user->can(), Warden::can(), cannot(), canAny(), authorize() and the warden.permission middleware all go through the same Gate. What keeps answering is the resolver, app(Contracts\Resolver::class) โ€” and your policies, wherever you have one.

๐ŸŽ Granting & Forbidding

๐Ÿ“Œ Assignments are one hop unless you turn nesting on. assign('auditor')->to($role) writes an edge between roles. By default holders of the outer role gain nothing from it โ€” set warden.roles.nested to true and they inherit the inner role's grants, to warden.roles.max_depth levels deep.

Off by default on purpose, because turning it on widens what every existing assignment reaches. The switch is read on every check rather than baked into a cached payload, so turning it back off takes effect immediately โ€” it is meant to work as an emergency lever. A cycle stops expanding at the depth ceiling instead of throwing.

can(), is() and whereIs() all nest together: a split would let can('publish') say yes while Warden::is($user)->an('editor') says no, painting a menu wrong for precisely the users with the most access.

Best Practices

โœ… Do โ€” use forbid() for exceptions:

โŒ Don't โ€” model exceptions with scattered conditionals; a forbid() row is queryable, auditable, and revocable:


โณ Temporary Access

Grants and role assignments can carry an end date. Past it they stop authorizing, stop appearing in whereCan(), and stop being listed by getPermissions() โ€” no command has to run for that to happen.

๐Ÿ“Œ The date lives on the assignment, not on the role. A role is a shared definition, so an end date there would end it for everyone. On the assignment, the same role can end on different days for different holders โ€” and when it does, the permissions that role lent go with it.

๐Ÿ“Œ until() goes before to(), like on(): writes execute immediately, so calling it afterwards throws rather than quietly doing nothing. Moving a date counts as a write โ€” it invalidates the cache and fires the same event as any other.

โš ๏ธ A forbid() cannot expire, and until() on one throws. A prohibition that lapsed by clock would turn a forbid beats every grant into until Tuesday, with the grant beneath it still live. Lift it deliberately with unforbid().

๐Ÿ“Œ A grant reached through a role outlives neither: the earlier of the two dates ends it.

๐Ÿ“Œ The boundary is exclusive. A row stops counting at the instant it names, not a tick later.

php artisan warden:clean --expired deletes rows past their date. It is hygiene, not part of the mechanism: an expired grant stops authorizing whether or not anyone runs it.


๐Ÿ  Ownership

Configure ownership resolution

๐Ÿ“Œ ownedVia() only registers; it never removes. ownedVia(Post::class, null) sets the global attribute to "App\Models\Post", which is never what you meant. Use notOwned() to take one class out, or 'default_attribute' => null in the config to turn the fallback off everywhere.

๐Ÿ“Œ A toOwn() grant against a class that resolves no ownership can never grant. The row is written and looks healthy; warden logs a warning so it is greppable.

Best Practices

โœ… Do โ€” let ownership carry the common case, forbid the exceptions:

โŒ Don't โ€” reimplement ownership inside policies you'll have to keep in sync.


๐ŸŽฏ Scoped Roles

Restrict a role to any model โ€” no global team_id required.

Configure membership resolution

๐Ÿ“Œ A restricted role's grants apply when the checked entity belongs to the context. Checks without an instance fail closed. Role membership checks (isAn('editor')) ignore restrictions by design.

Best Practices

โœ… Do โ€” model teams with the models you already have:

โŒ Don't โ€” fall back to one global role plus scattered if ($user->org_id === โ€ฆ) checks.


๐Ÿข Multi-tenancy

Behavior with no active tenant

Configure warden.scope.null_behavior:

๐Ÿ“Œ Writes always target one exact scope. A write under tenant 5 only affects tenant-5 rows. Global rules are only writable globally.

Reads and deletes are therefore asymmetric: a check under tenant 5 answers global or tenant 5, while retract() and disallow() delete tenant-5 rows only. So a retract under a tenant can succeed and leave the authority still holding the role globally. retract()->from() exposes retractedCount() for callers that need to tell the cases apart:

โš ๏ธ The scope rule is warden's, not Eloquent's. TenantScope filters reads and stamps creates; it does not isolate writes. $permission->delete() and $role->delete() reach rows in every tenant, and the foreign keys cascade below Eloquent entirely. Remove rows through warden's own verbs, or through warden:clean.

โš ๏ธ Pivot tenancy is a plain predicate, not a registered scope, so withoutGlobalScopes() does not lift it. Widen deliberately with Warden::tenant()->removeOnce(...), which is the supported escape hatch.

Under null_behavior => 'strict' it narrows instead. With no active tenant, strict reads only global rows, so removeOnce() turns (scope is null or scope = $tenant) into scope is null โ€” strictly fewer rows than the read it was meant to widen. Under the default 'all' it widens as described.

๐Ÿ“Œ Relation writes obey the rule too. detach(), sync(), toggle(), syncWithoutDetaching() and updateExistingPivot() on roles() and permissions() touch only rows at the active write scope, and attach() stamps it. A global row the tenant inherits stays out of reach in both directions: under tenant 5, sync([$role]) adds the tenant-5 row beside the global one instead of adopting it, and sync([]) leaves the global one standing.

โš ๏ธ Scope, yes; restriction, no. A relation write narrows to one scope and stops there: it does not filter restricted_to_*, so $user->roles()->detach($editor) removes the scoped-role assignments along with the plain one. That mirrors Warden::retract('editor')->from($user) without ->on(), which deletes them all the same way โ€” the relation is not narrower than the verb it reflects. To remove one context and leave the others, name it: Warden::retract('editor')->on($org)->from($user).

โš ๏ธ A relation captures its write scope when it is built, not when it writes. $user->roles() resolves the active tenant at construction time, so a relation held in a property across a tenant change still writes to the scope it was born in. Ask for it again after switching tenants, or write through Warden::assign() / retract(), which resolve the scope per call โ€” and which also dispatch the typed events and report retractedCount().

๐Ÿ“Œ A role is global unless the write mints a tenant one. Under an active tenant, allow('editor') attaches to a global editor if one exists, rather than creating a tenant-scoped twin. Roles are looked up by name and scope; a tenant twin only exists once something writes it.

๐Ÿ“Œ The permission catalog behaves the same way, and in both halves a row the tenant minted for itself wins the global one it shadows. Under a tenant, allow($user)->to('publish') reuses a global publish row when that is all there is, and picks the tenant's own the moment one exists. Set Warden::tenant()->onlyRelations() to keep the catalog global on purpose.

๐Ÿ“Œ Tenancy::writeScope() takes forRoleGrant, and it defaults to false. A bare call therefore reports the scope of an authority grant; ask with forRoleGrant: true when the holder is a role, or the answer describes a different write than the one you meant.

Best Practices

โœ… Do โ€” remove a global forbid where it lives: outside any tenant:

โŒ Don't โ€” expect a tenant-scoped unforbid() to lift a global forbid.


๐Ÿ”ง Conditional Permissions (ABAC)

Grants can carry conditions, written in the grammar your queries already use:

Available operators

Method Description
where('col', 'value') Entity attribute equals value
where('col', '>=', 5) With explicit operator
whereColumn('owner_id', 'id') Compare against authority's attribute
orWhere(...) OR grouping
orWhere(fn) Nested closure grouping

๐Ÿ“Œ Important:

  • Precedence is SQL's: AND binds tighter than OR.
  • Comparisons are strict โ€” no PHP type juggling.
  • A null attribute satisfies no operator at all, != included, and values whose types are not decidably comparable fail closed the same way. A missing attribute is different: under Model::preventAccessingMissingAttributes() it throws rather than failing closed.
  • Constrained grants share one catalog row per distinct rule, so editing a permission's options changes the rule for every holder of that shape. Write a new condition instead of editing a shared row.
  • A boolean value matches only a column the model casts to bool, and such a column matches only a boolean, so writing either mismatch is refused: where('classified', true) needs 'classified' => 'bool' in the model's $casts. A row stored before 3.0 keeps failing closed in checks and in queries alike โ€” php artisan warden:doctor lists those rows.
  • The refusal exists because of what the mismatch does to a forbid(): a condition that can never be true makes the prohibition inert, the grant underneath it stays live, and explain() reports that grant without ever mentioning the forbid โ€” a missing cast read as "allowed".
  • A permission with no entity is only ever checked without an instance, so constraining one is refused: the shape that would make it match is the shape that rejects it.
  • A constrained grant never matches instance-less checks (can('view'), can('view', Document::class)) โ€” they fail closed.
  • to()->where() is two writes, not one. to() lands an unconstrained grant that authorises every instance, and where() re-points it at the constrained twin. Only the second step runs in a transaction; between the two the live row is unconditional, and a throw in where() โ€” an unknown operator, a permission with no entity โ€” leaves it that way. Wrap the whole chain in your own transaction when that window matters.

Best Practices

โœ… Do โ€” grant broadly, constrain the sensitive part:

โŒ Don't โ€” encode workflow logic as constraints (e.g., "drafts visible on Tuesdays"). Complex rules belong in policies.


๐Ÿ“Š Querying by Permission

Checks answer "can X do Y?"; Warden can also answer "over which rows?"

Instance grants, class grants, wildcards, everyone-grants, role grants, forbids, tenancy, ownership, and ABAC constraints all compile into the query.

โš ๏ธ What cannot become SQL fails closed: closure-resolved ownership and restricted-role grants contribute no rows.

โš ๏ธ No Gate, no policies. whereCan() answers from warden's own rows only. A policy that would have granted or denied a row is not consulted, so a query and a check can disagree wherever a policy has the last word.

โš ๏ธ The trait is required. Without it, Post::whereCan($user, 'view') never reaches Warden: Laravel reads it as a dynamic where against a column named can, and you get zero rows or a driver error instead of an answer.

Best Practices

โœ… Do โ€” drive index pages straight from authorization:

โŒ Don't โ€” post-filter with ->get()->filter(fn ($p) => $user->can('view', $p)) โ€” that's the N+1 this scope exists to delete.


๐Ÿ” Debugging with explain()

Which of permission and role are populated depends on the cause:

Cause allowed() permission role
GrantedDirectly true the row โ€”
GrantedViaRole true the row the role
GrantedToEveryone true the row โ€”
ForbiddenDirectly false the row โ€”
ForbiddenViaRole false the row the role
ForbiddenToEveryone false the row โ€”
ConditionsNotMet false the row whose conditions were not satisfied โ€”
NoMatchingGrant false โ€” โ€”
NotApplicable false โ€” โ€”

๐Ÿ“Œ ConditionsNotMet and NoMatchingGrant are different answers: the first names a row that matched the shape but whose conditions were not satisfied โ€” they failed against the instance, or the check named a class and there was no instance to satisfy them with โ€” while the second means nothing matched at all. Both leave Warden abstaining so your policies decide.

๐Ÿ“Œ Always answered by the database engine โ€” never from cache โ€” so it diagnoses stale-cache issues too.


๐Ÿ“ก Events

Every write dispatches a typed, readonly event with hydrated models (never raw IDs). Disable globally with warden.events_enabled.

Event Fired By Payload
PermissionGranted / PermissionForbidden allow(), forbid() ?Model $authority, Collection $permissions, $scope, ?Model $actor
PermissionRevoked / PermissionUnforbidden disallow(), unforbid() Same shape
RoleAssigned / RoleRetracted assign(), retract() Model $authority, Collection $roles, $scope, ?Model $restrictedTo, ?Model $actor
RolesSynced / PermissionsSynced sync() SyncResult diff: attached / detached / kept
RoleCreated/Deleted, PermissionCreated/Deleted Model layer The model

$actor defaults to the authenticated user. Queues, console commands and impersonation are cases only your application can answer, so point warden.actor_resolver at a class implementing Contracts\ActorResolver:

Pre-action events (opt-in)

Enable with warden.cancellable_events. A listener returning false aborts the write:

๐Ÿ“Œ A pre-action event covers the whole call, not one item of it. allow($user)->to(['a', 'b']) announces both names in one event, and a listener returning false aborts both: there is no way to veto one and keep the other. Split the call if you need per-item decisions.

๐Ÿ“Œ sync() never fires nor honors pre-action events โ€” its declarative diff events tell the whole story.

๐Ÿ“Œ Deleting a catalog row announces what the cascade was predicted to reach. A foreign key removes a permission's grants inside the engine, where no model event fires, so warden reads the doomed rows before the delete and dispatches one PermissionRevoked โ€” or PermissionUnforbidden โ€” per row afterwards. The read is the announcement: if the foreign key is not enforced, the events describe a deletion that did not happen.

๐Ÿ“Œ That cascade is blind to the active tenant. The doomed rows are read with withoutGlobalScopes(), on purpose โ€” the delete destroys every tenant's grants regardless of which one is active, so counting only the current tenant would promise a smaller loss than the real one.


โš ๏ธ Exceptions

All typed, all catchable the Laravel way:

Exception Extends Notes
RoleDoesNotExist ModelNotFoundException โ€”
PermissionDoesNotExist ModelNotFoundException โ€”
UnauthorizedException AuthorizationException getRequiredPermissions() / getRequiredRoles()
ConfigurationException โ€” Fail-fast on bad config

๐Ÿ“Œ UnauthorizedException messages are translatable (shipped in English and Spanish). Displaying the missing permission/role name in the message is opt-in via warden.exceptions.display_*.


๐Ÿ”ข Enums

Every public signature that takes a permission or role name also accepts a string-backed enum:


๐Ÿ’พ Caching

Enabled by default. One minimal payload per authority, O(1) automatic invalidation, anti-stampede locking, Octane-safe.

Manual invalidation

Best Practices

โœ… Do โ€” write through Warden and let invalidation take care of itself:

โŒ Don't โ€” raw database edits (seeders, manual SQL) bypass invalidation. After hand-editing rows, call Warden::refresh() โ€” or better, make the edit through the API.

๐Ÿ“Œ "Through the API" includes the models. Editing a Grant, an AssignedRole or a catalog row through Eloquent invalidates too โ€” renaming a permission or rewriting its options reaches every cached check, because a permission's own columns are baked into the payload. What still needs Warden::refresh() is a write that fires no model event: the query builder, DB::table(), and a raw statement.

โš ๏ธ The in-memory matcher compares permission names byte-exactly, while a case-insensitive database collation may match Edit to edit. Use exact, consistent names.


๐Ÿงช Testing

Fake mode

A scripted rule answers for every authority unless you narrow it. Each verb below narrows the rule scripted just before it, so they chain:

Ownership, conditions and tenancy are decided by the same pieces the database engine uses, and a test suite asserts the fake and the engine answer alike across the shapes a rule can take. Narrowing before scripting a rule throws.

๐Ÿ“Œ The fake is not looser than the engine. A rule with no entity answers entity-less checks only, a condition abstains where it has no instance to read, and an unscripted check abstains so your app's policies still decide. Where the fake cannot express something, it denies rather than granting.

WithPermissions trait

Artisan commands

๐Ÿ“Œ warden:doctor exits non-zero when it finds something, so it works as a CI gate. It reads every stored condition back through the rule the write path enforces and reports the ones that would be refused today, with the permission and how many grants and forbids point at it. It changes nothing: adding the missing cast and rewriting the condition mean different things, and only you know which one you meant.


๐Ÿ›ก๏ธ Middleware & Blade

Off by default. Enable via config:

Middleware

Blade


๐Ÿ—๏ธ Schema & Models

Four tables:

Table Purpose
permissions The catalog
roles Role definitions
assigned_roles Role โ†” authority pivot
grants Permission โ†” authority (with forbidden flag)

๐Ÿ“Œ Revoking removes the grant, never the catalog row. The row is shared, so pruning it inline would destroy a rule other holders point at. warden:clean is the supported way to reclaim rows nothing points at, and --duplicates collapses rows that identify the same permission.

๐Ÿ“Œ Both pivot relations mix granted and forbidden rows. $role->permissions() and $permission->roles() return every pivot row, whichever polarity it carries โ€” filter to read one side:

๐Ÿ“Œ Titles are generated once, on creation, and only when none was given. A rename keeps the old title, and setting title to null on an update leaves it null. Recompute one deliberately with Support\Titles\PermissionTitle::generate() or RoleTitle::generate() โ€” the same calls the hook makes.

๐Ÿ“Œ Ask before you rewrite a title. PermissionTitle::generations() and RoleTitle::generations() return every title Warden could have written for a name, current first โ€” each generator this package has published is transcribed and frozen. A stored title inside that list was Warden's; one outside it was typed by a person and is not yours to overwrite.

php artisan warden:retitle applies exactly that rule across the catalogue: a title an older Warden generated converges on the current wording, a title someone wrote stays, and a null stays null. Run it with --dry-run first.

Any model can hold roles and permissions:

Swap models via config

๐Ÿ“Œ Never hardcode package classes in relations. Always resolve via config.


โš™๏ธ Configuration

Everything lives in config/warden.php:

Section Controls
models Swappable Role, Permission, Grant, AssignedRole models
tables Table names and database connection
morphs Morph aliases (warden.role, warden.permission)
gate Gate behavior (run_before_policies, register)
ownership Global/per-class ownership attribute
scope Multi-tenancy semantics
cache Store, prefix, TTL
events Enable/disable events, cancellable pre-action events
exceptions Display permission/role names in messages

๐Ÿ“– Recipes

Authorize someone other than the current user

Ownership through a pivot table

โš ๏ธ Closure-resolved ownership cannot compile into whereCan().

Default role for new users

๐Ÿ’ก There is no "role for everyone" by design. Use Warden::allowEveryone()->to(...) for global grants.

Landlord vs tenant databases

Point warden tables at their own connection with warden.connection. The published migration honors it (Schema::connection(...)), and the migration class is anonymous to avoid collisions.

Replace a role instead of stacking

Long-lived processes (Tinker, Octane, queues)

Writes through the API invalidate caches automatically. Only raw DB edits need Warden::refresh(). Tenant state lives in container-scoped bindings, so Octane requests and queue jobs reset themselves.


๐Ÿ”„ Migrating from silber/bouncer

The fluent API is intentionally compatible. The schema upgrades in place (abilities โ†’ permissions, permissions pivot โ†’ grants). See MIGRATING-FROM-BOUNCER.md for the full equivalence table.


๐Ÿงช Development

No local PHP or Composer needed โ€” everything runs through Docker:


๐Ÿ‘ค Credits & License

Licensed under the MIT License.


Authorization that explains itself.


All versions of warden with dependencies

PHP Build Version
Package Version
Requires php Version ^8.4
illuminate/auth Version ^13.0
illuminate/contracts Version ^13.0
illuminate/database Version ^13.0
symfony/uid Version ^7.4 || ^8.0
Composer command for our command line client (download client) This client runs in each environment. You don't need a specific PHP version etc. The first 20 API calls are free. Standard composer command

The package elpandape/warden contains the following files

Loading the files please wait ...