patrickhanna/laravel-warrant
Laravel Warrant is a lightweight authorization package for issuing and validating signed, expiring warrants that grant access to specific actions or resources. It keeps permission checks explicit and secure, making capability-style access easy to integrate into Laravel apps.
Install and publish the package assets:
composer require patrickhanna/laravel-warrant
php artisan vendor:publish --provider="Warrant\WarrantServiceProvider"
php artisan migrate
Add the ability trait to the models that should hold permissions (typically User, plus whatever you scope against — Team, Project, Post):
use Warrant\Concerns\HasWarrantAbilities;
class User extends Authenticatable
{
use HasWarrantAbilities;
}
Where to look first: the published config/warrant.php (table names, morph map, cache settings) and the package migrations. Since the package is small and low-adoption, read src/ before trusting any blog post — the source is a 10-minute read and is the real documentation.
First use case: replace a hardcoded gate with a database-defined ability. Instead of:
Gate::define('post.publish', fn (User $user) => $user->is_admin);
seed an ability and check it the same way you already check gates:
Warrant::createAbility('post.publish');
// later, anywhere
if ($user->can('post.publish')) { ... }
The win is that the ability list, and who holds it, becomes data you can change per environment (or per tenant) without a deploy.
Define abilities in a seeder, not in code. Keep ability names in one place and make the seeder idempotent (firstOrCreate on the slug) so it's safe to re-run:
foreach (['post.view', 'post.create', 'post.update', 'post.publish', 'post.delete'] as $name) {
Warrant::createAbility($name);
}
Roles as bundles, abilities as the unit of check. Assign abilities to roles, roles to users, and always authorize against the ability — never the role — in application code. That way adding a role later doesn't require touching controllers.
$editor = Warrant::createRole('editor');
$editor->attachAbilities(['post.create', 'post.update']);
$user->assignRole('editor');
Instance-scoped checks are the reason to use this package. The valuable pattern is granting an ability on a specific record rather than globally:
$user->allow('post.update', $post);
$user->can('post.update', $post); // true
$user->can('post.update', $otherPost); // false
This maps cleanly onto team/multi-tenant setups: give the user post.update scoped to $team, and let the policy translate a request for a Post into a check against $post->team.
Bridging into existing Policies. Your policies still do the domain reasoning; Warrant answers "is this person permitted in principle." A typical policy method:
public function update(User $user, Post $post): bool
{
return $user->can('post.update', $post->team)
&& ! $post->is_locked;
}
Keep authorize() and Gate in your controllers — the package should be an implementation detail of Gate::allows, not a parallel authorization path.
Route middleware and Blade for the coarse checks, policies for the fine ones:
Route::post('/posts/{post}/publish', PublishController::class)
->middleware('can:post.publish,post');
Testing. Seed the abilities you need inside the test rather than loading the production seeder, and assert both the positive and negative case — permission systems fail open more often than they fail loudly:
$user->allow('post.update', $post);
$this->actingAs($user)->put("/posts/{$post->id}")->assertOk();
false, not an exception. A typo in 'post.publish' is indistinguishable from "not permitted." Centralize ability names as constants or an enum and reference those everywhere.config/warrant.php), re-running seeders or editing rows directly in the DB will look like a no-op. Clear the cache after any out-of-band change, and in tests, flush between cases.$user->can('post.update', $post) check will fire a query per row unless assignments are eager loaded or the check falls back to a cached role map. Load the user's roles/abilities once per request.Relation::enforceMorphMap() before seeding any Warrant rows. Changing morph aliases later orphans existing assignments.Gate::before that grants everything to your admin role, rather than granting every ability individually — it keeps the ability table honest about what normal users can do.dependents: 0 there's effectively no community knowledge base; expect to read the source to answer your own questions. Treat the package as a small piece of vendored infrastructure: pin an exact version in composer.json, and cover your authorization matrix with tests so a future Laravel upgrade that breaks the package shows up as a red test rather than a production hole.How can I help you explore Laravel packages today?