Skip to content

Conversation

@binaryfire
Copy link
Contributor

Problem

The without_middleware route option doesn't work correctly when trying to exclude middleware that's inside a middleware group.

  // This doesn't work - VerifyCsrfToken is inside 'web' group
  Route::post('/broadcasting/auth', $handler, [
      'middleware' => ['web'],
      'without_middleware' => [VerifyCsrfToken::class]
  ]);

Why it fails: The exclusion was applied at route registration time using array_diff():

  // RouteCollector::addRoute() - old behavior
  $middleware = array_diff(['web'], [VerifyCsrfToken::class]);
  // Result: ['web'] - no change! Strings don't match.

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:

Stage Before After
Registration array_diff() immediately Store middleware and exclusions separately
Request time Groups already filtered Expand groups, THEN apply exclusions
  // Route registration
  Route::post('/test', $handler, [
      'middleware' => ['web'],
      'without_middleware' => [VerifyCsrfToken::class]
  ]);

  // At request time:
  // 1. Get middleware: ['web']
  // 2. Get exclusions: [VerifyCsrfToken::class]  
  // 3. Expand groups: ['web'] → [StartSession, VerifyCsrfToken, ShareErrors]
  // 4. Apply exclusions: filter out VerifyCsrfToken
  // 5. Final: [StartSession, ShareErrors]

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 MiddlewareExclusionManager
  • HasMiddleware::getMiddlewareForRequest() - Fetch exclusions and pass to resolver
  • HasMiddleware::resolveMiddleware() - Apply exclusions after group expansion
  • HasMiddleware::expandExcludedMiddleware() - New method to resolve aliases/groups in exclusions

Tests:

  • MiddlewareExclusionManagerTest - Unit tests for the new manager
  • RouteCollectorTest - Updated to verify new storage behavior
  • KernelTest - Integration tests for exclusion scenarios (from groups, with aliases, excluding entire groups)

Features

The exclusion system supports:

  1. Excluding from groups - Main use case
    ['middleware' => ['web'], 'without_middleware' => [VerifyCsrfToken::class]]
  2. Excluding via alias - If exclusion is an alias, it's resolved
    ['middleware' => ['web'], 'without_middleware' => ['csrf']] // 'csrf' alias resolved
  3. Excluding entire groups - All middleware in the group are excluded
    ['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.

Copy link

Copilot AI left a 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 MiddlewareExclusionManager to 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.

Comment on lines +159 to +179
// 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;
Copy link

Copilot AI Dec 18, 2025

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.

Suggested change
// 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;

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

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.

@albertcht albertcht added the enhancement Improved feature or adjustments. label Dec 18, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement Improved feature or adjustments.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants