Skip to content

Route middleware

Warrant registers a warrant route middleware. You rarely write the raw string yourself — build it with Warrant\WarrantMiddleware.

Gate a route by schema key — no model involved:

use Warrant\WarrantMiddleware;
Route::post('/documents', [DocumentController::class, 'store'])
->middleware(WarrantMiddleware::canCreate('documents'));

Under the hood canCreate('documents') produces the middleware string warrant:documents,create.

Gate by a route-model-bound parameter. The parameter must resolve to a model instance; Warrant finds its schema from the model’s class:

Route::get('/documents/{document}', [DocumentController::class, 'show'])
->middleware(WarrantMiddleware::string('document', 'view'));

Shortcuts for the common verbs. Each takes the target and an optional route-group closure:

WarrantMiddleware::canView('documents');
WarrantMiddleware::canCreate('documents');
WarrantMiddleware::canUpdate('documents');
WarrantMiddleware::canDelete('documents');
WarrantMiddleware::canArchive('documents');
WarrantMiddleware::canManage('documents'); // uses the literal ability 'manage'

guard() (and the helper closures) wrap a group of routes in one check:

WarrantMiddleware::guard('documents', 'view', function () {
Route::get('/documents', [DocumentController::class, 'index']);
Route::get('/documents/{document}', [DocumentController::class, 'show']);
});
// Same, via a helper:
WarrantMiddleware::canView('documents', function () {
Route::get('/documents', [DocumentController::class, 'index']);
});

string() and guard() accept an AbilityMatchMode. The mode segment is only added to the middleware string when it isn’t the default ALL:

use Warrant\AbilityMatchMode;
WarrantMiddleware::string('document', ['view', 'approve'], AbilityMatchMode::ANY);
// -> warrant:document,any,view,approve

Alongside the row-level checks, the reachability questions have matching guards — gate a section by whether the user could ever act, or short-circuit a route to those who provably never can:

// Reachable only if the user could ever view a document — otherwise 403:
Route::get('/documents', ...)->middleware(WarrantMiddleware::couldEver('documents', 'view'));
// Only when the ability is guaranteed by the rules' shape:
WarrantMiddleware::always('documents', 'create', fn () => Route::post('/documents', ...));
// Only when the user provably never can (e.g. an upsell page):
Route::get('/upgrade', ...)->middleware(WarrantMiddleware::never('documents', 'approve'));

These are target-free — the first argument is always a schema key (or a schema/model class), never a route parameter, because reachability has no row to bind. Unlike warrant:, the mode and match mode live in the middleware alias rather than the parameters, so everything after the colon is just the schema key and abilities (and an ability may safely be named any or all):

warrant.could-ever:documents,view warrant.could-ever.any:documents,view,approve
warrant.always:documents,view warrant.always.any:documents,view,approve
warrant.never:documents,view warrant.never.any:documents,view,approve

When the middleware runs, it:

  1. Tries to resolve the target as a schema key (capability / no-model path).
  2. If that fails, treats the target as a route parameter name, resolves it to a model, and finds the schema from the model’s class.
  3. Parses the segment after the target: all / any becomes the match mode; anything else is the first ability.
  4. Calls authorize — the throwing check — aborting 403 when the user is unauthenticated or lacks the abilities. Because it goes through authorize, a denial on a model-bound route carries the responsible rule’s denial message instead of a bare status.