From 53e822ddb0406d07667d1ff8c29f9bbb86337e53 Mon Sep 17 00:00:00 2001 From: klsoft-web Date: Fri, 6 Feb 2026 09:00:51 +0300 Subject: [PATCH 1/5] Remove restrictions from prependMiddleware() and middleware() methods --- CHANGELOG.md | 1 + src/Group.php | 13 ------------- src/Route.php | 26 ++++++++++++++------------ tests/GroupTest.php | 22 +++++++++------------- tests/RouteTest.php | 27 +++++++++++++++++---------- 5 files changed, 41 insertions(+), 48 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cf1093..6ad0573 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 4.0.3 under development - Enh #276: Explicitly import classes, functions, and constants in the "use" section (@rustamwin) +- Enh #277: Remove restrictions from prependMiddleware() and middleware() methods (@klsoft-web) ## 4.0.2 December 13, 2025 diff --git a/src/Group.php b/src/Group.php index fe013b5..e23092b 100644 --- a/src/Group.php +++ b/src/Group.php @@ -5,7 +5,6 @@ namespace Yiisoft\Router; use InvalidArgumentException; -use RuntimeException; use Yiisoft\Router\Internal\MiddlewareFilter; use function in_array; @@ -28,8 +27,6 @@ final class Group */ private array $hosts = []; private ?string $namePrefix = null; - private bool $routesAdded = false; - private bool $middlewareAdded = false; private array $disabledMiddlewares = []; /** @@ -58,13 +55,8 @@ public static function create(?string $prefix = null): self public function routes(self|Route ...$routes): self { - if ($this->middlewareAdded) { - throw new RuntimeException('routes() can not be used after prependMiddleware().'); - } - $new = clone $this; $new->routes = $routes; - $new->routesAdded = true; return $new; } @@ -89,10 +81,6 @@ public function withCors(array|callable|string|null $middlewareDefinition): self */ public function middleware(array|callable|string ...$definition): self { - if ($this->routesAdded) { - throw new RuntimeException('middleware() can not be used after routes().'); - } - $new = clone $this; array_push( $new->middlewares, @@ -116,7 +104,6 @@ public function prependMiddleware(array|callable|string ...$definition): self ...array_values($definition), ); - $new->middlewareAdded = true; $new->enabledMiddlewaresCache = null; return $new; diff --git a/src/Route.php b/src/Route.php index d9ab77d..0b865ac 100644 --- a/src/Route.php +++ b/src/Route.php @@ -5,12 +5,13 @@ namespace Yiisoft\Router; use InvalidArgumentException; -use RuntimeException; use Stringable; use Yiisoft\Http\Method; use Yiisoft\Router\Internal\MiddlewareFilter; use function in_array; +use function array_slice; +use function count; /** * Route defines a mapping from URL to callback / name and vice versa. @@ -197,16 +198,21 @@ public function defaults(array $defaults): self */ public function middleware(array|callable|string ...$definition): self { + $route = clone $this; if ($this->actionAdded) { - throw new RuntimeException('middleware() can not be used after action().'); + $lastIndex = count($route->middlewares) - 1; + $route->middlewares = array_merge( + array_slice($route->middlewares, 0, $lastIndex), + array_values($definition), + array_slice($route->middlewares, $lastIndex), + ); + } else { + array_push( + $route->middlewares, + ...array_values($definition), + ); } - $route = clone $this; - array_push( - $route->middlewares, - ...array_values($definition), - ); - $route->enabledMiddlewaresCache = null; return $route; @@ -227,10 +233,6 @@ public function middleware(array|callable|string ...$definition): self */ public function prependMiddleware(array|callable|string ...$definition): self { - if (!$this->actionAdded) { - throw new RuntimeException('prependMiddleware() can not be used before action().'); - } - $route = clone $this; array_unshift( $route->middlewares, diff --git a/tests/GroupTest.php b/tests/GroupTest.php index b266059..4be5c96 100644 --- a/tests/GroupTest.php +++ b/tests/GroupTest.php @@ -12,7 +12,6 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; -use RuntimeException; use Yiisoft\Middleware\Dispatcher\MiddlewareDispatcher; use Yiisoft\Middleware\Dispatcher\MiddlewareFactory; use Yiisoft\Router\Group; @@ -124,16 +123,11 @@ public function testNamedArgumentsInMiddlewareMethods(): void public function testRoutesAfterMiddleware(): void { - $group = Group::create(); - - $middleware1 = static fn() => new Response(); - - $group = $group->prependMiddleware($middleware1); - - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('routes() can not be used after prependMiddleware().'); + $group = Group::create() + ->prependMiddleware(TestMiddleware1::class) + ->routes(Route::get('/')); - $group->routes(Route::get('/')); + $this->assertSame([TestMiddleware1::class], $group->getData('enabledMiddlewares')); } public function testAddNestedMiddleware(): void @@ -439,10 +433,12 @@ public function testWithCorsWithNestedGroups2(): void public function testMiddlewareAfterRoutes(): void { $group = Group::create()->routes(Route::get('/info')->action(static fn() => 'info')); + $group = $group->middleware(TestMiddleware1::class, TestMiddleware2::class); - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('middleware() can not be used after routes().'); - $group->middleware(static fn() => new Response()); + $this->assertSame( + [TestMiddleware1::class, TestMiddleware2::class], + $group->getData('enabledMiddlewares'), + ); } public function testDuplicateHosts(): void diff --git a/tests/RouteTest.php b/tests/RouteTest.php index 506038d..be695ae 100644 --- a/tests/RouteTest.php +++ b/tests/RouteTest.php @@ -13,7 +13,6 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; -use RuntimeException; use Yiisoft\Http\Method; use Yiisoft\Middleware\Dispatcher\MiddlewareDispatcher; use Yiisoft\Middleware\Dispatcher\MiddlewareFactory; @@ -230,22 +229,30 @@ public function testDispatcherInjecting(): void $this->assertSame(200, $response->getStatusCode()); } - public function testMiddlewareAfterAction(): void + public function testMiddlewareBeforAction(): void { - $route = Route::get('/')->action([TestController::class, 'index']); + $route = Route::get('/'); + $route = $route->prependMiddleware(TestMiddleware1::class); + $route = $route->action([TestController::class, 'index']); - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('middleware() can not be used after action().'); - $route->middleware(static fn() => new Response()); + $this->assertSame( + [TestMiddleware1::class, [TestController::class, 'index']], + $route->getData('enabledMiddlewares'), + ); } - public function testPrependMiddlewareBeforeAction(): void + public function testMiddlewareAfterAction(): void { $route = Route::get('/'); + $route = $route->middleware(TestMiddleware1::class); + $route = $route->action([TestController::class, 'index']); + $route = $route->middleware(TestMiddleware2::class); + $route = $route->middleware(TestMiddleware3::class); - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('prependMiddleware() can not be used before action().'); - $route->prependMiddleware(static fn() => new Response()); + $this->assertSame( + [TestMiddleware1::class, TestMiddleware2::class, TestMiddleware3::class, [TestController::class, 'index']], + $route->getData('enabledMiddlewares'), + ); } public function testDisabledMiddlewareDefinitions(): void From 6c1a9fd5c44f40572414c16e243611f342bffa48 Mon Sep 17 00:00:00 2001 From: klsoft-web Date: Sat, 7 Feb 2026 13:18:34 +0300 Subject: [PATCH 2/5] Update src/Route.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Route.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Route.php b/src/Route.php index 0b865ac..c6a56ee 100644 --- a/src/Route.php +++ b/src/Route.php @@ -200,7 +200,7 @@ public function middleware(array|callable|string ...$definition): self { $route = clone $this; if ($this->actionAdded) { - $lastIndex = count($route->middlewares) - 1; + $lastIndex = count($route->middlewares) - 1; $route->middlewares = array_merge( array_slice($route->middlewares, 0, $lastIndex), array_values($definition), From 6a4d28a84c17425a150222c384d0b34c84722e38 Mon Sep 17 00:00:00 2001 From: klsoft-web Date: Sat, 7 Feb 2026 13:40:10 +0300 Subject: [PATCH 3/5] Refactoring --- src/Route.php | 4 ++-- tests/RouteTest.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Route.php b/src/Route.php index c6a56ee..29fb51c 100644 --- a/src/Route.php +++ b/src/Route.php @@ -8,10 +8,9 @@ use Stringable; use Yiisoft\Http\Method; use Yiisoft\Router\Internal\MiddlewareFilter; - -use function in_array; use function array_slice; use function count; +use function in_array; /** * Route defines a mapping from URL to callback / name and vice versa. @@ -195,6 +194,7 @@ public function defaults(array $defaults): self /** * Appends a handler middleware definition that should be invoked for a matched route. * First added handler will be executed first. + * If no actions have been added, the middleware is added to the end of the list. Otherwise, it is added before the action. */ public function middleware(array|callable|string ...$definition): self { diff --git a/tests/RouteTest.php b/tests/RouteTest.php index be695ae..c0c0c24 100644 --- a/tests/RouteTest.php +++ b/tests/RouteTest.php @@ -229,7 +229,7 @@ public function testDispatcherInjecting(): void $this->assertSame(200, $response->getStatusCode()); } - public function testMiddlewareBeforAction(): void + public function testPrependMiddlewareBeforeAction(): void { $route = Route::get('/'); $route = $route->prependMiddleware(TestMiddleware1::class); From f4406b3116fbf41a45aa78c4a918892ddcfa271b Mon Sep 17 00:00:00 2001 From: klsoft-web <7967163+klsoft-web@users.noreply.github.com> Date: Sat, 7 Feb 2026 10:40:46 +0000 Subject: [PATCH 4/5] Apply PHP CS Fixer and Rector changes (CI) --- src/Route.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Route.php b/src/Route.php index 29fb51c..fa7e1b2 100644 --- a/src/Route.php +++ b/src/Route.php @@ -8,6 +8,7 @@ use Stringable; use Yiisoft\Http\Method; use Yiisoft\Router\Internal\MiddlewareFilter; + use function array_slice; use function count; use function in_array; From beab7385c1b5d4e5de7b3847ae84f599433ef27b Mon Sep 17 00:00:00 2001 From: klsoft-web Date: Sun, 8 Feb 2026 12:35:42 +0300 Subject: [PATCH 5/5] Refactoring --- CHANGELOG.md | 2 +- tests/GroupTest.php | 16 ++++++++++++++-- tests/RouteTest.php | 14 +++++++------- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ad0573..9a3d3e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ ## 4.0.3 under development - Enh #276: Explicitly import classes, functions, and constants in the "use" section (@rustamwin) -- Enh #277: Remove restrictions from prependMiddleware() and middleware() methods (@klsoft-web) +- Enh #277: Remove restrictions from `prependMiddleware()` and `middleware()` methods (@klsoft-web) ## 4.0.2 December 13, 2025 diff --git a/tests/GroupTest.php b/tests/GroupTest.php index 4be5c96..016b02a 100644 --- a/tests/GroupTest.php +++ b/tests/GroupTest.php @@ -22,6 +22,7 @@ use Yiisoft\Router\Tests\Support\TestMiddleware1; use Yiisoft\Router\Tests\Support\TestMiddleware2; use Yiisoft\Router\Tests\Support\TestMiddleware3; +use Yiisoft\Router\Tests\Support\TestController; final class GroupTest extends TestCase { @@ -432,13 +433,24 @@ public function testWithCorsWithNestedGroups2(): void public function testMiddlewareAfterRoutes(): void { - $group = Group::create()->routes(Route::get('/info')->action(static fn() => 'info')); - $group = $group->middleware(TestMiddleware1::class, TestMiddleware2::class); + $group = Group::create() + ->routes(Route::get('/info') + ->middleware(TestMiddleware3::class) + ->action([TestController::class, 'index'])) + ->middleware(TestMiddleware1::class, TestMiddleware2::class); + + $collector = new RouteCollector(); + $collector->addRoute($group); + $routeCollection = new RouteCollection($collector); $this->assertSame( [TestMiddleware1::class, TestMiddleware2::class], $group->getData('enabledMiddlewares'), ); + $this->assertSame( + [TestMiddleware1::class, TestMiddleware2::class, TestMiddleware3::class, [TestController::class, 'index']], + $routeCollection->getRoute('GET /info')->getData('enabledMiddlewares'), + ); } public function testDuplicateHosts(): void diff --git a/tests/RouteTest.php b/tests/RouteTest.php index c0c0c24..44d1d74 100644 --- a/tests/RouteTest.php +++ b/tests/RouteTest.php @@ -231,9 +231,9 @@ public function testDispatcherInjecting(): void public function testPrependMiddlewareBeforeAction(): void { - $route = Route::get('/'); - $route = $route->prependMiddleware(TestMiddleware1::class); - $route = $route->action([TestController::class, 'index']); + $route = Route::get('/') + ->prependMiddleware(TestMiddleware1::class) + ->action([TestController::class, 'index']); $this->assertSame( [TestMiddleware1::class, [TestController::class, 'index']], @@ -244,10 +244,10 @@ public function testPrependMiddlewareBeforeAction(): void public function testMiddlewareAfterAction(): void { $route = Route::get('/'); - $route = $route->middleware(TestMiddleware1::class); - $route = $route->action([TestController::class, 'index']); - $route = $route->middleware(TestMiddleware2::class); - $route = $route->middleware(TestMiddleware3::class); + $route = $route->middleware(TestMiddleware1::class) + ->action([TestController::class, 'index']) + ->middleware(TestMiddleware2::class) + ->middleware(TestMiddleware3::class); $this->assertSame( [TestMiddleware1::class, TestMiddleware2::class, TestMiddleware3::class, [TestController::class, 'index']],