Three registration styles, matching the three things Laravel's router and kernel can do with middleware.
public function hasMiddlewareAliases(array $aliases): staticpublic function hasMiddlewareGroups(array $groups): staticpublic function hasMiddlewareGlobals(array $middlewares): static
Aliases
An alias is a short name a route can refer to. It costs nothing until a route uses it, which makes it the right default for a package.
use Acme\Blog\Http\Middleware\{EnsureUserIsAuthor, VerifyBlogToken}; $packager ->name('Blog') ->hasMiddlewareAliases([ 'blog.author' => EnsureUserIsAuthor::class, 'blog.token' => VerifyBlogToken::class, ]);
Route::middleware(['api', 'blog.token'])->group(function () { Route::post('/posts', [PostController::class, 'store'])->middleware('blog.author');});
Consumers can use the alias too, on their own routes — which is the point.
Prefix your aliases. auth is taken; blog.auth is not. A package that registers a bare alias
will overwrite whatever the application had under that name, silently, and the failure surfaces
somewhere completely unrelated.
Groups
Pushes middleware onto an existing group, or creates a new one:
$packager->hasMiddlewareGroups([ 'web' => [TrackBlogVisit::class], 'blog' => [ VerifyBlogToken::class, EnsureUserIsAuthor::class, ],]);
Route::middleware('blog')->group(function () { // …});
The middleware is appended with pushMiddlewareToGroup(), so it runs after whatever the group
already contained.
Repeated calls accumulate per group rather than replacing it:
$packager ->hasMiddlewareGroups(['blog' => [VerifyBlogToken::class]]) ->hasMiddlewareGroups(['blog' => [EnsureUserIsAuthor::class]]); // blog => [VerifyBlogToken, EnsureUserIsAuthor]
Pushing into web affects the whole application
Every request through the web group now runs your middleware — including pages that have nothing
to do with your package. Reach for it only when the behaviour genuinely is application-wide (request
tracking, a locale switch your package owns), and make it configurable when you can:
$packager->when( config('blog.track_visits', false), fn (Packager $p) => $p->hasMiddlewareGroups(['web' => [TrackBlogVisit::class]]),);
Global middleware
Pushed onto the HTTP kernel, so it runs on every request, before routing:
$packager->hasMiddlewareGlobals([ ForceJsonResponse::class,]);
Almost never the right choice for a package
Global middleware runs on every request in the host application, including routes owned by other packages and by the application itself. It cannot be excluded per route and cannot be turned off without editing your package. Very few packages have a legitimate need for it — a security package enforcing a header, perhaps.
If your middleware needs to run for your package's routes, put it in your route file. If it needs to run for the application's routes, push it into a group and let the consumer decide.
Writing middleware
namespace Acme\Blog\Http\Middleware; use Closure;use Illuminate\Http\Request;use Symfony\Component\HttpFoundation\Response; class EnsureUserIsAuthor{ public function handle(Request $request, Closure $next): Response { abort_unless($request->user()?->isBlogAuthor(), 403, __('blog::messages.not_an_author')); return $next($request); }}
With parameters
public function handle(Request $request, Closure $next, string $ability = 'read'): Response{ abort_unless($request->user()?->tokenCan("blog:{$ability}"), 403); return $next($request);}
Route::middleware('blog.token:write')->post('/posts', …);
Making registration configurable
A consumer who wants to swap your middleware for their own should not have to fork the package:
return [ 'middleware' => [ 'author' => \Acme\Blog\Http\Middleware\EnsureUserIsAuthor::class, ],];
$packager ->name('Blog') ->hasConfig() ->registeredPackage(function (Packager $packager) { $packager->hasMiddlewareAliases([ 'blog.author' => config('blog.middleware.author'), ]); });
The registeredPackage hook runs after your config is merged, so the value is available — but note
that it also runs after register() has already done its work, and middleware is booted later, so
the alias still lands in time.
Ordering
The toolkit registers aliases, then groups, then globals — in that order, all inside
bootMiddleware(). It does not attempt to control Laravel's own middleware priority. If your
middleware must run before or after a framework one, the consumer sets that in their
bootstrap/app.php:
->withMiddleware(function (Middleware $middleware) { $middleware->priority([ \Illuminate\Session\Middleware\StartSession::class, \Acme\Blog\Http\Middleware\TrackBlogVisit::class, \Illuminate\Routing\Middleware\SubstituteBindings::class, ]);})
Document the requirement rather than trying to enforce it.
Introspection
$packager->isSetMiddlewareAliases(); // bool$packager->getMiddlewareAliases(); // ['alias' => Middleware::class]$packager->isSetMiddlewareGroups(); // bool$packager->getMiddlewareGroups(); // ['group' => [Middleware::class, …]]$packager->isSetMiddlewareGlobals(); // bool$packager->getMiddlewareGlobals(); // [Middleware::class, …]
Testing
use Illuminate\Routing\Router; test('the package registers its middleware alias', function () { expect(app(Router::class)->getMiddleware()) ->toHaveKey('blog.author');}); test('the package pushes middleware into its group', function () { expect(app(Router::class)->getMiddlewareGroups()['blog']) ->toContain(VerifyBlogToken::class);});