-
-
Notifications
You must be signed in to change notification settings - Fork 14
feat(router): Support middleware exclusions from groups via without_middleware #295
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR implements support for excluding middleware from groups using the without_middleware route option. Previously, exclusions were applied at route registration time using array_diff(), which failed when trying to exclude middleware that existed within a middleware group (since the group name and middleware class name didn't match). The new implementation stores middleware and exclusions separately, then applies exclusions after group expansion during request handling.
Key changes:
- Introduced
MiddlewareExclusionManagerto store exclusions separately from middleware - Modified middleware resolution to apply exclusions after expanding groups and aliases
- Added support for excluding individual middleware from groups, excluding via aliases, and excluding entire groups
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
src/router/src/MiddlewareExclusionManager.php |
New manager class that stores middleware exclusions by server/route/method, mirroring the MiddlewareManager pattern |
src/router/src/RouteCollector.php |
Updated to store middleware and exclusions separately instead of applying array_diff immediately |
src/foundation/src/Http/Traits/HasMiddleware.php |
Modified to fetch exclusions and apply them after middleware group expansion; added expandExcludedMiddleware method to resolve aliases and groups in exclusions |
tests/Router/MiddlewareExclusionManagerTest.php |
Comprehensive unit tests for the new MiddlewareExclusionManager class |
tests/Router/RouteCollectorTest.php |
Updated test to verify middleware and exclusions are stored separately |
tests/Foundation/Http/KernelTest.php |
Integration tests covering exclusion from groups, with aliases, and excluding entire groups |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Check if it's an alias | ||
| if (isset($this->middlewareAliases[$middleware])) { | ||
| $expanded[] = $this->middlewareAliases[$middleware]; | ||
| continue; | ||
| } | ||
|
|
||
| // Check if it's a group - expand all middleware in the group | ||
| if (isset($this->middlewareGroups[$middleware])) { | ||
| foreach ($this->middlewareGroups[$middleware] as $groupMiddleware) { | ||
| // Resolve alias within group | ||
| if (isset($this->middlewareAliases[$groupMiddleware])) { | ||
| $expanded[] = $this->middlewareAliases[$groupMiddleware]; | ||
| } else { | ||
| $expanded[] = $groupMiddleware; | ||
| } | ||
| } | ||
| continue; | ||
| } | ||
|
|
||
| // It's a class name | ||
| $expanded[] = $middleware; |
Copilot
AI
Dec 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The expandExcludedMiddleware method doesn't parse middleware strings to extract the base name. If someone passes middleware with parameters in the exclusion list (e.g., 'throttle:60,1'), it won't match against the actual middleware name during filtering since getName() returns just the base class name without parameters.
While this may not be a common use case, the method should parse excluded middleware strings using parseMiddleware to extract the base name, similar to how middleware is parsed elsewhere in the codebase, to ensure consistent behavior and handle edge cases.
| // Check if it's an alias | |
| if (isset($this->middlewareAliases[$middleware])) { | |
| $expanded[] = $this->middlewareAliases[$middleware]; | |
| continue; | |
| } | |
| // Check if it's a group - expand all middleware in the group | |
| if (isset($this->middlewareGroups[$middleware])) { | |
| foreach ($this->middlewareGroups[$middleware] as $groupMiddleware) { | |
| // Resolve alias within group | |
| if (isset($this->middlewareAliases[$groupMiddleware])) { | |
| $expanded[] = $this->middlewareAliases[$groupMiddleware]; | |
| } else { | |
| $expanded[] = $groupMiddleware; | |
| } | |
| } | |
| continue; | |
| } | |
| // It's a class name | |
| $expanded[] = $middleware; | |
| // Normalize the middleware string (e.g. strip parameters) before resolving | |
| $parsed = $this->parseMiddleware($middleware); | |
| $name = $parsed->getName(); | |
| // Check if it's an alias | |
| if (isset($this->middlewareAliases[$name])) { | |
| // Normalize the aliased middleware as well | |
| $aliasedParsed = $this->parseMiddleware($this->middlewareAliases[$name]); | |
| $expanded[] = $aliasedParsed->getName(); | |
| continue; | |
| } | |
| // Check if it's a group - expand all middleware in the group | |
| if (isset($this->middlewareGroups[$name])) { | |
| foreach ($this->middlewareGroups[$name] as $groupMiddleware) { | |
| // Normalize each middleware in the group | |
| $groupParsed = $this->parseMiddleware($groupMiddleware); | |
| $groupName = $groupParsed->getName(); | |
| // Resolve alias within group using normalized name | |
| if (isset($this->middlewareAliases[$groupName])) { | |
| $aliasedGroupParsed = $this->parseMiddleware($this->middlewareAliases[$groupName]); | |
| $expanded[] = $aliasedGroupParsed->getName(); | |
| } else { | |
| $expanded[] = $groupName; | |
| } | |
| } | |
| continue; | |
| } | |
| // It's a direct middleware reference; use its normalized name | |
| $expanded[] = $name; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@albertcht The issue Copilot is point out already existed for without_middlware. I don't think accepting invalid parameters and mutating them to fix them is a good idea. Instead, I've added a step to throw an exception if : is in the value that's passed in.
Problem
The
without_middlewareroute option doesn't work correctly when trying to exclude middleware that's inside a middleware group.Why it fails: The exclusion was applied at route registration time using
array_diff():The exclusion happens BEFORE middleware groups are expanded, so excluding a class from a group name has no effect.
Solution
Store middleware and exclusions separately, then apply exclusions AFTER group expansion during request handling.
New flow:
Changes
New class:
Hypervel\Router\MiddlewareExclusionManager- Stores exclusions by server/route/method (mirrors MiddlewareManager pattern)Modified:
RouteCollector::addRoute()- Store middleware without filtering, store exclusions in MiddlewareExclusionManagerHasMiddleware::getMiddlewareForRequest()- Fetch exclusions and pass to resolverHasMiddleware::resolveMiddleware()- Apply exclusions after group expansionHasMiddleware::expandExcludedMiddleware()- New method to resolve aliases/groups in exclusionsTests:
MiddlewareExclusionManagerTest- Unit tests for the new managerRouteCollectorTest- Updated to verify new storage behaviorKernelTest- Integration tests for exclusion scenarios (from groups, with aliases, excluding entire groups)Features
The exclusion system supports:
['middleware' => ['web'], 'without_middleware' => [VerifyCsrfToken::class]]['middleware' => ['web'], 'without_middleware' => ['csrf']] // 'csrf' alias resolved['middleware' => ['web', 'api'], 'without_middleware' => ['api']]Performance
No impact - middleware resolution is already cached per route/method. The exclusion filtering only runs once on first request to each route, then the result is cached.
Backward Compatibility
The API is unchanged - without_middleware route option works the same way, it just actually works now with groups.