dentro/yalr
YALR (Yet Another Laravel Roles) is a lightweight roles and permissions package for Laravel. Assign roles to users, attach permissions to roles, and check abilities through simple methods, middleware, and Blade directives.
composer require dentro/yalr
php artisan vendor:publish --tag=yalr # config/yalr.php
Check the compatibility matrix before you start. Releases are cut primarily to track Laravel majors (v1.6.0 is nothing but Laravel 13 support), so the yalr version you install is effectively pinned to your Laravel version. Upgrade Laravel and the package in the same PR, not separately.
Three things to read, in this order:
config/yalr.php — the only real configuration surface. It defines which directories get scanned, the default middleware stack applied to attribute-discovered routes, and whether the compiled route manifest is cached.src/Attributes/ directory in the vendor package — this is the actual API surface. The attribute classes are tiny and self-documenting; reading them is faster and more accurate than trusting the README (which lags the code on a package this young).php artisan route:list — your ground truth for everything else.Point the config at a directory of controllers:
// config/yalr.php
return [
'paths' => [
app_path('Http/Controllers/Api'),
],
'middleware' => ['web'],
'cache' => env('APP_ENV') === 'production',
];
First use case: convert one small, self-contained controller — a health check or settings controller — before touching anything auth-related. Annotate it:
namespace App\Http\Controllers\Api;
use DigitalEntropy\Yalr\Attributes\Get;
use DigitalEntropy\Yalr\Attributes\Group;
#[Group(prefix: 'api', middleware: ['api'], name: 'api.')]
class HealthController
{
#[Get('/health', name: 'health')]
public function show()
{
return response()->json(['ok' => true]);
}
}
Then php artisan route:list --path=api/health. If the route shows up with the right URI, verb, middleware, and name, the integration works and you can scale up. If it doesn't, the failure is almost always a path/namespace mismatch, not the attributes.
One controller = one resource group. The natural unit of work is a class annotated with a class-level #[Group] carrying prefix, middleware, name, and optionally domain. Methods carry the verb attribute. This maps cleanly onto Laravel resource controllers and means a PR that adds an endpoint touches exactly one file plus its test.
Middleware layering. Put shared concerns (auth:sanctum, throttle:60,1, tenant resolution) on the class attribute, and one-offs on the method. Method-level attributes should override, not accumulate — verify the merge semantics early, because double-applying auth twice on a route is harmless but double-applying a rate limiter or a tenancy scope is not.
Split roots by audience, not by feature. Register separate directories for Controllers/Api, Controllers/Web, Controllers/Admin, each with its own default middleware and prefix in config. This gives you the equivalent of three Route::middleware(...)->group() blocks without a single line of route file, and it makes "which stack does this endpoint run under?" answerable by looking at the namespace.
Coexistence with routes/web.php. Keep conventional route files for anything dynamic or closure-based (webhooks with odd signatures, catch-all fallbacks, redirect tables). Register YALR roots after your explicit routes so that wildcard fallbacks declared conventionally still win. Do not register the same controller directory from both a route file and the YALR config — you will get duplicate route names and route:list will look like a bug.
Route model binding works normally as long as the parameter name in the path matches the typed argument:
#[Get('/users/{user}', name: 'users.show')]
public function show(User $user) { /* implicit binding */ }
Deployment/CI pattern. The reflection scan is boot-time work. Enable the cache in production and make the build step explicit:
php artisan yalr:cache # or route:cache, depending on version
php artisan route:list --json > routes.json # artifact for diffing
Committing the generated route manifest as a build artifact is worth it: it turns "an attribute silently stopped registering" into a reviewable diff instead of a 404 in production.
Testing is unchanged. Attribute routing is compile-time sugar — $this->get(route('api.users.show', $user)) behaves identically. The one thing worth adding is a smoke test that asserts the total route count or a handful of critical route names exist, so a broken scan fails CI rather than staging.
route:list before touching the attribute.paths contains app/Http/Controllers/Api, the scanner expects App\Http\Controllers\Api\*. A missing or renamed subdirectory silently yields zero routes — a favorite cause of "it worked yesterday."/users/me before /users/{user} is not something you can guarantee) or add explicit where constraints.local with caching on, editing an attribute does nothing until you clear. Set cache => false locally and stop wondering why your edits aren't live."dentro/yalr": "^1.6", read the changelog on upgrade, and treat a Laravel major upgrade as "wait for the matching yalr release" rather than assuming forward compatibility.php artisan route:list -v for middleware stacks, --path= to isolate, and a plain dd() inside the scanner if you need to confirm which files were discovered. There is no dedicated debug command.middleware arrays.PATCH /orders/{order} lives in OrderStatusController.How can I help you explore Laravel packages today?