From a60d03bccc8f73b776393e4f3f8ba80489c497f8 Mon Sep 17 00:00:00 2001 From: Rustam Date: Tue, 28 Mar 2023 15:20:37 +0500 Subject: [PATCH 01/63] Add attributes support --- .phpstorm.meta.php/Group.php | 7 +- .phpstorm.meta.php/Route.php | 4 +- src/Attribute/Delete.php | 32 ++++ src/Attribute/Get.php | 32 ++++ src/Attribute/Head.php | 32 ++++ src/Attribute/Options.php | 32 ++++ src/Attribute/Patch.php | 32 ++++ src/Attribute/Post.php | 32 ++++ src/Attribute/Put.php | 32 ++++ src/Group.php | 111 +++++++------- src/MatchingResult.php | 31 +--- src/Middleware/Router.php | 6 +- src/Route.php | 278 +++++++++++++++++++++++----------- src/RouteCollection.php | 4 +- tests/GroupTest.php | 124 ++++----------- tests/MatchingResultTest.php | 52 ------- tests/RouteCollectionTest.php | 168 ++++++++++---------- tests/RouteTest.php | 76 +--------- 18 files changed, 604 insertions(+), 481 deletions(-) create mode 100644 src/Attribute/Delete.php create mode 100644 src/Attribute/Get.php create mode 100644 src/Attribute/Head.php create mode 100644 src/Attribute/Options.php create mode 100644 src/Attribute/Patch.php create mode 100644 src/Attribute/Post.php create mode 100644 src/Attribute/Put.php diff --git a/.phpstorm.meta.php/Group.php b/.phpstorm.meta.php/Group.php index 70eb3db3..0facbb6a 100644 --- a/.phpstorm.meta.php/Group.php +++ b/.phpstorm.meta.php/Group.php @@ -11,9 +11,8 @@ 'host', 'hosts', 'corsMiddleware', - 'items', - 'middlewareDefinitions', - 'hasDispatcher', + 'routes', + 'middlewares', 'hasCorsMiddleware' ); -} \ No newline at end of file +} diff --git a/.phpstorm.meta.php/Route.php b/.phpstorm.meta.php/Route.php index e30885ca..ae15f025 100644 --- a/.phpstorm.meta.php/Route.php +++ b/.phpstorm.meta.php/Route.php @@ -13,8 +13,6 @@ 'methods', 'override', 'defaults', - 'dispatcherWithMiddlewares', - 'hasDispatcher', 'hasMiddlewares' ); -} \ No newline at end of file +} diff --git a/src/Attribute/Delete.php b/src/Attribute/Delete.php new file mode 100644 index 00000000..acab5a4e --- /dev/null +++ b/src/Attribute/Delete.php @@ -0,0 +1,32 @@ +corsMiddleware = $corsMiddleware; } /** * Create a new group instance. * * @param string|null $prefix URL prefix to prepend to all routes of the group. - * @param MiddlewareDispatcher|null $dispatcher Middleware dispatcher to use for the group. */ public static function create( ?string $prefix = null, - MiddlewareDispatcher $dispatcher = null + array $middlewares = [], + array $hosts = [], + ?string $namePrefix = null, + array $disabledMiddlewares = [], + array|callable|string|null $corsMiddleware = null ): self { - return new self($prefix, $dispatcher); + return new self( + prefix: $prefix, + middlewares: $middlewares, + hosts: $hosts, + namePrefix: $namePrefix, + disabledMiddlewares: $disabledMiddlewares, + corsMiddleware: $corsMiddleware + ); } public function routes(self|Route ...$routes): self @@ -59,32 +70,12 @@ public function routes(self|Route ...$routes): self throw new RuntimeException('routes() can not be used after prependMiddleware().'); } $new = clone $this; - foreach ($routes as $route) { - if ($new->dispatcher !== null && !$route->getData('hasDispatcher')) { - $route = $route->withDispatcher($new->dispatcher); - } - $new->items[] = $route; - } - + $new->routes = $routes; $new->routesAdded = true; return $new; } - public function withDispatcher(MiddlewareDispatcher $dispatcher): self - { - $group = clone $this; - $group->dispatcher = $dispatcher; - foreach ($group->items as $index => $item) { - if (!$item->getData('hasDispatcher')) { - $item = $item->withDispatcher($dispatcher); - $group->items[$index] = $item; - } - } - - return $group; - } - /** * Adds a middleware definition that handles CORS requests. * If set, routes for {@see Method::OPTIONS} request will be added automatically. @@ -110,9 +101,10 @@ public function middleware(array|callable|string ...$middlewareDefinition): self } $new = clone $this; array_push( - $new->middlewareDefinitions, + $new->middlewares, ...array_values($middlewareDefinition) ); + $new->builtMiddlewares = []; return $new; } @@ -124,10 +116,11 @@ public function prependMiddleware(array|callable|string ...$middlewareDefinition { $new = clone $this; array_unshift( - $new->middlewareDefinitions, + $new->middlewares, ...array_values($middlewareDefinition) ); $new->middlewareAdded = true; + $new->builtMiddlewares = []; return $new; } @@ -167,9 +160,10 @@ public function disableMiddleware(mixed ...$middlewareDefinition): self { $new = clone $this; array_push( - $new->disabledMiddlewareDefinitions, + $new->disabledMiddlewares, ...array_values($middlewareDefinition), ); + $new->builtMiddlewares = []; return $new; } @@ -178,10 +172,10 @@ public function disableMiddleware(mixed ...$middlewareDefinition): self * @psalm-param T $key * @psalm-return ( * T is ('prefix'|'namePrefix'|'host') ? string|null : - * (T is 'items' ? Group[]|Route[] : + * (T is 'routes' ? Group[]|Route[] : * (T is 'hosts' ? array : - * (T is ('hasCorsMiddleware'|'hasDispatcher') ? bool : - * (T is 'middlewareDefinitions' ? list : + * (T is 'hasCorsMiddleware' ? bool : + * (T is 'middlewares' ? list : * (T is 'corsMiddleware' ? array|callable|string|null : mixed) * ) * ) @@ -197,23 +191,28 @@ public function getData(string $key): mixed 'host' => $this->hosts[0] ?? null, 'hosts' => $this->hosts, 'corsMiddleware' => $this->corsMiddleware, - 'items' => $this->items, + 'routes' => $this->routes, 'hasCorsMiddleware' => $this->corsMiddleware !== null, - 'hasDispatcher' => $this->dispatcher !== null, - 'middlewareDefinitions' => $this->getMiddlewareDefinitions(), + 'middlewares' => $this->getBuiltMiddlewares(), default => throw new InvalidArgumentException('Unknown data key: ' . $key), }; } - private function getMiddlewareDefinitions(): array + private function getBuiltMiddlewares(): array { + if ($this->builtMiddlewares !== []) { + return $this->builtMiddlewares; + } + + $builtMiddlewares = $this->middlewares; + /** @var mixed $definition */ - foreach ($this->middlewareDefinitions as $index => $definition) { - if (in_array($definition, $this->disabledMiddlewareDefinitions, true)) { - unset($this->middlewareDefinitions[$index]); + foreach ($builtMiddlewares as $index => $definition) { + if (in_array($definition, $this->disabledMiddlewares, true)) { + unset($builtMiddlewares[$index]); } } - return array_values($this->middlewareDefinitions); + return $this->builtMiddlewares = array_values($builtMiddlewares); } } diff --git a/src/MatchingResult.php b/src/MatchingResult.php index 30c59182..bf8d7b35 100644 --- a/src/MatchingResult.php +++ b/src/MatchingResult.php @@ -12,7 +12,7 @@ use Yiisoft\Http\Method; use Yiisoft\Middleware\Dispatcher\MiddlewareDispatcher; -final class MatchingResult implements MiddlewareInterface +final class MatchingResult { /** * @var array @@ -24,19 +24,10 @@ final class MatchingResult implements MiddlewareInterface */ private array $methods = []; - private ?MiddlewareDispatcher $dispatcher = null; - private function __construct(private ?Route $route) { } - public function withDispatcher(MiddlewareDispatcher $dispatcher): self - { - $new = clone $this; - $new->dispatcher = $dispatcher; - return $new; - } - /** * @param array $arguments */ @@ -86,6 +77,9 @@ public function methods(): array return $this->methods; } + /** + * @psalm-assert-if-true true $this->isSuccess() + */ public function route(): Route { if ($this->route === null) { @@ -94,21 +88,4 @@ public function route(): Route return $this->route; } - - public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface - { - if (!$this->isSuccess()) { - return $handler->handle($request); - } - - // Inject dispatcher only if we have not previously injected. - // This improves performance in event-loop applications. - if ($this->dispatcher !== null && !$this->route->getData('hasDispatcher')) { - $this->route->injectDispatcher($this->dispatcher); - } - - return $this->route - ->getData('dispatcherWithMiddlewares') - ->dispatch($request, $handler); - } } diff --git a/src/Middleware/Router.php b/src/Middleware/Router.php index 631823ff..258ff368 100644 --- a/src/Middleware/Router.php +++ b/src/Middleware/Router.php @@ -54,8 +54,8 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface $this->currentRoute->setRouteWithArguments($result->route(), $result->arguments()); - return $result - ->withDispatcher($this->dispatcher) - ->process($request, $handler); + return $this->dispatcher + ->withMiddlewares($result->route()->getBuiltMiddlewares()) + ->dispatch($request, $handler); } } diff --git a/src/Route.php b/src/Route.php index 27127695..8a2af6f9 100644 --- a/src/Route.php +++ b/src/Route.php @@ -4,98 +4,190 @@ namespace Yiisoft\Router; +use Attribute; use InvalidArgumentException; use RuntimeException; use Stringable; use Yiisoft\Http\Method; -use Yiisoft\Middleware\Dispatcher\MiddlewareDispatcher; use function in_array; /** * Route defines a mapping from URL to callback / name and vice versa. */ -final class Route implements Stringable +#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] +class Route implements Stringable { - private ?string $name = null; - - /** - * @var string[] - */ - private array $hosts = []; - private bool $override = false; private bool $actionAdded = false; - - /** - * @var array[]|callable[]|string[] - */ - private array $middlewareDefinitions = []; - - private array $disabledMiddlewareDefinitions = []; - /** - * @var array + * @var callable[]|array[]|string[] */ - private array $defaults = []; + private array $builtMiddlewares = []; /** - * @param string[] $methods + * @param array $defaults Parameter default values indexed by parameter names. + * @param bool $override Marks route as override. When added it will replace existing route with the same name. + * @param array $disabledMiddlewares Excludes middleware from being invoked when action is handled. + * It is useful to avoid invoking one of the parent group middleware for + * a certain route. */ - private function __construct( + public function __construct( private array $methods, private string $pattern, - private ?MiddlewareDispatcher $dispatcher = null + private ?string $name = null, + private array $middlewares = [], + private array $defaults = [], + private array $hosts = [], + private bool $override = false, + private array $disabledMiddlewares = [], ) { } - /** - * @psalm-assert MiddlewareDispatcher $this->dispatcher - */ - public function injectDispatcher(MiddlewareDispatcher $dispatcher): void - { - $this->dispatcher = $dispatcher; - } - - public function withDispatcher(MiddlewareDispatcher $dispatcher): self - { - $route = clone $this; - $route->dispatcher = $dispatcher; - return $route; - } - - public static function get(string $pattern, ?MiddlewareDispatcher $dispatcher = null): self - { - return self::methods([Method::GET], $pattern, $dispatcher); + public static function get( + string $pattern, + ?string $name = null, + array $middlewares = [], + array $defaults = [], + array $hosts = [], + bool $override = false, + array $disabledMiddlewares = [] + ): self { + return self::methods( + [Method::GET], + $pattern, + $name, + $middlewares, + $defaults, + $hosts, + $override, + $disabledMiddlewares + ); } - public static function post(string $pattern, ?MiddlewareDispatcher $dispatcher = null): self - { - return self::methods([Method::POST], $pattern, $dispatcher); + public static function post( + string $pattern, + ?string $name = null, + array $middlewares = [], + array $defaults = [], + array $hosts = [], + bool $override = false, + array $disabledMiddlewares = [] + ): self { + return self::methods( + [Method::POST], + $pattern, + $name, + $middlewares, + $defaults, + $hosts, + $override, + $disabledMiddlewares + ); } - public static function put(string $pattern, ?MiddlewareDispatcher $dispatcher = null): self - { - return self::methods([Method::PUT], $pattern, $dispatcher); + public static function put( + string $pattern, + ?string $name = null, + array $middlewares = [], + array $defaults = [], + array $hosts = [], + bool $override = false, + array $disabledMiddlewares = [] + ): self { + return self::methods( + [Method::PUT], + $pattern, + $name, + $middlewares, + $defaults, + $hosts, + $override, + $disabledMiddlewares + ); } - public static function delete(string $pattern, ?MiddlewareDispatcher $dispatcher = null): self - { - return self::methods([Method::DELETE], $pattern, $dispatcher); + public static function delete( + string $pattern, + ?string $name = null, + array $middlewares = [], + array $defaults = [], + array $hosts = [], + bool $override = false, + array $disabledMiddlewares = [] + ): self { + return self::methods( + [Method::DELETE], + $pattern, + $name, + $middlewares, + $defaults, + $hosts, + $override, + $disabledMiddlewares + ); } - public static function patch(string $pattern, ?MiddlewareDispatcher $dispatcher = null): self - { - return self::methods([Method::PATCH], $pattern, $dispatcher); + public static function patch( + string $pattern, + ?string $name = null, + array $middlewares = [], + array $defaults = [], + array $hosts = [], + bool $override = false, + array $disabledMiddlewares = [] + ): self { + return self::methods( + [Method::PATCH], + $pattern, + $name, + $middlewares, + $defaults, + $hosts, + $override, + $disabledMiddlewares + ); } - public static function head(string $pattern, ?MiddlewareDispatcher $dispatcher = null): self - { - return self::methods([Method::HEAD], $pattern, $dispatcher); + public static function head( + string $pattern, + ?string $name = null, + array $middlewares = [], + array $defaults = [], + array $hosts = [], + bool $override = false, + array $disabledMiddlewares = [] + ): self { + return self::methods( + [Method::HEAD], + $pattern, + $name, + $middlewares, + $defaults, + $hosts, + $override, + $disabledMiddlewares + ); } - public static function options(string $pattern, ?MiddlewareDispatcher $dispatcher = null): self - { - return self::methods([Method::OPTIONS], $pattern, $dispatcher); + public static function options( + string $pattern, + ?string $name = null, + array $middlewares = [], + array $defaults = [], + array $hosts = [], + bool $override = false, + array $disabledMiddlewares = [] + ): self { + return self::methods( + [Method::OPTIONS], + $pattern, + $name, + $middlewares, + $defaults, + $hosts, + $override, + $disabledMiddlewares + ); } /** @@ -104,9 +196,23 @@ public static function options(string $pattern, ?MiddlewareDispatcher $dispatche public static function methods( array $methods, string $pattern, - ?MiddlewareDispatcher $dispatcher = null + ?string $name = null, + array $middlewares = [], + array $defaults = [], + array $hosts = [], + bool $override = false, + array $disabledMiddlewares = [] ): self { - return new self($methods, $pattern, $dispatcher); + return new self( + methods: $methods, + pattern: $pattern, + name: $name, + middlewares: $middlewares, + defaults: $defaults, + hosts: $hosts, + override: $override, + disabledMiddlewares: $disabledMiddlewares + ); } public function name(string $name): self @@ -177,9 +283,10 @@ public function middleware(array|callable|string ...$middlewareDefinition): self } $route = clone $this; array_push( - $route->middlewareDefinitions, + $route->middlewares, ...array_values($middlewareDefinition) ); + $route->builtMiddlewares = []; return $route; } @@ -194,9 +301,10 @@ public function prependMiddleware(array|callable|string ...$middlewareDefinition } $route = clone $this; array_unshift( - $route->middlewareDefinitions, + $route->middlewares, ...array_values($middlewareDefinition) ); + $route->builtMiddlewares = []; return $route; } @@ -206,8 +314,9 @@ public function prependMiddleware(array|callable|string ...$middlewareDefinition public function action(array|callable|string $middlewareDefinition): self { $route = clone $this; - $route->middlewareDefinitions[] = $middlewareDefinition; + $route->middlewares[] = $middlewareDefinition; $route->actionAdded = true; + $route->builtMiddlewares = []; return $route; } @@ -220,9 +329,10 @@ public function disableMiddleware(mixed ...$middlewareDefinition): self { $route = clone $this; array_push( - $route->disabledMiddlewareDefinitions, + $route->disabledMiddlewares, ...array_values($middlewareDefinition) ); + $route->builtMiddlewares = []; return $route; } @@ -235,9 +345,7 @@ public function disableMiddleware(mixed ...$middlewareDefinition): self * (T is 'hosts' ? array : * (T is 'methods' ? array : * (T is 'defaults' ? array : - * (T is ('override'|'hasMiddlewares'|'hasDispatcher') ? bool : - * (T is 'dispatcherWithMiddlewares' ? MiddlewareDispatcher : mixed) - * ) + * (T is ('override'|'hasMiddlewares') ? bool : mixed) * ) * ) * ) @@ -255,9 +363,7 @@ public function getData(string $key): mixed 'methods' => $this->methods, 'defaults' => $this->defaults, 'override' => $this->override, - 'dispatcherWithMiddlewares' => $this->getDispatcherWithMiddlewares(), - 'hasMiddlewares' => $this->middlewareDefinitions !== [], - 'hasDispatcher' => $this->dispatcher !== null, + 'hasMiddlewares' => $this->middlewares !== [], default => throw new InvalidArgumentException('Unknown data key: ' . $key), }; } @@ -295,31 +401,31 @@ public function __debugInfo() 'defaults' => $this->defaults, 'override' => $this->override, 'actionAdded' => $this->actionAdded, - 'middlewareDefinitions' => $this->middlewareDefinitions, - 'disabledMiddlewareDefinitions' => $this->disabledMiddlewareDefinitions, - 'middlewareDispatcher' => $this->dispatcher, + 'middlewares' => $this->middlewares, + 'builtMiddlewares' => $this->builtMiddlewares, + 'disabledMiddlewares' => $this->disabledMiddlewares, ]; } - private function getDispatcherWithMiddlewares(): MiddlewareDispatcher + /** + * @return callable[]|array[]|string[] + */ + public function getBuiltMiddlewares(): array { - if ($this->dispatcher === null) { - throw new RuntimeException(sprintf('There is no dispatcher in the route %s.', $this->getData('name'))); - } - - // Don't add middlewares to dispatcher if we did it earlier. + // Don't build middlewares if we did it earlier. // This improves performance in event-loop applications. - if ($this->dispatcher->hasMiddlewares()) { - return $this->dispatcher; + if ($this->builtMiddlewares !== []) { + return $this->builtMiddlewares; } - /** @var mixed $definition */ - foreach ($this->middlewareDefinitions as $index => $definition) { - if (in_array($definition, $this->disabledMiddlewareDefinitions, true)) { - unset($this->middlewareDefinitions[$index]); + $builtMiddlewares = $this->middlewares; + + foreach ($builtMiddlewares as $index => $definition) { + if (in_array($definition, $this->disabledMiddlewares, true)) { + unset($builtMiddlewares[$index]); } } - return $this->dispatcher = $this->dispatcher->withMiddlewares($this->middlewareDefinitions); + return $this->builtMiddlewares = $builtMiddlewares; } } diff --git a/src/RouteCollection.php b/src/RouteCollection.php index 629ed549..a753547f 100644 --- a/src/RouteCollection.php +++ b/src/RouteCollection.php @@ -104,12 +104,12 @@ private function injectGroup(Group $group, array &$tree, string $prefix = '', st { $prefix .= (string) $group->getData('prefix'); $namePrefix .= (string) $group->getData('namePrefix'); - $items = $group->getData('items'); + $items = $group->getData('routes'); $pattern = null; $hosts = []; foreach ($items as $item) { if (!$this->isStaticRoute($item)) { - $item = $item->prependMiddleware(...$group->getData('middlewareDefinitions')); + $item = $item->prependMiddleware(...$group->getData('middlewares')); } if (!empty($group->getData('hosts')) && empty($item->getData('hosts'))) { diff --git a/tests/GroupTest.php b/tests/GroupTest.php index a091bc96..1453e4cd 100644 --- a/tests/GroupTest.php +++ b/tests/GroupTest.php @@ -37,9 +37,9 @@ public function testAddMiddleware(): void $group = $group ->middleware($middleware1) ->middleware($middleware2); - $this->assertCount(2, $group->getData('middlewareDefinitions')); - $this->assertSame($middleware1, $group->getData('middlewareDefinitions')[0]); - $this->assertSame($middleware2, $group->getData('middlewareDefinitions')[1]); + $this->assertCount(2, $group->getData('middlewares')); + $this->assertSame($middleware1, $group->getData('middlewares')[0]); + $this->assertSame($middleware2, $group->getData('middlewares')[1]); } public function testDisabledMiddlewareDefinitions(): void @@ -49,8 +49,8 @@ public function testDisabledMiddlewareDefinitions(): void ->prependMiddleware(TestMiddleware1::class, TestMiddleware2::class) ->disableMiddleware(TestMiddleware1::class, TestMiddleware3::class); - $this->assertCount(1, $group->getData('middlewareDefinitions')); - $this->assertSame(TestMiddleware2::class, $group->getData('middlewareDefinitions')[0]); + $this->assertCount(1, $group->getData('middlewares')); + $this->assertSame(TestMiddleware2::class, $group->getData('middlewares')[0]); } public function testNamedArgumentsInMiddlewareMethods(): void @@ -60,8 +60,8 @@ public function testNamedArgumentsInMiddlewareMethods(): void ->prependMiddleware(middleware1: TestMiddleware1::class, middleware2: TestMiddleware2::class) ->disableMiddleware(middleware1: TestMiddleware1::class, middleware2: TestMiddleware3::class); - $this->assertCount(1, $group->getData('middlewareDefinitions')); - $this->assertSame(TestMiddleware2::class, $group->getData('middlewareDefinitions')[0]); + $this->assertCount(1, $group->getData('middlewares')); + $this->assertSame(TestMiddleware2::class, $group->getData('middlewares')[0]); } public function testRoutesAfterMiddleware(): void @@ -94,7 +94,7 @@ public function testAddNestedMiddleware(): void return $handler->handle($request); }; - $group = Group::create('/outergroup', $this->getDispatcher()) + $group = Group::create('/outergroup') ->middleware($middleware1) ->routes( Group::create('/innergroup') @@ -111,8 +111,8 @@ public function testAddNestedMiddleware(): void $routeCollection = new RouteCollection($collector); $route = $routeCollection->getRoute('request1'); - $response = $route - ->getData('dispatcherWithMiddlewares') + $response = $this->getDispatcher() + ->withMiddlewares($route->getBuiltMiddlewares()) ->dispatch($request, $this->getRequestHandler()); $this->assertSame(200, $response->getStatusCode()); $this->assertSame('middleware2', $response->getReasonPhrase()); @@ -132,7 +132,7 @@ public function testGroupMiddlewareFullStackCalled(): void return $handler->handle($request); }; - $group = Group::create('/group', $this->getDispatcher()) + $group = Group::create('/group') ->middleware($middleware1) ->middleware($middleware2) ->routes( @@ -146,8 +146,8 @@ public function testGroupMiddlewareFullStackCalled(): void $routeCollection = new RouteCollection($collector); $route = $routeCollection->getRoute('request1'); - $response = $route - ->getData('dispatcherWithMiddlewares') + $response = $this->getDispatcher() + ->withMiddlewares($route->getBuiltMiddlewares()) ->dispatch($request, $this->getRequestHandler()); $this->assertSame(200, $response->getStatusCode()); $this->assertSame('middleware2', $response->getReasonPhrase()); @@ -161,7 +161,7 @@ public function testGroupMiddlewareStackInterrupted(): void $middleware1 = fn () => new Response(403); $middleware2 = fn () => new Response(405); - $group = Group::create('/group', $this->getDispatcher()) + $group = Group::create('/group') ->middleware($middleware1) ->middleware($middleware2) ->routes( @@ -175,9 +175,9 @@ public function testGroupMiddlewareStackInterrupted(): void $routeCollection = new RouteCollection($collector); $route = $routeCollection->getRoute('request1'); - $response = $route - ->getData('dispatcherWithMiddlewares') - ->dispatch($request, $this->getRequestHandler()); + $response = $this->getDispatcher() + ->withMiddlewares($route->getBuiltMiddlewares()) + ->dispatch($request, $this->getRequestHandler()); $this->assertSame(403, $response->getStatusCode()); } @@ -205,27 +205,27 @@ public function testAddGroup(): void ), ); - $this->assertCount(1, $root->getData('items')); + $this->assertCount(1, $root->getData('routes')); /** @var Group $api */ - $api = $root->getData('items')[0]; + $api = $root->getData('routes')[0]; $this->assertSame('/api', $api->getData('prefix')); - $this->assertCount(2, $api->getData('items')); - $this->assertSame($logoutRoute, $api->getData('items')[0]); + $this->assertCount(2, $api->getData('routes')); + $this->assertSame($logoutRoute, $api->getData('routes')[0]); /** @var Group $postGroup */ - $postGroup = $api->getData('items')[1]; + $postGroup = $api->getData('routes')[1]; $this->assertInstanceOf(Group::class, $postGroup); - $this->assertCount(2, $api->getData('middlewareDefinitions')); - $this->assertSame($middleware1, $api->getData('middlewareDefinitions')[0]); - $this->assertSame($middleware2, $api->getData('middlewareDefinitions')[1]); + $this->assertCount(2, $api->getData('middlewares')); + $this->assertSame($middleware1, $api->getData('middlewares')[0]); + $this->assertSame($middleware2, $api->getData('middlewares')[1]); $this->assertSame('/post', $postGroup->getData('prefix')); - $this->assertCount(2, $postGroup->getData('items')); - $this->assertSame($listRoute, $postGroup->getData('items')[0]); - $this->assertSame($viewRoute, $postGroup->getData('items')[1]); - $this->assertEmpty($postGroup->getData('middlewareDefinitions')); + $this->assertCount(2, $postGroup->getData('routes')); + $this->assertSame($listRoute, $postGroup->getData('routes')[0]); + $this->assertSame($viewRoute, $postGroup->getData('routes')[1]); + $this->assertEmpty($postGroup->getData('middlewares')); } public function testHost(): void @@ -259,54 +259,6 @@ public function testGetDataWithWrongKey(): void $group->getData('wrong'); } - public function testDispatcherInjected(): void - { - $dispatcher = $this->getDispatcher(); - - $apiGroup = Group::create('/api', $dispatcher) - ->routes( - Route::get('/info')->name('api-info'), - Group::create('/v1') - ->routes( - Route::get('/user')->name('api-v1-user/index'), - Route::get('/user/{id}')->name('api-v1-user/view'), - Group::create('/news') - ->routes( - Route::get('/post')->name('api-v1-news-post/index'), - Route::get('/post/{id}')->name('api-v1-news-post/view'), - ), - Group::create('/blog') - ->routes( - Route::get('/post')->name('api-v1-blog-post/index'), - Route::get('/post/{id}')->name('api-v1-blog-post/view'), - ), - Route::get('/note')->name('api-v1-note/index'), - Route::get('/note/{id}')->name('api-v1-note/view'), - ), - Group::create('/v2') - ->routes( - Route::get('/user')->name('api-v2-user/index'), - Route::get('/user/{id}')->name('api-v2-user/view'), - Group::create('/news') - ->routes( - Route::get('/post')->name('api-v2-news-post/index'), - Route::get('/post/{id}')->name('api-v2-news-post/view'), - Group::create('/blog') - ->routes( - Route::get('/post')->name('api-v2-blog-post/index'), - Route::get('/post/{id}')->name('api-v2-blog-post/view'), - Route::get('/note')->name('api-v2-note/index'), - Route::get('/note/{id}')->name('api-v2-note/view') - ) - ) - ) - ); - - $items = $apiGroup->getData('items'); - - $this->assertAllRoutesAndGroupsHaveDispatcher($items); - } - public function testWithCors(): void { $group = Group::create() @@ -418,15 +370,9 @@ public function testDuplicateHosts(): void public function testImmutability(): void { - $container = new SimpleContainer(); - $middlewareDispatcher = new MiddlewareDispatcher( - new MiddlewareFactory($container), - ); - $group = Group::create(); $this->assertNotSame($group, $group->routes()); - $this->assertNotSame($group, $group->withDispatcher($middlewareDispatcher)); $this->assertNotSame($group, $group->withCors(null)); $this->assertNotSame($group, $group->middleware()); $this->assertNotSame($group, $group->prependMiddleware()); @@ -453,16 +399,4 @@ private function getDispatcher(): MiddlewareDispatcher $this->createMock(EventDispatcherInterface::class) ); } - - private function assertAllRoutesAndGroupsHaveDispatcher(array $items): void - { - $func = function ($item) use (&$func) { - $this->assertTrue($item->getData('hasDispatcher')); - if ($item instanceof Group) { - $items = $item->getData('items'); - array_walk($items, $func); - } - }; - array_walk($items, $func); - } } diff --git a/tests/MatchingResultTest.php b/tests/MatchingResultTest.php index b75ec6be..fc4b68e0 100644 --- a/tests/MatchingResultTest.php +++ b/tests/MatchingResultTest.php @@ -48,31 +48,6 @@ public function testFromFailureOnNotFoundFailure(): void $this->assertFalse($result->isMethodFailure()); } - public function testProcessSuccess(): void - { - $container = $this->createMock(ContainerInterface::class); - $dispatcher = new MiddlewareDispatcher( - new MiddlewareFactory($container), - $this->createMock(EventDispatcherInterface::class) - ); - $route = Route::post('/', $dispatcher)->middleware($this->getMiddleware()); - $result = MatchingResult::fromSuccess($route, []); - $request = new ServerRequest('POST', '/'); - - $response = $result->process($request, $this->getRequestHandler()); - $this->assertSame(201, $response->getStatusCode()); - } - - public function testProcessFailure(): void - { - $request = new ServerRequest('POST', '/'); - - $response = MatchingResult::fromFailure([Method::GET, Method::HEAD]) - ->process($request, $this->getRequestHandler()); - - $this->assertSame(404, $response->getStatusCode()); - } - public function testRouteOnFailure(): void { $result = MatchingResult::fromFailure([Method::GET, Method::HEAD]); @@ -81,31 +56,4 @@ public function testRouteOnFailure(): void $this->expectExceptionMessage('There is no route in the matching result.'); $result->route(); } - - public function testImmutability(): void - { - $container = new SimpleContainer(); - $middlewareDispatcher = new MiddlewareDispatcher( - new MiddlewareFactory($container), - ); - - $result = MatchingResult::fromFailure([Method::GET]); - - $this->assertNotSame($result, $result->withDispatcher($middlewareDispatcher)); - } - - private function getMiddleware(): callable - { - return static fn () => new Response(201); - } - - private function getRequestHandler(): RequestHandlerInterface - { - return new class () implements RequestHandlerInterface { - public function handle(ServerRequestInterface $request): ResponseInterface - { - return new Response(404); - } - }; - } } diff --git a/tests/RouteCollectionTest.php b/tests/RouteCollectionTest.php index 99b0ca1e..2e8edbe8 100644 --- a/tests/RouteCollectionTest.php +++ b/tests/RouteCollectionTest.php @@ -80,8 +80,8 @@ public function testRouteOverride(): void { $listRoute = Route::get('/')->name('my-route'); $viewRoute = Route::get('/{id}') - ->name('my-route') - ->override(); + ->name('my-route') + ->override(); $group = Group::create()->routes($listRoute, $viewRoute); @@ -96,13 +96,13 @@ public function testRouteOverride(): void public function testRouteWithoutAction(): void { $group = Group::create() - ->middleware(fn () => 1) - ->routes( - Route::get('/test', $this->getDispatcher()) - ->action(fn () => 2) - ->name('test'), - Route::get('/images/{sile}')->name('image') - ); + ->middleware(fn () => 1) + ->routes( + Route::get('/test') + ->action(fn () => 2) + ->name('test'), + Route::get('/images/{sile}')->name('image') + ); $collector = new RouteCollector(); $collector->addGroup($group); @@ -115,31 +115,31 @@ public function testRouteWithoutAction(): void public function testGetRouterTree(): void { $group1 = Group::create('/api') - ->routes( - Route::get('/test', $this->getDispatcher()) - ->action(fn () => 2) - ->name('/test'), - Route::get('/images/{sile}')->name('/image'), - Group::create('/v1') - ->routes( - Route::get('/posts', $this->getDispatcher())->name('/posts'), - Route::get('/post/{sile}')->name('/post/view') - ) - ->namePrefix('/v1'), - Group::create('/v1') - ->routes( - Route::get('/tags', $this->getDispatcher())->name('/tags'), - Route::get('/tag/{slug}')->name('/tag/view'), - ) - ->namePrefix('/v1'), - )->namePrefix('/api'); + ->routes( + Route::get('/test') + ->action(fn () => 2) + ->name('/test'), + Route::get('/images/{sile}')->name('/image'), + Group::create('/v1') + ->routes( + Route::get('/posts')->name('/posts'), + Route::get('/post/{sile}')->name('/post/view') + ) + ->namePrefix('/v1'), + Group::create('/v1') + ->routes( + Route::get('/tags')->name('/tags'), + Route::get('/tag/{slug}')->name('/tag/view'), + ) + ->namePrefix('/v1'), + )->namePrefix('/api'); $group2 = Group::create('/api') - ->routes( - Route::get('/posts', $this->getDispatcher())->name('/posts'), - Route::get('/post/{sile}')->name('/post/view'), - ) - ->namePrefix('/api'); + ->routes( + Route::get('/posts')->name('/posts'), + Route::get('/post/{sile}')->name('/post/view'), + ) + ->namePrefix('/api'); $collector = new RouteCollector(); $collector->addGroup($group1); @@ -168,13 +168,13 @@ public function testGetRouterTree(): void public function testGetRoutes(): void { $group = Group::create() - ->middleware(fn () => 1) - ->routes( - Route::get('/test', $this->getDispatcher()) - ->action(fn () => 2) - ->name('test'), - Route::get('/images/{sile}')->name('image') - ); + ->middleware(fn () => 1) + ->routes( + Route::get('/test') + ->action(fn () => 2) + ->name('test'), + Route::get('/images/{sile}')->name('image') + ); $collector = new RouteCollector(); $collector->addGroup($group); @@ -188,19 +188,19 @@ public function testGetRoutes(): void public function testGroupHost(): void { $group = Group::create() - ->routes( - Group::create() - ->routes( - Route::get('/project/{name}')->name('project') - ) - ->hosts('https://yiipowered.com/', 'https://yiiframework.ru/'), - Group::create() - ->routes( - Route::get('/user/{username}')->name('user') - ), - Route::get('/images/{name}')->name('image') - ) - ->host('https://yiiframework.com/'); + ->routes( + Group::create() + ->routes( + Route::get('/project/{name}')->name('project') + ) + ->hosts('https://yiipowered.com/', 'https://yiiframework.ru/'), + Group::create() + ->routes( + Route::get('/user/{username}')->name('user') + ), + Route::get('/images/{name}')->name('image') + ) + ->host('https://yiiframework.com/'); $collector = new RouteCollector(); $collector->addGroup($group); @@ -218,20 +218,20 @@ public function testGroupHost(): void public function testGroupName(): void { $group = Group::create('api') - ->routes( - Group::create()->routes( - Group::create('/v1') - ->routes( - Route::get('/package/downloads/{package}')->name('/package/downloads') - ) - ->namePrefix('/v1'), - Group::create()->routes( - Route::get('')->name('/index') - ), - Route::get('/post/{slug}')->name('/post/view'), - Route::get('/user/{username}'), - ) - )->namePrefix('api'); + ->routes( + Group::create()->routes( + Group::create('/v1') + ->routes( + Route::get('/package/downloads/{package}')->name('/package/downloads') + ) + ->namePrefix('/v1'), + Group::create()->routes( + Route::get('')->name('/index') + ), + Route::get('/post/{slug}')->name('/post/view'), + Route::get('/user/{username}'), + ) + )->namePrefix('api'); $collector = new RouteCollector(); $collector->addGroup($group); @@ -257,13 +257,13 @@ public function testCollectorMiddlewareFullstackCalled(): void implode($request->getAttributes()) ); $listRoute = Route::get('/') - ->action($action) - ->name('list'); - $viewRoute = Route::get('/{id}', $this->getDispatcher()) - ->action($action) - ->name('view'); + ->action($action) + ->name('list'); + $viewRoute = Route::get('/{id}') + ->action($action) + ->name('view'); - $group = Group::create(null, $this->getDispatcher())->routes($listRoute); + $group = Group::create(null)->routes($listRoute); $middleware = function (ServerRequestInterface $request, RequestHandlerInterface $handler) { $request = $request->withAttribute('middleware', 'middleware1'); @@ -279,12 +279,12 @@ public function testCollectorMiddlewareFullstackCalled(): void $route1 = $routeCollection->getRoute('list'); $route2 = $routeCollection->getRoute('view'); $request = new ServerRequest('GET', '/'); - $response1 = $route1 - ->getData('dispatcherWithMiddlewares') - ->dispatch($request, $this->getRequestHandler()); - $response2 = $route2 - ->getData('dispatcherWithMiddlewares') - ->dispatch($request, $this->getRequestHandler()); + $response1 = $this->getDispatcher() + ->withMiddlewares($route1->getBuiltMiddlewares()) + ->dispatch($request, $this->getRequestHandler()); + $response2 = $this->getDispatcher() + ->withMiddlewares($route2->getBuiltMiddlewares()) + ->dispatch($request, $this->getRequestHandler()); $this->assertEquals('middleware1', $response1->getReasonPhrase()); $this->assertEquals('middleware1', $response2->getReasonPhrase()); @@ -321,9 +321,9 @@ public function testMiddlewaresOrder(bool $groupWrapped): void ->prependMiddleware(TestMiddleware1::class); $rawRoute = Route::get('/') - ->middleware(TestMiddleware3::class) - ->action([TestController::class, 'index']) - ->name('main'); + ->middleware(TestMiddleware3::class) + ->action([TestController::class, 'index']) + ->name('main'); if ($groupWrapped) { $collector->addGroup( @@ -334,9 +334,8 @@ public function testMiddlewaresOrder(bool $groupWrapped): void } $route = (new RouteCollection($collector))->getRoute('main'); - $route->injectDispatcher($injectDispatcher); - $dispatcher = $route->getData('dispatcherWithMiddlewares'); + $dispatcher = $injectDispatcher->withMiddlewares($route->getBuiltMiddlewares()); $response = $dispatcher->dispatch($request, $this->getRequestHandler()); $this->assertSame(200, $response->getStatusCode()); @@ -361,9 +360,8 @@ public function testStaticRouteWithCollectorMiddlewares(): void ); $route = (new RouteCollection($collector))->getRoute('image'); - $route->injectDispatcher($injectDispatcher); - $dispatcher = $route->getData('dispatcherWithMiddlewares'); + $dispatcher = $injectDispatcher->withMiddlewares($route->getBuiltMiddlewares()); $this->expectException(RuntimeException::class); $this->expectExceptionMessage('Stack is empty.'); diff --git a/tests/RouteTest.php b/tests/RouteTest.php index d2cc255d..4beed803 100644 --- a/tests/RouteTest.php +++ b/tests/RouteTest.php @@ -212,23 +212,6 @@ public function testToStringSimple(): void $this->assertSame('GET /', (string)$route); } - public function testDispatcherInjecting(): void - { - $request = new ServerRequest('GET', '/'); - $container = $this->getContainer( - [ - TestController::class => new TestController(), - ] - ); - $dispatcher = $this->getDispatcher($container); - $route = Route::get('/')->action([TestController::class, 'index']); - $route->injectDispatcher($dispatcher); - $response = $route - ->getData('dispatcherWithMiddlewares') - ->dispatch($request, $this->getRequestHandler()); - $this->assertSame(200, $response->getStatusCode()); - } - public function testMiddlewareAfterAction(): void { $route = Route::get('/')->action([TestController::class, 'index']); @@ -264,9 +247,8 @@ public function testDisabledMiddlewareDefinitions(): void ->middleware(TestMiddleware1::class, TestMiddleware2::class, TestMiddleware3::class) ->action([TestController::class, 'index']) ->disableMiddleware(TestMiddleware1::class, TestMiddleware3::class); - $route->injectDispatcher($injectDispatcher); - $dispatcher = $route->getData('dispatcherWithMiddlewares'); + $dispatcher = $injectDispatcher->withMiddlewares($route->getBuiltMiddlewares()); $response = $dispatcher->dispatch($request, $this->getRequestHandler()); $this->assertSame(200, $response->getStatusCode()); @@ -290,52 +272,14 @@ public function testPrependMiddlewareDefinitions(): void ->middleware(TestMiddleware3::class) ->action([TestController::class, 'index']) ->prependMiddleware(TestMiddleware1::class, TestMiddleware2::class); - $route->injectDispatcher($injectDispatcher); - $dispatcher = $route->getData('dispatcherWithMiddlewares'); + $dispatcher = $injectDispatcher->withMiddlewares($route->getBuiltMiddlewares()); $response = $dispatcher->dispatch($request, $this->getRequestHandler()); $this->assertSame(200, $response->getStatusCode()); $this->assertSame('123', (string) $response->getBody()); } - public function testGetDispatcherWithoutDispatcher(): void - { - $route = Route::get('/')->name('test'); - - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('There is no dispatcher in the route test.'); - $route->getData('dispatcherWithMiddlewares'); - } - - public function testGetDispatcherWithMiddlewares(): void - { - $request = new ServerRequest('GET', '/'); - - $injectDispatcher = $this - ->getDispatcher( - $this->getContainer([ - TestMiddleware1::class => new TestMiddleware1(), - TestMiddleware2::class => new TestMiddleware2(), - TestController::class => new TestController(), - ]) - ) - ->withMiddlewares([ - TestMiddleware1::class, - TestMiddleware2::class, - [TestController::class, 'index'], - ]); - - $route = Route::get('/'); - $route->injectDispatcher($injectDispatcher); - - $dispatcher = $route->getData('dispatcherWithMiddlewares'); - - $response = $dispatcher->dispatch($request, $this->getRequestHandler()); - $this->assertSame(200, $response->getStatusCode()); - $this->assertSame('12', (string) $response->getBody()); - } - public function testDebugInfo(): void { $route = Route::get('/') @@ -370,19 +314,21 @@ public function testDebugInfo(): void [override] => 1 [actionAdded] => 1 - [middlewareDefinitions] => Array + [middlewares] => Array ( [0] => Yiisoft\Router\Tests\Support\TestMiddleware3 [1] => Yiisoft\Router\Tests\Support\TestMiddleware1 [2] => go ) - [disabledMiddlewareDefinitions] => Array + [builtMiddlewares] => Array ( - [0] => Yiisoft\Router\Tests\Support\TestMiddleware2 ) - [middlewareDispatcher] => + [disabledMiddlewares] => Array + ( + [0] => Yiisoft\Router\Tests\Support\TestMiddleware2 + ) ) EOL; @@ -399,15 +345,9 @@ public function testDuplicateHosts(): void public function testImmutability(): void { - $container = new SimpleContainer(); - $middlewareDispatcher = new MiddlewareDispatcher( - new MiddlewareFactory($container), - ); - $route = Route::get('/'); $routeWithAction = $route->action(''); - $this->assertNotSame($route, $route->withDispatcher($middlewareDispatcher)); $this->assertNotSame($route, $route->name('')); $this->assertNotSame($route, $route->pattern('')); $this->assertNotSame($route, $route->host('')); From b328c82d0b0f06eaf3b0e6c8b3b6936512872d73 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Tue, 28 Mar 2023 10:21:02 +0000 Subject: [PATCH 02/63] Apply fixes from StyleCI --- src/Attribute/Delete.php | 2 ++ src/Attribute/Get.php | 2 ++ src/Attribute/Head.php | 2 ++ src/Attribute/Options.php | 2 ++ src/Attribute/Patch.php | 2 ++ src/Attribute/Post.php | 2 ++ src/Attribute/Put.php | 2 ++ src/Group.php | 2 ++ src/MatchingResult.php | 5 ----- src/Route.php | 6 ++++-- tests/GroupTest.php | 1 - tests/MatchingResultTest.php | 10 ---------- tests/RouteTest.php | 1 - 13 files changed, 20 insertions(+), 19 deletions(-) diff --git a/src/Attribute/Delete.php b/src/Attribute/Delete.php index acab5a4e..a9b4c557 100644 --- a/src/Attribute/Delete.php +++ b/src/Attribute/Delete.php @@ -1,5 +1,7 @@ Date: Tue, 28 Mar 2023 16:54:58 +0500 Subject: [PATCH 03/63] Fix psalm annotation --- src/MatchingResult.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MatchingResult.php b/src/MatchingResult.php index 16efb625..74541b65 100644 --- a/src/MatchingResult.php +++ b/src/MatchingResult.php @@ -73,7 +73,7 @@ public function methods(): array } /** - * @psalm-assert-if-true true $this->isSuccess() + * @psalm-assert-if-true !null $this->route */ public function route(): Route { From 69d983215ba97ab05f4e11ed0bd217d23fc10d3f Mon Sep 17 00:00:00 2001 From: Rustam Date: Tue, 28 Mar 2023 17:03:33 +0500 Subject: [PATCH 04/63] Add tests --- tests/GroupTest.php | 11 +++++++++++ tests/RouteTest.php | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/tests/GroupTest.php b/tests/GroupTest.php index cf67dd81..244a29fa 100644 --- a/tests/GroupTest.php +++ b/tests/GroupTest.php @@ -380,6 +380,17 @@ public function testImmutability(): void $this->assertNotSame($group, $group->disableMiddleware()); } + public function testBuiltMiddlewares(): void + { + $group = Group::create() + ->middleware(static fn () => new Response(200)) + ->prependMiddleware(TestMiddleware1::class); + + $builtMiddlewares = $group->getData('middlewares'); + + $this->assertSame($builtMiddlewares, $group->getData('middlewares')); + } + private function getRequestHandler(): RequestHandlerInterface { return new class () implements RequestHandlerInterface { diff --git a/tests/RouteTest.php b/tests/RouteTest.php index 34e9737e..6f5d03fe 100644 --- a/tests/RouteTest.php +++ b/tests/RouteTest.php @@ -359,6 +359,17 @@ public function testImmutability(): void $this->assertNotSame($route, $route->disableMiddleware('')); } + public function testBuiltMiddlewares(): void + { + $route = Route::get('') + ->middleware(TestMiddleware1::class) + ->action(static fn () => new Response(200)); + + $builtMiddlewares = $route->getBuiltMiddlewares(); + + $this->assertSame($builtMiddlewares, $route->getBuiltMiddlewares()); + } + private function getRequestHandler(): RequestHandlerInterface { return new class () implements RequestHandlerInterface { From 777ff546ea331c44d9162666f5bc23fbdf26f726 Mon Sep 17 00:00:00 2001 From: Rustam Date: Mon, 12 Jun 2023 19:28:31 +0500 Subject: [PATCH 05/63] Add attributes registrar --- src/RouteAttributesRegistrar.php | 57 +++++++++++++++++++++++ src/RouteAttributesRegistrarInterface.php | 18 +++++++ 2 files changed, 75 insertions(+) create mode 100644 src/RouteAttributesRegistrar.php create mode 100644 src/RouteAttributesRegistrarInterface.php diff --git a/src/RouteAttributesRegistrar.php b/src/RouteAttributesRegistrar.php new file mode 100644 index 00000000..b94d045e --- /dev/null +++ b/src/RouteAttributesRegistrar.php @@ -0,0 +1,57 @@ +isUserDefined()) { + continue; + } + $routes = $this->getRoutes($reflectionClass); + $groupAttributes = $reflectionClass->getAttributes(Group::class, \ReflectionAttribute::IS_INSTANCEOF); + + if (!empty($groupAttributes)) { + [$groupAttribute] = $groupAttributes; + /** @var Group $group */ + $group = $groupAttribute->newInstance(); + $this->collector->addRoute($group->routes(...$routes)); + } else { + $this->collector->addRoute(...$routes); + } + } + } + + private function getRoutes(\ReflectionClass $reflectionClass): iterable + { + foreach ($reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $reflectionMethod) { + foreach ( + $reflectionMethod->getAttributes( + Route::class, + \ReflectionAttribute::IS_INSTANCEOF + ) as $reflectionAttribute + ) { + /** @var Route $route */ + $route = $reflectionAttribute->newInstance(); + + yield $route->action([$reflectionClass->getName(), $reflectionMethod->getName()]); + } + } + } +} diff --git a/src/RouteAttributesRegistrarInterface.php b/src/RouteAttributesRegistrarInterface.php new file mode 100644 index 00000000..6188756c --- /dev/null +++ b/src/RouteAttributesRegistrarInterface.php @@ -0,0 +1,18 @@ + Date: Mon, 12 Jun 2023 14:28:54 +0000 Subject: [PATCH 06/63] Apply fixes from StyleCI --- src/RouteAttributesRegistrar.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/RouteAttributesRegistrar.php b/src/RouteAttributesRegistrar.php index b94d045e..868c10bb 100644 --- a/src/RouteAttributesRegistrar.php +++ b/src/RouteAttributesRegistrar.php @@ -9,7 +9,8 @@ */ final class RouteAttributesRegistrar implements RouteAttributesRegistrarInterface { - public function __construct(private RouteCollectorInterface $collector) { + public function __construct(private RouteCollectorInterface $collector) + { } /** From 1651071750a67ebbb24b41564485621cf592fbae Mon Sep 17 00:00:00 2001 From: Rustam Date: Mon, 12 Jun 2023 23:54:33 +0500 Subject: [PATCH 07/63] Add tests --- src/RouteAttributesRegistrar.php | 10 +++++----- tests/Attribute/DeleteTest.php | 20 ++++++++++++++++++++ tests/Attribute/GetTest.php | 20 ++++++++++++++++++++ tests/Attribute/HeadTest.php | 20 ++++++++++++++++++++ tests/Attribute/OptionsTest.php | 20 ++++++++++++++++++++ tests/Attribute/PatchTest.php | 20 ++++++++++++++++++++ tests/Attribute/PostTest.php | 20 ++++++++++++++++++++ tests/Attribute/PutTest.php | 20 ++++++++++++++++++++ tests/RouteAttributesRegistrarTest.php | 25 +++++++++++++++++++++++++ tests/Support/TestController.php | 7 +++++++ 10 files changed, 177 insertions(+), 5 deletions(-) create mode 100644 tests/Attribute/DeleteTest.php create mode 100644 tests/Attribute/GetTest.php create mode 100644 tests/Attribute/HeadTest.php create mode 100644 tests/Attribute/OptionsTest.php create mode 100644 tests/Attribute/PatchTest.php create mode 100644 tests/Attribute/PostTest.php create mode 100644 tests/Attribute/PutTest.php create mode 100644 tests/RouteAttributesRegistrarTest.php diff --git a/src/RouteAttributesRegistrar.php b/src/RouteAttributesRegistrar.php index 868c10bb..c6c02f9a 100644 --- a/src/RouteAttributesRegistrar.php +++ b/src/RouteAttributesRegistrar.php @@ -9,7 +9,7 @@ */ final class RouteAttributesRegistrar implements RouteAttributesRegistrarInterface { - public function __construct(private RouteCollectorInterface $collector) + public function __construct(private RouteCollectorInterface $routeCollector) { } @@ -25,21 +25,21 @@ public function register(): void if (!$reflectionClass->isUserDefined()) { continue; } - $routes = $this->getRoutes($reflectionClass); + $routes = $this->lookupRoutes($reflectionClass); $groupAttributes = $reflectionClass->getAttributes(Group::class, \ReflectionAttribute::IS_INSTANCEOF); if (!empty($groupAttributes)) { [$groupAttribute] = $groupAttributes; /** @var Group $group */ $group = $groupAttribute->newInstance(); - $this->collector->addRoute($group->routes(...$routes)); + $this->routeCollector->addRoute($group->routes(...$routes)); } else { - $this->collector->addRoute(...$routes); + $this->routeCollector->addRoute(...$routes); } } } - private function getRoutes(\ReflectionClass $reflectionClass): iterable + private function lookupRoutes(\ReflectionClass $reflectionClass): iterable { foreach ($reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $reflectionMethod) { foreach ( diff --git a/tests/Attribute/DeleteTest.php b/tests/Attribute/DeleteTest.php new file mode 100644 index 00000000..567fce29 --- /dev/null +++ b/tests/Attribute/DeleteTest.php @@ -0,0 +1,20 @@ +assertSame('/post', $route->getData('pattern')); + $this->assertEquals([Method::DELETE], $route->getData('methods')); + } +} diff --git a/tests/Attribute/GetTest.php b/tests/Attribute/GetTest.php new file mode 100644 index 00000000..aad76124 --- /dev/null +++ b/tests/Attribute/GetTest.php @@ -0,0 +1,20 @@ +assertSame('/post', $route->getData('pattern')); + $this->assertEquals([Method::GET], $route->getData('methods')); + } +} diff --git a/tests/Attribute/HeadTest.php b/tests/Attribute/HeadTest.php new file mode 100644 index 00000000..ab3925b6 --- /dev/null +++ b/tests/Attribute/HeadTest.php @@ -0,0 +1,20 @@ +assertSame('/post', $route->getData('pattern')); + $this->assertEquals([Method::HEAD], $route->getData('methods')); + } +} diff --git a/tests/Attribute/OptionsTest.php b/tests/Attribute/OptionsTest.php new file mode 100644 index 00000000..35f6fe83 --- /dev/null +++ b/tests/Attribute/OptionsTest.php @@ -0,0 +1,20 @@ +assertSame('/post', $route->getData('pattern')); + $this->assertEquals([Method::OPTIONS], $route->getData('methods')); + } +} diff --git a/tests/Attribute/PatchTest.php b/tests/Attribute/PatchTest.php new file mode 100644 index 00000000..21b902d3 --- /dev/null +++ b/tests/Attribute/PatchTest.php @@ -0,0 +1,20 @@ +assertSame('/post', $route->getData('pattern')); + $this->assertEquals([Method::PATCH], $route->getData('methods')); + } +} diff --git a/tests/Attribute/PostTest.php b/tests/Attribute/PostTest.php new file mode 100644 index 00000000..80c3fc0b --- /dev/null +++ b/tests/Attribute/PostTest.php @@ -0,0 +1,20 @@ +assertSame('/post', $route->getData('pattern')); + $this->assertEquals([Method::POST], $route->getData('methods')); + } +} diff --git a/tests/Attribute/PutTest.php b/tests/Attribute/PutTest.php new file mode 100644 index 00000000..af5f5766 --- /dev/null +++ b/tests/Attribute/PutTest.php @@ -0,0 +1,20 @@ +assertSame('/post', $route->getData('pattern')); + $this->assertEquals([Method::PUT], $route->getData('methods')); + } +} diff --git a/tests/RouteAttributesRegistrarTest.php b/tests/RouteAttributesRegistrarTest.php new file mode 100644 index 00000000..0cd47643 --- /dev/null +++ b/tests/RouteAttributesRegistrarTest.php @@ -0,0 +1,25 @@ +register(); + + $this->assertCount(1, $items = $routeCollector->getItems()); + $this->assertCount(1, $items[0]->getBuiltMiddlewares()); + $this->assertSame([TestController::class, 'attributeAction'], $items[0]->getBuiltMiddlewares()[0]); + } +} diff --git a/tests/Support/TestController.php b/tests/Support/TestController.php index 44c17899..6d3c4faf 100644 --- a/tests/Support/TestController.php +++ b/tests/Support/TestController.php @@ -7,6 +7,7 @@ use Nyholm\Psr7\Response; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; +use Yiisoft\Router\Attribute\Get; final class TestController { @@ -14,4 +15,10 @@ public function index(ServerRequestInterface $request): ResponseInterface { return new Response(200, [], $request->getAttribute('content', '')); } + + #[Get('/')] + public function attributeAction(): Response + { + return new Response(200, [], 'test'); + } } From 838bbc4bee375c16991ae5c579f2f32191da337e Mon Sep 17 00:00:00 2001 From: Rustam Date: Thu, 15 Jun 2023 19:38:19 +0500 Subject: [PATCH 08/63] Fix tests & psalm issues --- src/Attribute/Delete.php | 7 + src/Attribute/Get.php | 7 + src/Attribute/Head.php | 7 + src/Attribute/Options.php | 7 + src/Attribute/Patch.php | 7 + src/Attribute/Post.php | 7 + src/Attribute/Put.php | 7 + src/Group.php | 51 ++++- src/Route.php | 246 +++++++++---------------- src/RouteAttributesRegistrar.php | 9 +- tests/RouteAttributesRegistrarTest.php | 10 +- tests/Support/TestController.php | 2 + 12 files changed, 201 insertions(+), 166 deletions(-) diff --git a/src/Attribute/Delete.php b/src/Attribute/Delete.php index a9b4c557..6a3f752c 100644 --- a/src/Attribute/Delete.php +++ b/src/Attribute/Delete.php @@ -11,6 +11,13 @@ #[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] final class Delete extends Route { + /** + * @param array $defaults Parameter default values indexed by parameter names. + * @param bool $override Marks route as override. When added it will replace existing route with the same name. + * @param array $disabledMiddlewares Excludes middleware from being invoked when action is handled. + * It is useful to avoid invoking one of the parent group middleware for + * a certain route. + */ public function __construct( string $pattern, ?string $name = null, diff --git a/src/Attribute/Get.php b/src/Attribute/Get.php index f8029574..2d5a5415 100644 --- a/src/Attribute/Get.php +++ b/src/Attribute/Get.php @@ -11,6 +11,13 @@ #[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] final class Get extends Route { + /** + * @param array $defaults Parameter default values indexed by parameter names. + * @param bool $override Marks route as override. When added it will replace existing route with the same name. + * @param array $disabledMiddlewares Excludes middleware from being invoked when action is handled. + * It is useful to avoid invoking one of the parent group middleware for + * a certain route. + */ public function __construct( string $pattern, ?string $name = null, diff --git a/src/Attribute/Head.php b/src/Attribute/Head.php index 797537c2..1c4e7a70 100644 --- a/src/Attribute/Head.php +++ b/src/Attribute/Head.php @@ -11,6 +11,13 @@ #[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] final class Head extends Route { + /** + * @param array $defaults Parameter default values indexed by parameter names. + * @param bool $override Marks route as override. When added it will replace existing route with the same name. + * @param array $disabledMiddlewares Excludes middleware from being invoked when action is handled. + * It is useful to avoid invoking one of the parent group middleware for + * a certain route. + */ public function __construct( string $pattern, ?string $name = null, diff --git a/src/Attribute/Options.php b/src/Attribute/Options.php index 9a8797da..f4521506 100644 --- a/src/Attribute/Options.php +++ b/src/Attribute/Options.php @@ -11,6 +11,13 @@ #[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] final class Options extends Route { + /** + * @param array $defaults Parameter default values indexed by parameter names. + * @param bool $override Marks route as override. When added it will replace existing route with the same name. + * @param array $disabledMiddlewares Excludes middleware from being invoked when action is handled. + * It is useful to avoid invoking one of the parent group middleware for + * a certain route. + */ public function __construct( string $pattern, ?string $name = null, diff --git a/src/Attribute/Patch.php b/src/Attribute/Patch.php index 7e7675fd..b587a6f6 100644 --- a/src/Attribute/Patch.php +++ b/src/Attribute/Patch.php @@ -11,6 +11,13 @@ #[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] final class Patch extends Route { + /** + * @param array $defaults Parameter default values indexed by parameter names. + * @param bool $override Marks route as override. When added it will replace existing route with the same name. + * @param array $disabledMiddlewares Excludes middleware from being invoked when action is handled. + * It is useful to avoid invoking one of the parent group middleware for + * a certain route. + */ public function __construct( string $pattern, ?string $name = null, diff --git a/src/Attribute/Post.php b/src/Attribute/Post.php index cd4561fe..5961f40a 100644 --- a/src/Attribute/Post.php +++ b/src/Attribute/Post.php @@ -11,6 +11,13 @@ #[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] final class Post extends Route { + /** + * @param array $defaults Parameter default values indexed by parameter names. + * @param bool $override Marks route as override. When added it will replace existing route with the same name. + * @param array $disabledMiddlewares Excludes middleware from being invoked when action is handled. + * It is useful to avoid invoking one of the parent group middleware for + * a certain route. + */ public function __construct( string $pattern, ?string $name = null, diff --git a/src/Attribute/Put.php b/src/Attribute/Put.php index eb69c023..aff8b532 100644 --- a/src/Attribute/Put.php +++ b/src/Attribute/Put.php @@ -11,6 +11,13 @@ #[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] final class Put extends Route { + /** + * @param array $defaults Parameter default values indexed by parameter names. + * @param bool $override Marks route as override. When added it will replace existing route with the same name. + * @param array $disabledMiddlewares Excludes middleware from being invoked when action is handled. + * It is useful to avoid invoking one of the parent group middleware for + * a certain route. + */ public function __construct( string $pattern, ?string $name = null, diff --git a/src/Group.php b/src/Group.php index 23c11ba0..38ade3c5 100644 --- a/src/Group.php +++ b/src/Group.php @@ -24,6 +24,14 @@ final class Group * @var array|callable|string|null Middleware definition for CORS requests. */ private $corsMiddleware; + /** + * @var string[] + */ + private array $hosts = []; + /** + * @var array[]|callable[]|string[] + */ + private array $middlewares = []; /** * @param array $disabledMiddlewares Excludes middleware from being invoked when action is handled. @@ -32,12 +40,16 @@ final class Group */ public function __construct( private ?string $prefix = null, - private array $middlewares = [], - private array $hosts = [], + array $middlewares = [], + array $hosts = [], private ?string $namePrefix = null, private array $disabledMiddlewares = [], array|callable|string|null $corsMiddleware = null ) { + $this->assertMiddlewares($middlewares); + $this->assertHosts($hosts); + $this->middlewares = $middlewares; + $this->hosts = $hosts; $this->corsMiddleware = $corsMiddleware; } @@ -202,7 +214,7 @@ public function getData(string $key): mixed private function getBuiltMiddlewares(): array { - if ($this->builtMiddlewares !== []) { + if (!empty($this->builtMiddlewares)) { return $this->builtMiddlewares; } @@ -217,4 +229,37 @@ private function getBuiltMiddlewares(): array return $this->builtMiddlewares = array_values($builtMiddlewares); } + + /** + * @psalm-assert array $hosts + */ + private function assertHosts(array $hosts): void + { + foreach ($hosts as $host) { + if (!is_string($host)) { + throw new \InvalidArgumentException('Invalid hosts provided, list of string expected.'); + } + } + } + + /** + * @psalm-assert array $middlewares + */ + private function assertMiddlewares(array $middlewares): void + { + /** @var mixed $middleware */ + foreach ($middlewares as $middleware) { + if (is_string($middleware)) { + continue; + } + + if (is_callable($middleware) || is_array($middleware)) { + continue; + } + + throw new \InvalidArgumentException( + 'Invalid middlewares provided, list of string or array or callable expected.' + ); + } + } } diff --git a/src/Route.php b/src/Route.php index 5445b2f1..5d5a3f53 100644 --- a/src/Route.php +++ b/src/Route.php @@ -15,7 +15,7 @@ /** * Route defines a mapping from URL to callback / name and vice versa. */ -#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] +#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] class Route implements Stringable { private bool $actionAdded = false; @@ -23,195 +23,92 @@ class Route implements Stringable * @var array[]|callable[]|string[] */ private array $builtMiddlewares = []; + /** + * @var array[]|callable[]|string[] + */ + private array $middlewares = []; + /** + * @var string[] + */ + private array $methods; + /** + * @var string[] + */ + private array $hosts = []; + /** + * @var array + */ + private array $defaults = []; /** - * @param array $defaults Parameter default values indexed by parameter names. + * @param array $defaults Parameter default values indexed by parameter names. * @param bool $override Marks route as override. When added it will replace existing route with the same name. * @param array $disabledMiddlewares Excludes middleware from being invoked when action is handled. * It is useful to avoid invoking one of the parent group middleware for * a certain route. */ public function __construct( - private array $methods, + array $methods, private string $pattern, private ?string $name = null, - private array $middlewares = [], - private array $defaults = [], - private array $hosts = [], + array $middlewares = [], + array $defaults = [], + array $hosts = [], private bool $override = false, private array $disabledMiddlewares = [], ) { + $this->assertListOfStrings($methods, 'methods'); + $this->assertMiddlewares($middlewares); + $this->assertListOfStrings($hosts, 'hosts'); + $this->methods = $methods; + $this->middlewares = $middlewares; + $this->hosts = $hosts; + $this->defaults = array_map('\strval', $defaults); } - public static function get( - string $pattern, - ?string $name = null, - array $middlewares = [], - array $defaults = [], - array $hosts = [], - bool $override = false, - array $disabledMiddlewares = [] - ): self { - return self::methods( - [Method::GET], - $pattern, - $name, - $middlewares, - $defaults, - $hosts, - $override, - $disabledMiddlewares - ); + public static function get(string $pattern): self + { + return self::methods([Method::GET], $pattern); } - public static function post( - string $pattern, - ?string $name = null, - array $middlewares = [], - array $defaults = [], - array $hosts = [], - bool $override = false, - array $disabledMiddlewares = [] - ): self { - return self::methods( - [Method::POST], - $pattern, - $name, - $middlewares, - $defaults, - $hosts, - $override, - $disabledMiddlewares - ); + public static function post(string $pattern): self + { + return self::methods([Method::POST], $pattern); } - public static function put( - string $pattern, - ?string $name = null, - array $middlewares = [], - array $defaults = [], - array $hosts = [], - bool $override = false, - array $disabledMiddlewares = [] - ): self { - return self::methods( - [Method::PUT], - $pattern, - $name, - $middlewares, - $defaults, - $hosts, - $override, - $disabledMiddlewares - ); + public static function put(string $pattern): self + { + return self::methods([Method::PUT], $pattern); } - public static function delete( - string $pattern, - ?string $name = null, - array $middlewares = [], - array $defaults = [], - array $hosts = [], - bool $override = false, - array $disabledMiddlewares = [] - ): self { - return self::methods( - [Method::DELETE], - $pattern, - $name, - $middlewares, - $defaults, - $hosts, - $override, - $disabledMiddlewares - ); + public static function delete(string $pattern): self + { + return self::methods([Method::DELETE], $pattern); } - public static function patch( - string $pattern, - ?string $name = null, - array $middlewares = [], - array $defaults = [], - array $hosts = [], - bool $override = false, - array $disabledMiddlewares = [] - ): self { - return self::methods( - [Method::PATCH], - $pattern, - $name, - $middlewares, - $defaults, - $hosts, - $override, - $disabledMiddlewares - ); + public static function patch(string $pattern): self + { + return self::methods([Method::PATCH], $pattern); } - public static function head( - string $pattern, - ?string $name = null, - array $middlewares = [], - array $defaults = [], - array $hosts = [], - bool $override = false, - array $disabledMiddlewares = [] - ): self { - return self::methods( - [Method::HEAD], - $pattern, - $name, - $middlewares, - $defaults, - $hosts, - $override, - $disabledMiddlewares - ); + public static function head(string $pattern): self + { + return self::methods([Method::HEAD], $pattern); } - public static function options( - string $pattern, - ?string $name = null, - array $middlewares = [], - array $defaults = [], - array $hosts = [], - bool $override = false, - array $disabledMiddlewares = [] - ): self { - return self::methods( - [Method::OPTIONS], - $pattern, - $name, - $middlewares, - $defaults, - $hosts, - $override, - $disabledMiddlewares - ); + public static function options(string $pattern): self + { + return self::methods([Method::OPTIONS], $pattern); } /** * @param string[] $methods */ - public static function methods( - array $methods, - string $pattern, - ?string $name = null, - array $middlewares = [], - array $defaults = [], - array $hosts = [], - bool $override = false, - array $disabledMiddlewares = [] - ): self { + public static function methods(array $methods, string $pattern): self + { return new self( methods: $methods, - pattern: $pattern, - name: $name, - middlewares: $middlewares, - defaults: $defaults, - hosts: $hosts, - override: $override, - disabledMiddlewares: $disabledMiddlewares + pattern: $pattern ); } @@ -365,7 +262,7 @@ public function getData(string $key): mixed 'methods' => $this->methods, 'defaults' => $this->defaults, 'override' => $this->override, - 'hasMiddlewares' => $this->middlewares !== [], + 'hasMiddlewares' => !empty($this->middlewares), default => throw new InvalidArgumentException('Unknown data key: ' . $key), }; } @@ -416,7 +313,7 @@ public function getBuiltMiddlewares(): array { // Don't build middlewares if we did it earlier. // This improves performance in event-loop applications. - if ($this->builtMiddlewares !== []) { + if (!empty($this->builtMiddlewares)) { return $this->builtMiddlewares; } @@ -430,4 +327,37 @@ public function getBuiltMiddlewares(): array return $this->builtMiddlewares = $builtMiddlewares; } + + /** + * @psalm-assert array $items + */ + private function assertListOfStrings(array $items, string $argument): void + { + foreach ($items as $item) { + if (!is_string($item)) { + throw new \InvalidArgumentException('Invalid ' . $argument . ' provided, list of string expected.'); + } + } + } + + /** + * @psalm-assert array $middlewares + */ + private function assertMiddlewares(array $middlewares): void + { + /** @var mixed $middleware */ + foreach ($middlewares as $middleware) { + if (is_string($middleware)) { + continue; + } + + if (is_callable($middleware) || is_array($middleware)) { + continue; + } + + throw new \InvalidArgumentException( + 'Invalid middlewares provided, list of string or array or callable expected.' + ); + } + } } diff --git a/src/RouteAttributesRegistrar.php b/src/RouteAttributesRegistrar.php index c6c02f9a..3e5855dc 100644 --- a/src/RouteAttributesRegistrar.php +++ b/src/RouteAttributesRegistrar.php @@ -32,14 +32,17 @@ public function register(): void [$groupAttribute] = $groupAttributes; /** @var Group $group */ $group = $groupAttribute->newInstance(); - $this->routeCollector->addRoute($group->routes(...$routes)); + $this->routeCollector->addRoute($group->routes(...iterator_to_array($routes))); } else { - $this->routeCollector->addRoute(...$routes); + $this->routeCollector->addRoute(...iterator_to_array($routes)); } } } - private function lookupRoutes(\ReflectionClass $reflectionClass): iterable + /** + * @return \Generator + */ + private function lookupRoutes(\ReflectionClass $reflectionClass): \Generator { foreach ($reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $reflectionMethod) { foreach ( diff --git a/tests/RouteAttributesRegistrarTest.php b/tests/RouteAttributesRegistrarTest.php index 0cd47643..0372aeee 100644 --- a/tests/RouteAttributesRegistrarTest.php +++ b/tests/RouteAttributesRegistrarTest.php @@ -11,6 +11,11 @@ class RouteAttributesRegistrarTest extends TestCase { + public function setUp(): void + { + parent::setUp(); + class_exists(TestController::class); + } public function testRegister(): void { $routeCollector = new RouteCollector(); @@ -19,7 +24,8 @@ public function testRegister(): void $registrar->register(); $this->assertCount(1, $items = $routeCollector->getItems()); - $this->assertCount(1, $items[0]->getBuiltMiddlewares()); - $this->assertSame([TestController::class, 'attributeAction'], $items[0]->getBuiltMiddlewares()[0]); + $this->assertCount(1, $items[0]->getData('routes')); + $this->assertCount(1, $items[0]->getData('routes')[0]->getBuiltMiddlewares()); + $this->assertSame([TestController::class, 'attributeAction'], $items[0]->getData('routes')[0]->getBuiltMiddlewares()[0]); } } diff --git a/tests/Support/TestController.php b/tests/Support/TestController.php index 6d3c4faf..57147cfa 100644 --- a/tests/Support/TestController.php +++ b/tests/Support/TestController.php @@ -8,7 +8,9 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Yiisoft\Router\Attribute\Get; +use Yiisoft\Router\Group; +#[Group('/test')] final class TestController { public function index(ServerRequestInterface $request): ResponseInterface From 4e6347652bb57573e6b783dc88545c52aa0cba11 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Thu, 15 Jun 2023 14:39:40 +0000 Subject: [PATCH 09/63] Apply fixes from StyleCI --- src/Attribute/Delete.php | 2 +- src/Attribute/Get.php | 2 +- src/Attribute/Head.php | 2 +- src/Attribute/Options.php | 2 +- src/Attribute/Patch.php | 2 +- src/Attribute/Post.php | 2 +- src/Attribute/Put.php | 2 +- src/Route.php | 4 ++-- tests/RouteAttributesRegistrarTest.php | 1 + 9 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/Attribute/Delete.php b/src/Attribute/Delete.php index 6a3f752c..6ed574cd 100644 --- a/src/Attribute/Delete.php +++ b/src/Attribute/Delete.php @@ -12,7 +12,7 @@ final class Delete extends Route { /** - * @param array $defaults Parameter default values indexed by parameter names. + * @param array $defaults Parameter default values indexed by parameter names. * @param bool $override Marks route as override. When added it will replace existing route with the same name. * @param array $disabledMiddlewares Excludes middleware from being invoked when action is handled. * It is useful to avoid invoking one of the parent group middleware for diff --git a/src/Attribute/Get.php b/src/Attribute/Get.php index 2d5a5415..007a6567 100644 --- a/src/Attribute/Get.php +++ b/src/Attribute/Get.php @@ -12,7 +12,7 @@ final class Get extends Route { /** - * @param array $defaults Parameter default values indexed by parameter names. + * @param array $defaults Parameter default values indexed by parameter names. * @param bool $override Marks route as override. When added it will replace existing route with the same name. * @param array $disabledMiddlewares Excludes middleware from being invoked when action is handled. * It is useful to avoid invoking one of the parent group middleware for diff --git a/src/Attribute/Head.php b/src/Attribute/Head.php index 1c4e7a70..fb5f6159 100644 --- a/src/Attribute/Head.php +++ b/src/Attribute/Head.php @@ -12,7 +12,7 @@ final class Head extends Route { /** - * @param array $defaults Parameter default values indexed by parameter names. + * @param array $defaults Parameter default values indexed by parameter names. * @param bool $override Marks route as override. When added it will replace existing route with the same name. * @param array $disabledMiddlewares Excludes middleware from being invoked when action is handled. * It is useful to avoid invoking one of the parent group middleware for diff --git a/src/Attribute/Options.php b/src/Attribute/Options.php index f4521506..9b35213f 100644 --- a/src/Attribute/Options.php +++ b/src/Attribute/Options.php @@ -12,7 +12,7 @@ final class Options extends Route { /** - * @param array $defaults Parameter default values indexed by parameter names. + * @param array $defaults Parameter default values indexed by parameter names. * @param bool $override Marks route as override. When added it will replace existing route with the same name. * @param array $disabledMiddlewares Excludes middleware from being invoked when action is handled. * It is useful to avoid invoking one of the parent group middleware for diff --git a/src/Attribute/Patch.php b/src/Attribute/Patch.php index b587a6f6..c16cbee0 100644 --- a/src/Attribute/Patch.php +++ b/src/Attribute/Patch.php @@ -12,7 +12,7 @@ final class Patch extends Route { /** - * @param array $defaults Parameter default values indexed by parameter names. + * @param array $defaults Parameter default values indexed by parameter names. * @param bool $override Marks route as override. When added it will replace existing route with the same name. * @param array $disabledMiddlewares Excludes middleware from being invoked when action is handled. * It is useful to avoid invoking one of the parent group middleware for diff --git a/src/Attribute/Post.php b/src/Attribute/Post.php index 5961f40a..6c3dfe88 100644 --- a/src/Attribute/Post.php +++ b/src/Attribute/Post.php @@ -12,7 +12,7 @@ final class Post extends Route { /** - * @param array $defaults Parameter default values indexed by parameter names. + * @param array $defaults Parameter default values indexed by parameter names. * @param bool $override Marks route as override. When added it will replace existing route with the same name. * @param array $disabledMiddlewares Excludes middleware from being invoked when action is handled. * It is useful to avoid invoking one of the parent group middleware for diff --git a/src/Attribute/Put.php b/src/Attribute/Put.php index aff8b532..1f4bde5a 100644 --- a/src/Attribute/Put.php +++ b/src/Attribute/Put.php @@ -12,7 +12,7 @@ final class Put extends Route { /** - * @param array $defaults Parameter default values indexed by parameter names. + * @param array $defaults Parameter default values indexed by parameter names. * @param bool $override Marks route as override. When added it will replace existing route with the same name. * @param array $disabledMiddlewares Excludes middleware from being invoked when action is handled. * It is useful to avoid invoking one of the parent group middleware for diff --git a/src/Route.php b/src/Route.php index 5d5a3f53..d30bb3bf 100644 --- a/src/Route.php +++ b/src/Route.php @@ -36,12 +36,12 @@ class Route implements Stringable */ private array $hosts = []; /** - * @var array + * @var array */ private array $defaults = []; /** - * @param array $defaults Parameter default values indexed by parameter names. + * @param array $defaults Parameter default values indexed by parameter names. * @param bool $override Marks route as override. When added it will replace existing route with the same name. * @param array $disabledMiddlewares Excludes middleware from being invoked when action is handled. * It is useful to avoid invoking one of the parent group middleware for diff --git a/tests/RouteAttributesRegistrarTest.php b/tests/RouteAttributesRegistrarTest.php index 0372aeee..9ece4e32 100644 --- a/tests/RouteAttributesRegistrarTest.php +++ b/tests/RouteAttributesRegistrarTest.php @@ -16,6 +16,7 @@ public function setUp(): void parent::setUp(); class_exists(TestController::class); } + public function testRegister(): void { $routeCollector = new RouteCollector(); From a0fb7a28adfb5ce5650dd87ea473f7d6fd0594b1 Mon Sep 17 00:00:00 2001 From: Rustam Date: Thu, 15 Jun 2023 22:55:34 +0500 Subject: [PATCH 10/63] Add test cases --- src/Group.php | 29 ++++------------ src/Route.php | 4 +-- tests/GroupTest.php | 17 ++++++++++ tests/RouteTest.php | 80 +++++++++++++++++++++++++++------------------ 4 files changed, 73 insertions(+), 57 deletions(-) diff --git a/src/Group.php b/src/Group.php index 38ade3c5..baddaf3c 100644 --- a/src/Group.php +++ b/src/Group.php @@ -58,22 +58,9 @@ public function __construct( * * @param string|null $prefix URL prefix to prepend to all routes of the group. */ - public static function create( - ?string $prefix = null, - array $middlewares = [], - array $hosts = [], - ?string $namePrefix = null, - array $disabledMiddlewares = [], - array|callable|string|null $corsMiddleware = null - ): self { - return new self( - prefix: $prefix, - middlewares: $middlewares, - hosts: $hosts, - namePrefix: $namePrefix, - disabledMiddlewares: $disabledMiddlewares, - corsMiddleware: $corsMiddleware - ); + public static function create(?string $prefix = null): self + { + return new self($prefix); } public function routes(self|Route ...$routes): self @@ -237,7 +224,7 @@ private function assertHosts(array $hosts): void { foreach ($hosts as $host) { if (!is_string($host)) { - throw new \InvalidArgumentException('Invalid hosts provided, list of string expected.'); + throw new \InvalidArgumentException('Invalid $hosts provided, list of string expected.'); } } } @@ -249,16 +236,12 @@ private function assertMiddlewares(array $middlewares): void { /** @var mixed $middleware */ foreach ($middlewares as $middleware) { - if (is_string($middleware)) { - continue; - } - - if (is_callable($middleware) || is_array($middleware)) { + if (is_string($middleware) || is_callable($middleware) || is_array($middleware)) { continue; } throw new \InvalidArgumentException( - 'Invalid middlewares provided, list of string or array or callable expected.' + 'Invalid $middlewares provided, list of string or array or callable expected.' ); } } diff --git a/src/Route.php b/src/Route.php index d30bb3bf..49329709 100644 --- a/src/Route.php +++ b/src/Route.php @@ -335,7 +335,7 @@ private function assertListOfStrings(array $items, string $argument): void { foreach ($items as $item) { if (!is_string($item)) { - throw new \InvalidArgumentException('Invalid ' . $argument . ' provided, list of string expected.'); + throw new \InvalidArgumentException('Invalid $' . $argument . ' provided, list of string expected.'); } } } @@ -356,7 +356,7 @@ private function assertMiddlewares(array $middlewares): void } throw new \InvalidArgumentException( - 'Invalid middlewares provided, list of string or array or callable expected.' + 'Invalid $middlewares provided, list of string or array or callable expected.' ); } } diff --git a/tests/GroupTest.php b/tests/GroupTest.php index c711fb87..569151b6 100644 --- a/tests/GroupTest.php +++ b/tests/GroupTest.php @@ -41,6 +41,15 @@ public function testAddMiddleware(): void $this->assertSame($middleware2, $group->getData('middlewares')[1]); } + public function testInvalidMiddlewares(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid $middlewares provided, list of string or array or callable expected.'); + + $middleware = static fn () => new Response(); + $group = new Group('/api', [$middleware, new \stdClass]); + } + public function testDisabledMiddlewareDefinitions(): void { $group = Group::create() @@ -241,6 +250,14 @@ public function testHosts(): void $this->assertSame(['https://yiiframework.com', 'https://yiiframework.ru'], $group->getData('hosts')); } + public function testInvalidHosts(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid $hosts provided, list of string expected.'); + + $group = new Group(hosts: ['https://yiiframework.com/', 123]); + } + public function testName(): void { $group = Group::create()->namePrefix('api'); diff --git a/tests/RouteTest.php b/tests/RouteTest.php index 6f5d03fe..7600f6f7 100644 --- a/tests/RouteTest.php +++ b/tests/RouteTest.php @@ -19,9 +19,9 @@ use Yiisoft\Router\Route; use Yiisoft\Router\Tests\Support\AssertTrait; use Yiisoft\Router\Tests\Support\Container; +use Yiisoft\Router\Tests\Support\TestController; use Yiisoft\Router\Tests\Support\TestMiddleware1; use Yiisoft\Router\Tests\Support\TestMiddleware2; -use Yiisoft\Router\Tests\Support\TestController; use Yiisoft\Router\Tests\Support\TestMiddleware3; final class RouteTest extends TestCase @@ -132,12 +132,12 @@ public function testHost(): void public function testHosts(): void { $route = Route::get('/') - ->hosts( - 'https://yiiframework.com/', - 'yf.com', - 'yii.com', - 'yf.ru' - ); + ->hosts( + 'https://yiiframework.com/', + 'yf.com', + 'yii.com', + 'yf.ru' + ); $this->assertSame( [ @@ -153,12 +153,12 @@ public function testHosts(): void public function testMultipleHosts(): void { $route = Route::get('/') - ->host('https://yiiframework.com/'); + ->host('https://yiiframework.com/'); $multipleRoute = Route::get('/') - ->hosts( - 'https://yiiframework.com/', - 'https://yiiframework.ru/' - ); + ->hosts( + 'https://yiiframework.com/', + 'https://yiiframework.ru/' + ); $this->assertCount(1, $route->getData('hosts')); $this->assertCount(2, $multipleRoute->getData('hosts')); @@ -198,17 +198,17 @@ public function dataToString(): array public function testToString(string $expected, string $pattern): void { $route = Route::methods([Method::GET, Method::POST], $pattern) - ->name('test.route') - ->host('yiiframework.com'); + ->name('test.route') + ->host('yiiframework.com'); - $this->assertSame('[test.route] GET,POST ' . $expected, (string)$route); + $this->assertSame('[test.route] GET,POST ' . $expected, (string) $route); } public function testToStringSimple(): void { $route = Route::get('/'); - $this->assertSame('GET /', (string)$route); + $this->assertSame('GET /', (string) $route); } public function testMiddlewareAfterAction(): void @@ -229,6 +229,14 @@ public function testPrependMiddlewareBeforeAction(): void $route->prependMiddleware(static fn () => new Response()); } + public function testInvalidMiddlewares(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid $middlewares provided, list of string or array or callable expected.'); + + $route = new Route([Method::GET], '/', middlewares: [static fn () => new Response(), (object) ['test' => 1]]); + } + public function testDisabledMiddlewareDefinitions(): void { $request = new ServerRequest('GET', '/'); @@ -243,9 +251,9 @@ public function testDisabledMiddlewareDefinitions(): void ); $route = Route::get('/') - ->middleware(TestMiddleware1::class, TestMiddleware2::class, TestMiddleware3::class) - ->action([TestController::class, 'index']) - ->disableMiddleware(TestMiddleware1::class, TestMiddleware3::class); + ->middleware(TestMiddleware1::class, TestMiddleware2::class, TestMiddleware3::class) + ->action([TestController::class, 'index']) + ->disableMiddleware(TestMiddleware1::class, TestMiddleware3::class); $dispatcher = $injectDispatcher->withMiddlewares($route->getBuiltMiddlewares()); @@ -268,9 +276,9 @@ public function testPrependMiddlewareDefinitions(): void ); $route = Route::get('/') - ->middleware(TestMiddleware3::class) - ->action([TestController::class, 'index']) - ->prependMiddleware(TestMiddleware1::class, TestMiddleware2::class); + ->middleware(TestMiddleware3::class) + ->action([TestController::class, 'index']) + ->prependMiddleware(TestMiddleware1::class, TestMiddleware2::class); $dispatcher = $injectDispatcher->withMiddlewares($route->getBuiltMiddlewares()); @@ -282,14 +290,14 @@ public function testPrependMiddlewareDefinitions(): void public function testDebugInfo(): void { $route = Route::get('/') - ->name('test') - ->host('example.com') - ->defaults(['age' => 42]) - ->override() - ->middleware(middleware: TestMiddleware1::class) - ->disableMiddleware(middleware: TestMiddleware2::class) - ->action('go') - ->prependMiddleware(middleware: TestMiddleware3::class); + ->name('test') + ->host('example.com') + ->defaults(['age' => 42]) + ->override() + ->middleware(middleware: TestMiddleware1::class) + ->disableMiddleware(middleware: TestMiddleware2::class) + ->action('go') + ->prependMiddleware(middleware: TestMiddleware3::class); $expected = <<assertSame(['a.com', 'b.com'], $route->getData('hosts')); } + public function testInvalidHosts(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid $hosts provided, list of string expected.'); + + $route = new Route([Method::GET], '/', hosts: ['b.com', 123]); + } + public function testImmutability(): void { $route = Route::get('/'); @@ -362,8 +378,8 @@ public function testImmutability(): void public function testBuiltMiddlewares(): void { $route = Route::get('') - ->middleware(TestMiddleware1::class) - ->action(static fn () => new Response(200)); + ->middleware(TestMiddleware1::class) + ->action(static fn () => new Response(200)); $builtMiddlewares = $route->getBuiltMiddlewares(); From ed2f60f8885b0b7ed73e502fb51d436b6fbcde62 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Thu, 15 Jun 2023 17:55:48 +0000 Subject: [PATCH 11/63] Apply fixes from StyleCI --- tests/GroupTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/GroupTest.php b/tests/GroupTest.php index 569151b6..3d57bbd6 100644 --- a/tests/GroupTest.php +++ b/tests/GroupTest.php @@ -47,7 +47,7 @@ public function testInvalidMiddlewares(): void $this->expectExceptionMessage('Invalid $middlewares provided, list of string or array or callable expected.'); $middleware = static fn () => new Response(); - $group = new Group('/api', [$middleware, new \stdClass]); + $group = new Group('/api', [$middleware, new \stdClass()]); } public function testDisabledMiddlewareDefinitions(): void From c239d5399005911d6b26e4d39f957e3bf52257ee Mon Sep 17 00:00:00 2001 From: Rustam Date: Sat, 19 Aug 2023 10:28:44 +0500 Subject: [PATCH 12/63] Improve Route --- .phpstorm.meta.php/Route.php | 3 ++- src/Middleware/Router.php | 2 +- src/Route.php | 10 +++++++++- tests/GroupTest.php | 6 +++--- tests/RouteAttributesRegistrarTest.php | 4 ++-- tests/RouteCollectionTest.php | 8 ++++---- tests/RouteTest.php | 8 ++++---- 7 files changed, 25 insertions(+), 16 deletions(-) diff --git a/.phpstorm.meta.php/Route.php b/.phpstorm.meta.php/Route.php index ae15f025..1b0dad0b 100644 --- a/.phpstorm.meta.php/Route.php +++ b/.phpstorm.meta.php/Route.php @@ -13,6 +13,7 @@ 'methods', 'override', 'defaults', - 'hasMiddlewares' + 'hasMiddlewares', + 'builtMiddlewares' ); } diff --git a/src/Middleware/Router.php b/src/Middleware/Router.php index 258ff368..e882151b 100644 --- a/src/Middleware/Router.php +++ b/src/Middleware/Router.php @@ -55,7 +55,7 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface $this->currentRoute->setRouteWithArguments($result->route(), $result->arguments()); return $this->dispatcher - ->withMiddlewares($result->route()->getBuiltMiddlewares()) + ->withMiddlewares($result->route()->getData('builtMiddlewares')) ->dispatch($request, $handler); } } diff --git a/src/Route.php b/src/Route.php index 49329709..9f58b925 100644 --- a/src/Route.php +++ b/src/Route.php @@ -51,6 +51,7 @@ public function __construct( array $methods, private string $pattern, private ?string $name = null, + array|callable|string $action = null, array $middlewares = [], array $defaults = [], array $hosts = [], @@ -64,6 +65,10 @@ public function __construct( $this->middlewares = $middlewares; $this->hosts = $hosts; $this->defaults = array_map('\strval', $defaults); + if (!empty($action)) { + $this->middlewares[] = $action; + $this->actionAdded = true; + } } public static function get(string $pattern): self @@ -244,7 +249,9 @@ public function disableMiddleware(mixed ...$middlewareDefinition): self * (T is 'hosts' ? array : * (T is 'methods' ? array : * (T is 'defaults' ? array : - * (T is ('override'|'hasMiddlewares') ? bool : mixed) + * (T is ('override'|'hasMiddlewares') ? bool : + * (T is 'builtMiddlewares' ? array : mixed) + * ) * ) * ) * ) @@ -263,6 +270,7 @@ public function getData(string $key): mixed 'defaults' => $this->defaults, 'override' => $this->override, 'hasMiddlewares' => !empty($this->middlewares), + 'builtMiddlewares' => $this->getBuiltMiddlewares(), default => throw new InvalidArgumentException('Unknown data key: ' . $key), }; } diff --git a/tests/GroupTest.php b/tests/GroupTest.php index 3d57bbd6..5057c332 100644 --- a/tests/GroupTest.php +++ b/tests/GroupTest.php @@ -120,7 +120,7 @@ public function testAddNestedMiddleware(): void $routeCollection = new RouteCollection($collector); $route = $routeCollection->getRoute('request1'); $response = $this->getDispatcher() - ->withMiddlewares($route->getBuiltMiddlewares()) + ->withMiddlewares($route->getData('builtMiddlewares')) ->dispatch($request, $this->getRequestHandler()); $this->assertSame(200, $response->getStatusCode()); $this->assertSame('middleware2', $response->getReasonPhrase()); @@ -155,7 +155,7 @@ public function testGroupMiddlewareFullStackCalled(): void $routeCollection = new RouteCollection($collector); $route = $routeCollection->getRoute('request1'); $response = $this->getDispatcher() - ->withMiddlewares($route->getBuiltMiddlewares()) + ->withMiddlewares($route->getData('builtMiddlewares')) ->dispatch($request, $this->getRequestHandler()); $this->assertSame(200, $response->getStatusCode()); $this->assertSame('middleware2', $response->getReasonPhrase()); @@ -184,7 +184,7 @@ public function testGroupMiddlewareStackInterrupted(): void $routeCollection = new RouteCollection($collector); $route = $routeCollection->getRoute('request1'); $response = $this->getDispatcher() - ->withMiddlewares($route->getBuiltMiddlewares()) + ->withMiddlewares($route->getData('builtMiddlewares')) ->dispatch($request, $this->getRequestHandler()); $this->assertSame(403, $response->getStatusCode()); } diff --git a/tests/RouteAttributesRegistrarTest.php b/tests/RouteAttributesRegistrarTest.php index 9ece4e32..66bad2c7 100644 --- a/tests/RouteAttributesRegistrarTest.php +++ b/tests/RouteAttributesRegistrarTest.php @@ -26,7 +26,7 @@ public function testRegister(): void $this->assertCount(1, $items = $routeCollector->getItems()); $this->assertCount(1, $items[0]->getData('routes')); - $this->assertCount(1, $items[0]->getData('routes')[0]->getBuiltMiddlewares()); - $this->assertSame([TestController::class, 'attributeAction'], $items[0]->getData('routes')[0]->getBuiltMiddlewares()[0]); + $this->assertCount(1, $items[0]->getData('routes')[0]->getData('builtMiddlewares')); + $this->assertSame([TestController::class, 'attributeAction'], $items[0]->getData('routes')[0]->getData('builtMiddlewares')[0]); } } diff --git a/tests/RouteCollectionTest.php b/tests/RouteCollectionTest.php index 47922aee..86eb2171 100644 --- a/tests/RouteCollectionTest.php +++ b/tests/RouteCollectionTest.php @@ -277,10 +277,10 @@ public function testCollectorMiddlewareFullstackCalled(): void $route2 = $routeCollection->getRoute('view'); $request = new ServerRequest('GET', '/'); $response1 = $this->getDispatcher() - ->withMiddlewares($route1->getBuiltMiddlewares()) + ->withMiddlewares($route1->getData('builtMiddlewares')) ->dispatch($request, $this->getRequestHandler()); $response2 = $this->getDispatcher() - ->withMiddlewares($route2->getBuiltMiddlewares()) + ->withMiddlewares($route2->getData('builtMiddlewares')) ->dispatch($request, $this->getRequestHandler()); $this->assertEquals('middleware1', $response1->getReasonPhrase()); @@ -328,7 +328,7 @@ public function testMiddlewaresOrder(bool $groupWrapped): void $route = (new RouteCollection($collector))->getRoute('main'); - $dispatcher = $injectDispatcher->withMiddlewares($route->getBuiltMiddlewares()); + $dispatcher = $injectDispatcher->withMiddlewares($route->getData('builtMiddlewares')); $response = $dispatcher->dispatch($request, $this->getRequestHandler()); $this->assertSame(200, $response->getStatusCode()); @@ -354,7 +354,7 @@ public function testStaticRouteWithCollectorMiddlewares(): void $route = (new RouteCollection($collector))->getRoute('image'); - $dispatcher = $injectDispatcher->withMiddlewares($route->getBuiltMiddlewares()); + $dispatcher = $injectDispatcher->withMiddlewares($route->getData('builtMiddlewares')); $this->expectException(RuntimeException::class); $this->expectExceptionMessage('Stack is empty.'); diff --git a/tests/RouteTest.php b/tests/RouteTest.php index 7600f6f7..27c33e0b 100644 --- a/tests/RouteTest.php +++ b/tests/RouteTest.php @@ -255,7 +255,7 @@ public function testDisabledMiddlewareDefinitions(): void ->action([TestController::class, 'index']) ->disableMiddleware(TestMiddleware1::class, TestMiddleware3::class); - $dispatcher = $injectDispatcher->withMiddlewares($route->getBuiltMiddlewares()); + $dispatcher = $injectDispatcher->withMiddlewares($route->getData('builtMiddlewares')); $response = $dispatcher->dispatch($request, $this->getRequestHandler()); $this->assertSame(200, $response->getStatusCode()); @@ -280,7 +280,7 @@ public function testPrependMiddlewareDefinitions(): void ->action([TestController::class, 'index']) ->prependMiddleware(TestMiddleware1::class, TestMiddleware2::class); - $dispatcher = $injectDispatcher->withMiddlewares($route->getBuiltMiddlewares()); + $dispatcher = $injectDispatcher->withMiddlewares($route->getData('builtMiddlewares')); $response = $dispatcher->dispatch($request, $this->getRequestHandler()); $this->assertSame(200, $response->getStatusCode()); @@ -381,9 +381,9 @@ public function testBuiltMiddlewares(): void ->middleware(TestMiddleware1::class) ->action(static fn () => new Response(200)); - $builtMiddlewares = $route->getBuiltMiddlewares(); + $builtMiddlewares = $route->getData('builtMiddlewares'); - $this->assertSame($builtMiddlewares, $route->getBuiltMiddlewares()); + $this->assertSame($builtMiddlewares, $route->getData('builtMiddlewares')); } private function getRequestHandler(): RequestHandlerInterface From e8367aa4c1b4cfda33cb1e5ae77faae26cb8c92a Mon Sep 17 00:00:00 2001 From: Rustam Date: Sat, 19 Aug 2023 11:28:03 +0500 Subject: [PATCH 13/63] Add test --- src/Route.php | 2 +- tests/RouteTest.php | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Route.php b/src/Route.php index 9f58b925..f79bab02 100644 --- a/src/Route.php +++ b/src/Route.php @@ -317,7 +317,7 @@ public function __debugInfo() /** * @return array[]|callable[]|string[] */ - public function getBuiltMiddlewares(): array + private function getBuiltMiddlewares(): array { // Don't build middlewares if we did it earlier. // This improves performance in event-loop applications. diff --git a/tests/RouteTest.php b/tests/RouteTest.php index 27c33e0b..d10b046f 100644 --- a/tests/RouteTest.php +++ b/tests/RouteTest.php @@ -28,6 +28,18 @@ final class RouteTest extends TestCase { use AssertTrait; + public function testSimpleInstance(): void + { + $route = new Route( + methods: [Method::GET], + pattern: '/', + action: [TestController::class, 'index'], + ); + + $this->assertInstanceOf(Route::class, $route); + $this->assertNotEmpty($route->getData('builtMiddlewares')); + } + public function testName(): void { $route = Route::get('/')->name('test.route'); From 2a60945da3ba44fa945639119925329bd055c6cf Mon Sep 17 00:00:00 2001 From: Rustam Date: Sat, 19 Aug 2023 16:22:12 +0500 Subject: [PATCH 14/63] Increase coverage --- src/Route.php | 2 ++ tests/Attribute/DeleteTest.php | 7 +++++++ tests/Attribute/GetTest.php | 7 +++++++ tests/Attribute/HeadTest.php | 7 +++++++ tests/Attribute/OptionsTest.php | 7 +++++++ tests/Attribute/PatchTest.php | 7 +++++++ tests/Attribute/PostTest.php | 7 +++++++ tests/Attribute/PutTest.php | 7 +++++++ tests/RouteTest.php | 5 ++++- 9 files changed, 55 insertions(+), 1 deletion(-) diff --git a/src/Route.php b/src/Route.php index f79bab02..b09d79c8 100644 --- a/src/Route.php +++ b/src/Route.php @@ -41,6 +41,8 @@ class Route implements Stringable private array $defaults = []; /** + * @param array|callable|string|null $action Action handler. It is a primary middleware definition that + * should be invoked last for a matched route. * @param array $defaults Parameter default values indexed by parameter names. * @param bool $override Marks route as override. When added it will replace existing route with the same name. * @param array $disabledMiddlewares Excludes middleware from being invoked when action is handled. diff --git a/tests/Attribute/DeleteTest.php b/tests/Attribute/DeleteTest.php index 567fce29..5eec0b13 100644 --- a/tests/Attribute/DeleteTest.php +++ b/tests/Attribute/DeleteTest.php @@ -17,4 +17,11 @@ public function testRoute(): void $this->assertSame('/post', $route->getData('pattern')); $this->assertEquals([Method::DELETE], $route->getData('methods')); } + + public function testOverride(): void + { + $route = new Delete('/', override: true); + + $this->assertTrue($route->getData('override')); + } } diff --git a/tests/Attribute/GetTest.php b/tests/Attribute/GetTest.php index aad76124..659daff0 100644 --- a/tests/Attribute/GetTest.php +++ b/tests/Attribute/GetTest.php @@ -17,4 +17,11 @@ public function testRoute(): void $this->assertSame('/post', $route->getData('pattern')); $this->assertEquals([Method::GET], $route->getData('methods')); } + + public function testOverride(): void + { + $route = new Get('/', override: true); + + $this->assertTrue($route->getData('override')); + } } diff --git a/tests/Attribute/HeadTest.php b/tests/Attribute/HeadTest.php index ab3925b6..9943acc2 100644 --- a/tests/Attribute/HeadTest.php +++ b/tests/Attribute/HeadTest.php @@ -17,4 +17,11 @@ public function testRoute(): void $this->assertSame('/post', $route->getData('pattern')); $this->assertEquals([Method::HEAD], $route->getData('methods')); } + + public function testOverride(): void + { + $route = new Head('/', override: true); + + $this->assertTrue($route->getData('override')); + } } diff --git a/tests/Attribute/OptionsTest.php b/tests/Attribute/OptionsTest.php index 35f6fe83..fa44dbfe 100644 --- a/tests/Attribute/OptionsTest.php +++ b/tests/Attribute/OptionsTest.php @@ -17,4 +17,11 @@ public function testRoute(): void $this->assertSame('/post', $route->getData('pattern')); $this->assertEquals([Method::OPTIONS], $route->getData('methods')); } + + public function testOverride(): void + { + $route = new Options('/', override: true); + + $this->assertTrue($route->getData('override')); + } } diff --git a/tests/Attribute/PatchTest.php b/tests/Attribute/PatchTest.php index 21b902d3..fca22ea1 100644 --- a/tests/Attribute/PatchTest.php +++ b/tests/Attribute/PatchTest.php @@ -17,4 +17,11 @@ public function testRoute(): void $this->assertSame('/post', $route->getData('pattern')); $this->assertEquals([Method::PATCH], $route->getData('methods')); } + + public function testOverride(): void + { + $route = new Patch('/', override: true); + + $this->assertTrue($route->getData('override')); + } } diff --git a/tests/Attribute/PostTest.php b/tests/Attribute/PostTest.php index 80c3fc0b..e4011e0f 100644 --- a/tests/Attribute/PostTest.php +++ b/tests/Attribute/PostTest.php @@ -17,4 +17,11 @@ public function testRoute(): void $this->assertSame('/post', $route->getData('pattern')); $this->assertEquals([Method::POST], $route->getData('methods')); } + + public function testOverride(): void + { + $route = new Post('/', override: true); + + $this->assertTrue($route->getData('override')); + } } diff --git a/tests/Attribute/PutTest.php b/tests/Attribute/PutTest.php index af5f5766..8d2c43f7 100644 --- a/tests/Attribute/PutTest.php +++ b/tests/Attribute/PutTest.php @@ -17,4 +17,11 @@ public function testRoute(): void $this->assertSame('/post', $route->getData('pattern')); $this->assertEquals([Method::PUT], $route->getData('methods')); } + + public function testOverride(): void + { + $route = new Put('/', override: true); + + $this->assertTrue($route->getData('override')); + } } diff --git a/tests/RouteTest.php b/tests/RouteTest.php index d10b046f..6e0f146f 100644 --- a/tests/RouteTest.php +++ b/tests/RouteTest.php @@ -34,10 +34,13 @@ public function testSimpleInstance(): void methods: [Method::GET], pattern: '/', action: [TestController::class, 'index'], + middlewares: [TestMiddleware1::class], + override: true, ); $this->assertInstanceOf(Route::class, $route); - $this->assertNotEmpty($route->getData('builtMiddlewares')); + $this->assertCount(2, $route->getData('builtMiddlewares')); + $this->assertTrue($route->getData('override')); } public function testName(): void From e7d7a43b6ad0a25218d3b245260bce8a2c1a2910 Mon Sep 17 00:00:00 2001 From: Rustam Mamadaminov Date: Tue, 10 Oct 2023 22:47:38 +0500 Subject: [PATCH 15/63] Add routes resource & attributes (#220) * Add routes via resource + attributes * Apply fixes from StyleCI * Apply Rector changes (CI) * Fix psalm * Add missing file * Apply fixes from StyleCI * Rename Resource to Provider, add more tests * Apply fixes from StyleCI * Increase coverage * Apply fixes from StyleCI * Use variadic --------- Co-authored-by: StyleCI Bot Co-authored-by: rustamwin --- composer.json | 7 +- src/Provider/ArrayRoutesProvider.php | 23 +++++++ src/Provider/AttributeRoutesProvider.php | 62 +++++++++++++++++ src/Provider/FileRoutesProvider.php | 79 ++++++++++++++++++++++ src/Provider/RoutesProviderInterface.php | 19 ++++++ src/Route.php | 2 +- src/RouteAttributesRegistrar.php | 61 ----------------- src/RouteAttributesRegistrarInterface.php | 18 ----- src/RouteCollector.php | 22 ++++++ src/RouteCollectorInterface.php | 7 ++ tests/Provider/ArrayRoutesProviderTest.php | 25 +++++++ tests/Provider/FileRoutesProviderTest.php | 53 +++++++++++++++ tests/RouteAttributesRegistrarTest.php | 32 --------- tests/RouteCollectorTest.php | 33 +++++++++ tests/Support/resources/foo.php | 8 +++ tests/Support/resources/routes.php | 11 +++ tests/Support/resources/test.php | 6 ++ 17 files changed, 354 insertions(+), 114 deletions(-) create mode 100644 src/Provider/ArrayRoutesProvider.php create mode 100644 src/Provider/AttributeRoutesProvider.php create mode 100644 src/Provider/FileRoutesProvider.php create mode 100644 src/Provider/RoutesProviderInterface.php delete mode 100644 src/RouteAttributesRegistrar.php delete mode 100644 src/RouteAttributesRegistrarInterface.php create mode 100644 tests/Provider/ArrayRoutesProviderTest.php create mode 100644 tests/Provider/FileRoutesProviderTest.php delete mode 100644 tests/RouteAttributesRegistrarTest.php create mode 100644 tests/Support/resources/foo.php create mode 100644 tests/Support/resources/routes.php create mode 100644 tests/Support/resources/test.php diff --git a/composer.json b/composer.json index 0bf73fdc..826b44a2 100644 --- a/composer.json +++ b/composer.json @@ -31,6 +31,7 @@ "require-dev": { "maglnet/composer-require-checker": "^4.4", "nyholm/psr7": "^1.5", + "olvlvl/composer-attribute-collector": "^2.0", "phpunit/phpunit": "^9.5", "psr/container": "^1.1|^2.0", "rector/rector": "^0.18.3", @@ -53,7 +54,8 @@ } }, "suggest": { - "yiisoft/router-fastroute": "Router implementation based on nikic/FastRoute" + "yiisoft/router-fastroute": "Router implementation based on nikic/FastRoute", + "olvlvl/composer-attribute-collector": "Required to register routes using PHP attributes" }, "extra": { "config-plugin-options": { @@ -69,7 +71,8 @@ "allow-plugins": { "infection/extension-installer": true, "composer/package-versions-deprecated": true, - "yiisoft/config": false + "yiisoft/config": false, + "olvlvl/composer-attribute-collector": true } }, "scripts": { diff --git a/src/Provider/ArrayRoutesProvider.php b/src/Provider/ArrayRoutesProvider.php new file mode 100644 index 00000000..83abd222 --- /dev/null +++ b/src/Provider/ArrayRoutesProvider.php @@ -0,0 +1,23 @@ +routes; + } +} diff --git a/src/Provider/AttributeRoutesProvider.php b/src/Provider/AttributeRoutesProvider.php new file mode 100644 index 00000000..373227b0 --- /dev/null +++ b/src/Provider/AttributeRoutesProvider.php @@ -0,0 +1,62 @@ + + */ + private static array $reflectionsCache = []; + + public function getRoutes(): array + { + $routes = []; + $groupRoutes = []; + $routePredicate = Attributes::predicateForAttributeInstanceOf(Route::class); + $targetMethods = Attributes::filterTargetMethods($routePredicate); + foreach ($targetMethods as $targetMethod) { + /** @var Route $route */ + $route = $targetMethod->attribute; + $targetMethodReflection = self::$reflectionsCache[$targetMethod->class] ??= new \ReflectionMethod( + $targetMethod->class, + $targetMethod->name + ); + /** @var Group[] $groupAttributes */ + $groupAttributes = $targetMethodReflection->getAttributes( + Group::class, + \ReflectionAttribute::IS_INSTANCEOF + ); + if (!empty($groupAttributes)) { + $groupRoutes[$targetMethod->class][] = $route->action([$targetMethod->class, $targetMethod->name]); + } else { + $routes[] = $route->action([$targetMethod->class, $targetMethod->name]); + } + } + $groupPredicate = static fn (string $attribute): bool => is_a($attribute, Route::class, true) + || is_a($attribute, Group::class, true); + $targetClasses = Attributes::filterTargetClasses($groupPredicate); + foreach ($targetClasses as $targetClass) { + if (isset($groupRoutes[$targetClass->name])) { + /** @var Group $group */ + $group = $targetClass->attribute; + $routes[] = $group->routes(...$groupRoutes[$targetClass->name]); + } else { + /** @var Route $group */ + $routes[] = $group->action($targetClass->name); + } + } + return $routes; + } +} diff --git a/src/Provider/FileRoutesProvider.php b/src/Provider/FileRoutesProvider.php new file mode 100644 index 00000000..ee130eed --- /dev/null +++ b/src/Provider/FileRoutesProvider.php @@ -0,0 +1,79 @@ +file)) { + throw new \RuntimeException( + 'Failed to provide routes from "' . $this->file . '". File or directory not found.' + ); + } + if (is_dir($this->file) && !is_file($this->file)) { + $directoryRoutes = []; + $files = new \CallbackFilterIterator( + new \FilesystemIterator( + $this->file, + \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS + ), + fn (\SplFileInfo $fileInfo) => $fileInfo->isFile() && $fileInfo->getExtension() === 'php' + ); + /** @var \SplFileInfo[] $files */ + foreach ($files as $file) { + /** @var mixed $fileRoutes */ + $fileRoutes = $scopeRequire($file->getRealPath(), $this->scope); + if (is_array($fileRoutes) && $this->isRoutesAreValid($fileRoutes)) { + array_push( + $directoryRoutes, + ...$fileRoutes + ); + } + } + return $directoryRoutes; + } + + /** @var mixed $routes */ + $routes = $scopeRequire($this->file, $this->scope); + if (is_array($routes) && $this->isRoutesAreValid($routes)) { + return $routes; + } + + return []; + } + + /** + * @psalm-assert-if-true Route[]|Group[] $routes + */ + private function isRoutesAreValid(array $routes): bool + { + foreach ($routes as $route) { + if ( + !is_a($route, Route::class, true) && !is_a($route, Group::class, true) + ) { + return false; + } + } + return true; + } +} diff --git a/src/Provider/RoutesProviderInterface.php b/src/Provider/RoutesProviderInterface.php new file mode 100644 index 00000000..60dc2f3e --- /dev/null +++ b/src/Provider/RoutesProviderInterface.php @@ -0,0 +1,19 @@ +isUserDefined()) { - continue; - } - $routes = $this->lookupRoutes($reflectionClass); - $groupAttributes = $reflectionClass->getAttributes(Group::class, \ReflectionAttribute::IS_INSTANCEOF); - - if (!empty($groupAttributes)) { - [$groupAttribute] = $groupAttributes; - /** @var Group $group */ - $group = $groupAttribute->newInstance(); - $this->routeCollector->addRoute($group->routes(...iterator_to_array($routes))); - } else { - $this->routeCollector->addRoute(...iterator_to_array($routes)); - } - } - } - - /** - * @return \Generator - */ - private function lookupRoutes(\ReflectionClass $reflectionClass): \Generator - { - foreach ($reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $reflectionMethod) { - foreach ( - $reflectionMethod->getAttributes( - Route::class, - \ReflectionAttribute::IS_INSTANCEOF - ) as $reflectionAttribute - ) { - /** @var Route $route */ - $route = $reflectionAttribute->newInstance(); - - yield $route->action([$reflectionClass->getName(), $reflectionMethod->getName()]); - } - } - } -} diff --git a/src/RouteAttributesRegistrarInterface.php b/src/RouteAttributesRegistrarInterface.php deleted file mode 100644 index 6188756c..00000000 --- a/src/RouteAttributesRegistrarInterface.php +++ /dev/null @@ -1,18 +0,0 @@ -providers, + ...array_values($provider) + ); + return $this; + } + public function middleware(array|callable|string ...$middlewareDefinition): RouteCollectorInterface { array_push( @@ -45,6 +61,12 @@ public function prependMiddleware(array|callable|string ...$middlewareDefinition public function getItems(): array { + foreach ($this->providers as $provider) { + array_push( + $this->items, + ...$provider->getRoutes() + ); + } return $this->items; } diff --git a/src/RouteCollectorInterface.php b/src/RouteCollectorInterface.php index 38c5861c..c64246d5 100644 --- a/src/RouteCollectorInterface.php +++ b/src/RouteCollectorInterface.php @@ -4,6 +4,8 @@ namespace Yiisoft\Router; +use Yiisoft\Router\Provider\RoutesProviderInterface; + interface RouteCollectorInterface { /** @@ -11,6 +13,11 @@ interface RouteCollectorInterface */ public function addRoute(Route|Group ...$routes): self; + /** + * Add a provider of routes + */ + public function addProvider(RoutesProviderInterface ...$provider): self; + /** * Appends a handler middleware definition that should be invoked for a matched route. * First added handler will be executed first. diff --git a/tests/Provider/ArrayRoutesProviderTest.php b/tests/Provider/ArrayRoutesProviderTest.php new file mode 100644 index 00000000..edbc7c1e --- /dev/null +++ b/tests/Provider/ArrayRoutesProviderTest.php @@ -0,0 +1,25 @@ +routes(Route::get('/blog')), + ]; + + $resource = new ArrayRoutesProvider($routes); + + $this->assertSame($routes, $resource->getRoutes()); + } +} diff --git a/tests/Provider/FileRoutesProviderTest.php b/tests/Provider/FileRoutesProviderTest.php new file mode 100644 index 00000000..c346991f --- /dev/null +++ b/tests/Provider/FileRoutesProviderTest.php @@ -0,0 +1,53 @@ +routes = require $this->file; + } + + public function testGetRoutes(): void + { + $provider = new FileRoutesProvider($this->file); + + $this->assertEquals($this->routes, $provider->getRoutes()); + } + + public function testGetRoutesInDirectory(): void + { + $provider = new FileRoutesProvider(dirname($this->file)); + + $this->assertEquals($this->routes, $provider->getRoutes()); + } + + public function testGetRoutesWithNotExistFile(): void + { + $file = __DIR__ . '/wrong.php'; + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Failed to provide routes from "' . $file . '". File or directory not found.'); + + $provider = new FileRoutesProvider($file); + $provider->getRoutes(); + } + + public function testGetRoutesWithEmptyRoutes(): void + { + $file = dirname(__DIR__) . '/Support/resources/foo.php'; + + $provider = new FileRoutesProvider($file); + + $this->assertEmpty($provider->getRoutes()); + } +} diff --git a/tests/RouteAttributesRegistrarTest.php b/tests/RouteAttributesRegistrarTest.php deleted file mode 100644 index 66bad2c7..00000000 --- a/tests/RouteAttributesRegistrarTest.php +++ /dev/null @@ -1,32 +0,0 @@ -register(); - - $this->assertCount(1, $items = $routeCollector->getItems()); - $this->assertCount(1, $items[0]->getData('routes')); - $this->assertCount(1, $items[0]->getData('routes')[0]->getData('builtMiddlewares')); - $this->assertSame([TestController::class, 'attributeAction'], $items[0]->getData('routes')[0]->getData('builtMiddlewares')[0]); - } -} diff --git a/tests/RouteCollectorTest.php b/tests/RouteCollectorTest.php index 5fa1ea10..ffbf7ac0 100644 --- a/tests/RouteCollectorTest.php +++ b/tests/RouteCollectorTest.php @@ -7,6 +7,7 @@ use Nyholm\Psr7\Response; use PHPUnit\Framework\TestCase; use Yiisoft\Router\Group; +use Yiisoft\Router\Provider\ArrayRoutesProvider; use Yiisoft\Router\Route; use Yiisoft\Router\RouteCollector; @@ -59,6 +60,38 @@ public function testAddGroup(): void $this->assertContainsOnlyInstancesOf(Group::class, $collector->getItems()); } + public function testAddProvider(): void + { + $logoutRoute = Route::post('/logout'); + $listRoute = Route::get('/'); + $viewRoute = Route::get('/{id}'); + $postGroup = Group::create('/post') + ->routes( + $listRoute, + $viewRoute + ); + + $rootGroup = Group::create() + ->routes( + Group::create('/api') + ->routes( + $logoutRoute, + $postGroup + ), + ); + + $testGroup = Group::create() + ->routes( + Route::get('test/') + ); + + $collector = new RouteCollector(); + $collector->addProvider(new ArrayRoutesProvider([$rootGroup, $postGroup, $testGroup])); + + $this->assertCount(3, $collector->getItems()); + $this->assertContainsOnlyInstancesOf(Group::class, $collector->getItems()); + } + public function testAddMiddleware(): void { $collector = new RouteCollector(); diff --git a/tests/Support/resources/foo.php b/tests/Support/resources/foo.php new file mode 100644 index 00000000..30129aa1 --- /dev/null +++ b/tests/Support/resources/foo.php @@ -0,0 +1,8 @@ +routes(Route::get('/blog')), +]; diff --git a/tests/Support/resources/test.php b/tests/Support/resources/test.php new file mode 100644 index 00000000..94ec2919 --- /dev/null +++ b/tests/Support/resources/test.php @@ -0,0 +1,6 @@ + Date: Wed, 11 Oct 2023 16:35:14 +0500 Subject: [PATCH 16/63] Fix AttributeRoutesProvider --- src/Provider/AttributeRoutesProvider.php | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/Provider/AttributeRoutesProvider.php b/src/Provider/AttributeRoutesProvider.php index 373227b0..829845da 100644 --- a/src/Provider/AttributeRoutesProvider.php +++ b/src/Provider/AttributeRoutesProvider.php @@ -11,6 +11,7 @@ /** * An attribute provider provides routes that declared via PHP Attributes. * Currently, uses `olvlvl/composer-attribute-collector`. {@link https://github.com/olvlvl/composer-attribute-collector}. + * * @codeCoverageIgnore */ final class AttributeRoutesProvider implements RoutesProviderInterface @@ -29,9 +30,8 @@ public function getRoutes(): array foreach ($targetMethods as $targetMethod) { /** @var Route $route */ $route = $targetMethod->attribute; - $targetMethodReflection = self::$reflectionsCache[$targetMethod->class] ??= new \ReflectionMethod( - $targetMethod->class, - $targetMethod->name + $targetMethodReflection = self::$reflectionsCache[$targetMethod->class] ??= new \ReflectionClass( + $targetMethod->class ); /** @var Group[] $groupAttributes */ $groupAttributes = $targetMethodReflection->getAttributes( @@ -48,12 +48,10 @@ public function getRoutes(): array || is_a($attribute, Group::class, true); $targetClasses = Attributes::filterTargetClasses($groupPredicate); foreach ($targetClasses as $targetClass) { - if (isset($groupRoutes[$targetClass->name])) { - /** @var Group $group */ - $group = $targetClass->attribute; + $group = $targetClass->attribute; + if ($group instanceof Group && isset($groupRoutes[$targetClass->name])) { $routes[] = $group->routes(...$groupRoutes[$targetClass->name]); - } else { - /** @var Route $group */ + } elseif ($group instanceof Route) { $routes[] = $group->action($targetClass->name); } } From 7a0e222d45ba4a8bc54b9f673c70c29400fbcc5b Mon Sep 17 00:00:00 2001 From: Rustam Date: Sun, 15 Oct 2023 06:45:33 +0500 Subject: [PATCH 17/63] Allow attributes to use in classes --- src/Attribute/Delete.php | 2 +- src/Attribute/Get.php | 2 +- src/Attribute/Head.php | 2 +- src/Attribute/Options.php | 2 +- src/Attribute/Patch.php | 2 +- src/Attribute/Post.php | 2 +- src/Attribute/Put.php | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Attribute/Delete.php b/src/Attribute/Delete.php index 6ed574cd..cb464486 100644 --- a/src/Attribute/Delete.php +++ b/src/Attribute/Delete.php @@ -8,7 +8,7 @@ use Yiisoft\Http\Method; use Yiisoft\Router\Route; -#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] +#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] final class Delete extends Route { /** diff --git a/src/Attribute/Get.php b/src/Attribute/Get.php index 007a6567..0a3c5552 100644 --- a/src/Attribute/Get.php +++ b/src/Attribute/Get.php @@ -8,7 +8,7 @@ use Yiisoft\Http\Method; use Yiisoft\Router\Route; -#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] +#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] final class Get extends Route { /** diff --git a/src/Attribute/Head.php b/src/Attribute/Head.php index fb5f6159..eb5f44c8 100644 --- a/src/Attribute/Head.php +++ b/src/Attribute/Head.php @@ -8,7 +8,7 @@ use Yiisoft\Http\Method; use Yiisoft\Router\Route; -#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] +#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] final class Head extends Route { /** diff --git a/src/Attribute/Options.php b/src/Attribute/Options.php index 9b35213f..206f2f3b 100644 --- a/src/Attribute/Options.php +++ b/src/Attribute/Options.php @@ -8,7 +8,7 @@ use Yiisoft\Http\Method; use Yiisoft\Router\Route; -#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] +#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] final class Options extends Route { /** diff --git a/src/Attribute/Patch.php b/src/Attribute/Patch.php index c16cbee0..dbd6c667 100644 --- a/src/Attribute/Patch.php +++ b/src/Attribute/Patch.php @@ -8,7 +8,7 @@ use Yiisoft\Http\Method; use Yiisoft\Router\Route; -#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] +#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] final class Patch extends Route { /** diff --git a/src/Attribute/Post.php b/src/Attribute/Post.php index 6c3dfe88..7cbb1774 100644 --- a/src/Attribute/Post.php +++ b/src/Attribute/Post.php @@ -8,7 +8,7 @@ use Yiisoft\Http\Method; use Yiisoft\Router\Route; -#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] +#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] final class Post extends Route { /** diff --git a/src/Attribute/Put.php b/src/Attribute/Put.php index 1f4bde5a..87d15cf6 100644 --- a/src/Attribute/Put.php +++ b/src/Attribute/Put.php @@ -8,7 +8,7 @@ use Yiisoft\Http\Method; use Yiisoft\Router\Route; -#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] +#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] final class Put extends Route { /** From 2c38b063d240455286ab922f8c8c5abb39fcd298 Mon Sep 17 00:00:00 2001 From: Rustam Date: Sun, 15 Oct 2023 06:59:26 +0500 Subject: [PATCH 18/63] Fix psalm annotation --- src/Provider/AttributeRoutesProvider.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Provider/AttributeRoutesProvider.php b/src/Provider/AttributeRoutesProvider.php index 829845da..d9ee9ba9 100644 --- a/src/Provider/AttributeRoutesProvider.php +++ b/src/Provider/AttributeRoutesProvider.php @@ -17,7 +17,7 @@ final class AttributeRoutesProvider implements RoutesProviderInterface { /** - * @var array + * @var array */ private static array $reflectionsCache = []; From 6a2bd4c00428ba80546d4d1af14b8a51ea3fdc20 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Sun, 15 Oct 2023 17:04:19 +0300 Subject: [PATCH 19/63] Add `RouteAttributeInterface` (#221) --- src/Attribute/Delete.php | 14 +++++-- src/Attribute/Get.php | 14 +++++-- src/Attribute/Head.php | 14 +++++-- src/Attribute/Options.php | 14 +++++-- src/Attribute/Patch.php | 14 +++++-- src/Attribute/Post.php | 14 +++++-- src/Attribute/Put.php | 14 +++++-- src/Attribute/Route.php | 49 +++++++++++++++++++++++ src/Attribute/RouteAttributeInterface.php | 12 ++++++ src/Provider/AttributeRoutesProvider.php | 18 +++++---- src/Route.php | 4 +- tests/Attribute/DeleteTest.php | 10 +++-- tests/Attribute/GetTest.php | 10 +++-- tests/Attribute/HeadTest.php | 12 ++++-- tests/Attribute/OptionsTest.php | 10 +++-- tests/Attribute/PatchTest.php | 10 +++-- tests/Attribute/PostTest.php | 10 +++-- tests/Attribute/PutTest.php | 10 +++-- 18 files changed, 199 insertions(+), 54 deletions(-) create mode 100644 src/Attribute/Route.php create mode 100644 src/Attribute/RouteAttributeInterface.php diff --git a/src/Attribute/Delete.php b/src/Attribute/Delete.php index cb464486..f05af35e 100644 --- a/src/Attribute/Delete.php +++ b/src/Attribute/Delete.php @@ -5,14 +5,17 @@ namespace Yiisoft\Router\Attribute; use Attribute; +use Stringable; use Yiisoft\Http\Method; use Yiisoft\Router\Route; #[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] -final class Delete extends Route +final class Delete implements RouteAttributeInterface { + private Route $route; + /** - * @param array $defaults Parameter default values indexed by parameter names. + * @param array $defaults Parameter default values indexed by parameter names. * @param bool $override Marks route as override. When added it will replace existing route with the same name. * @param array $disabledMiddlewares Excludes middleware from being invoked when action is handled. * It is useful to avoid invoking one of the parent group middleware for @@ -27,7 +30,7 @@ public function __construct( bool $override = false, array $disabledMiddlewares = [] ) { - parent::__construct( + $this->route = new Route( methods: [Method::DELETE], pattern: $pattern, name: $name, @@ -38,4 +41,9 @@ public function __construct( disabledMiddlewares: $disabledMiddlewares ); } + + public function getRoute(): Route + { + return $this->route; + } } diff --git a/src/Attribute/Get.php b/src/Attribute/Get.php index 0a3c5552..82f3ead3 100644 --- a/src/Attribute/Get.php +++ b/src/Attribute/Get.php @@ -5,14 +5,17 @@ namespace Yiisoft\Router\Attribute; use Attribute; +use Stringable; use Yiisoft\Http\Method; use Yiisoft\Router\Route; #[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] -final class Get extends Route +final class Get implements RouteAttributeInterface { + private Route $route; + /** - * @param array $defaults Parameter default values indexed by parameter names. + * @param array $defaults Parameter default values indexed by parameter names. * @param bool $override Marks route as override. When added it will replace existing route with the same name. * @param array $disabledMiddlewares Excludes middleware from being invoked when action is handled. * It is useful to avoid invoking one of the parent group middleware for @@ -27,7 +30,7 @@ public function __construct( bool $override = false, array $disabledMiddlewares = [] ) { - parent::__construct( + $this->route = new Route( methods: [Method::GET], pattern: $pattern, name: $name, @@ -38,4 +41,9 @@ public function __construct( disabledMiddlewares: $disabledMiddlewares ); } + + public function getRoute(): Route + { + return $this->route; + } } diff --git a/src/Attribute/Head.php b/src/Attribute/Head.php index eb5f44c8..9ec6fdfd 100644 --- a/src/Attribute/Head.php +++ b/src/Attribute/Head.php @@ -5,14 +5,17 @@ namespace Yiisoft\Router\Attribute; use Attribute; +use Stringable; use Yiisoft\Http\Method; use Yiisoft\Router\Route; #[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] -final class Head extends Route +final class Head implements RouteAttributeInterface { + private Route $route; + /** - * @param array $defaults Parameter default values indexed by parameter names. + * @param array $defaults Parameter default values indexed by parameter names. * @param bool $override Marks route as override. When added it will replace existing route with the same name. * @param array $disabledMiddlewares Excludes middleware from being invoked when action is handled. * It is useful to avoid invoking one of the parent group middleware for @@ -27,7 +30,7 @@ public function __construct( bool $override = false, array $disabledMiddlewares = [] ) { - parent::__construct( + $this->route = new Route( methods: [Method::HEAD], pattern: $pattern, name: $name, @@ -38,4 +41,9 @@ public function __construct( disabledMiddlewares: $disabledMiddlewares ); } + + public function getRoute(): Route + { + return $this->route; + } } diff --git a/src/Attribute/Options.php b/src/Attribute/Options.php index 206f2f3b..3bd889ac 100644 --- a/src/Attribute/Options.php +++ b/src/Attribute/Options.php @@ -5,14 +5,17 @@ namespace Yiisoft\Router\Attribute; use Attribute; +use Stringable; use Yiisoft\Http\Method; use Yiisoft\Router\Route; #[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] -final class Options extends Route +final class Options implements RouteAttributeInterface { + private Route $route; + /** - * @param array $defaults Parameter default values indexed by parameter names. + * @param array $defaults Parameter default values indexed by parameter names. * @param bool $override Marks route as override. When added it will replace existing route with the same name. * @param array $disabledMiddlewares Excludes middleware from being invoked when action is handled. * It is useful to avoid invoking one of the parent group middleware for @@ -27,7 +30,7 @@ public function __construct( bool $override = false, array $disabledMiddlewares = [] ) { - parent::__construct( + $this->route = new Route( methods: [Method::OPTIONS], pattern: $pattern, name: $name, @@ -38,4 +41,9 @@ public function __construct( disabledMiddlewares: $disabledMiddlewares ); } + + public function getRoute(): Route + { + return $this->route; + } } diff --git a/src/Attribute/Patch.php b/src/Attribute/Patch.php index dbd6c667..5095c744 100644 --- a/src/Attribute/Patch.php +++ b/src/Attribute/Patch.php @@ -5,14 +5,17 @@ namespace Yiisoft\Router\Attribute; use Attribute; +use Stringable; use Yiisoft\Http\Method; use Yiisoft\Router\Route; #[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] -final class Patch extends Route +final class Patch implements RouteAttributeInterface { + private Route $route; + /** - * @param array $defaults Parameter default values indexed by parameter names. + * @param array $defaults Parameter default values indexed by parameter names. * @param bool $override Marks route as override. When added it will replace existing route with the same name. * @param array $disabledMiddlewares Excludes middleware from being invoked when action is handled. * It is useful to avoid invoking one of the parent group middleware for @@ -27,7 +30,7 @@ public function __construct( bool $override = false, array $disabledMiddlewares = [] ) { - parent::__construct( + $this->route = new Route( methods: [Method::PATCH], pattern: $pattern, name: $name, @@ -38,4 +41,9 @@ public function __construct( disabledMiddlewares: $disabledMiddlewares ); } + + public function getRoute(): Route + { + return $this->route; + } } diff --git a/src/Attribute/Post.php b/src/Attribute/Post.php index 7cbb1774..df798195 100644 --- a/src/Attribute/Post.php +++ b/src/Attribute/Post.php @@ -5,14 +5,17 @@ namespace Yiisoft\Router\Attribute; use Attribute; +use Stringable; use Yiisoft\Http\Method; use Yiisoft\Router\Route; #[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] -final class Post extends Route +final class Post implements RouteAttributeInterface { + private Route $route; + /** - * @param array $defaults Parameter default values indexed by parameter names. + * @param array $defaults Parameter default values indexed by parameter names. * @param bool $override Marks route as override. When added it will replace existing route with the same name. * @param array $disabledMiddlewares Excludes middleware from being invoked when action is handled. * It is useful to avoid invoking one of the parent group middleware for @@ -27,7 +30,7 @@ public function __construct( bool $override = false, array $disabledMiddlewares = [] ) { - parent::__construct( + $this->route = new Route( methods: [Method::POST], pattern: $pattern, name: $name, @@ -38,4 +41,9 @@ public function __construct( disabledMiddlewares: $disabledMiddlewares ); } + + public function getRoute(): Route + { + return $this->route; + } } diff --git a/src/Attribute/Put.php b/src/Attribute/Put.php index 87d15cf6..83dab1d2 100644 --- a/src/Attribute/Put.php +++ b/src/Attribute/Put.php @@ -5,14 +5,17 @@ namespace Yiisoft\Router\Attribute; use Attribute; +use Stringable; use Yiisoft\Http\Method; use Yiisoft\Router\Route; #[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] -final class Put extends Route +final class Put implements RouteAttributeInterface { + private Route $route; + /** - * @param array $defaults Parameter default values indexed by parameter names. + * @param array $defaults Parameter default values indexed by parameter names. * @param bool $override Marks route as override. When added it will replace existing route with the same name. * @param array $disabledMiddlewares Excludes middleware from being invoked when action is handled. * It is useful to avoid invoking one of the parent group middleware for @@ -27,7 +30,7 @@ public function __construct( bool $override = false, array $disabledMiddlewares = [] ) { - parent::__construct( + $this->route = new Route( methods: [Method::PUT], pattern: $pattern, name: $name, @@ -38,4 +41,9 @@ public function __construct( disabledMiddlewares: $disabledMiddlewares ); } + + public function getRoute(): Route + { + return $this->route; + } } diff --git a/src/Attribute/Route.php b/src/Attribute/Route.php new file mode 100644 index 00000000..76409248 --- /dev/null +++ b/src/Attribute/Route.php @@ -0,0 +1,49 @@ + $defaults Parameter default values indexed by parameter names. + * @param bool $override Marks route as override. When added it will replace existing route with the same name. + * @param array $disabledMiddlewares Excludes middleware from being invoked when action is handled. + * It is useful to avoid invoking one of the parent group middleware for + * a certain route. + */ + public function __construct( + array $methods, + string $pattern, + ?string $name = null, + array $middlewares = [], + array $defaults = [], + array $hosts = [], + bool $override = false, + array $disabledMiddlewares = [] + ) { + $this->route = new RouteObject( + methods: $methods, + pattern: $pattern, + name: $name, + middlewares: $middlewares, + defaults: $defaults, + hosts: $hosts, + override: $override, + disabledMiddlewares: $disabledMiddlewares + ); + } + + public function getRoute(): RouteObject + { + return $this->route; + } +} diff --git a/src/Attribute/RouteAttributeInterface.php b/src/Attribute/RouteAttributeInterface.php new file mode 100644 index 00000000..cecfa630 --- /dev/null +++ b/src/Attribute/RouteAttributeInterface.php @@ -0,0 +1,12 @@ + + * @var array */ private static array $reflectionsCache = []; @@ -25,18 +26,19 @@ public function getRoutes(): array { $routes = []; $groupRoutes = []; - $routePredicate = Attributes::predicateForAttributeInstanceOf(Route::class); + $routePredicate = Attributes::predicateForAttributeInstanceOf(RouteAttributeInterface::class); $targetMethods = Attributes::filterTargetMethods($routePredicate); foreach ($targetMethods as $targetMethod) { - /** @var Route $route */ - $route = $targetMethod->attribute; - $targetMethodReflection = self::$reflectionsCache[$targetMethod->class] ??= new \ReflectionClass( + /** @var RouteAttributeInterface $routeAttribute */ + $routeAttribute = $targetMethod->attribute; + $route = $routeAttribute->getRoute(); + $targetMethodReflection = self::$reflectionsCache[$targetMethod->class] ??= new ReflectionClass( $targetMethod->class ); /** @var Group[] $groupAttributes */ $groupAttributes = $targetMethodReflection->getAttributes( Group::class, - \ReflectionAttribute::IS_INSTANCEOF + ReflectionAttribute::IS_INSTANCEOF ); if (!empty($groupAttributes)) { $groupRoutes[$targetMethod->class][] = $route->action([$targetMethod->class, $targetMethod->name]); diff --git a/src/Route.php b/src/Route.php index 2ef94438..e58fc445 100644 --- a/src/Route.php +++ b/src/Route.php @@ -4,7 +4,6 @@ namespace Yiisoft\Router; -use Attribute; use InvalidArgumentException; use RuntimeException; use Stringable; @@ -15,8 +14,7 @@ /** * Route defines a mapping from URL to callback / name and vice versa. */ -#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] -class Route implements Stringable +final class Route implements Stringable { private bool $actionAdded = false; /** diff --git a/tests/Attribute/DeleteTest.php b/tests/Attribute/DeleteTest.php index 5eec0b13..66acb374 100644 --- a/tests/Attribute/DeleteTest.php +++ b/tests/Attribute/DeleteTest.php @@ -12,15 +12,19 @@ class DeleteTest extends TestCase { public function testRoute(): void { - $route = new Delete('/post'); + $attribute = new Delete('/post'); + + $route = $attribute->getRoute(); $this->assertSame('/post', $route->getData('pattern')); - $this->assertEquals([Method::DELETE], $route->getData('methods')); + $this->assertSame([Method::DELETE], $route->getData('methods')); } public function testOverride(): void { - $route = new Delete('/', override: true); + $attribute = new Delete('/', override: true); + + $route = $attribute->getRoute(); $this->assertTrue($route->getData('override')); } diff --git a/tests/Attribute/GetTest.php b/tests/Attribute/GetTest.php index 659daff0..855f1568 100644 --- a/tests/Attribute/GetTest.php +++ b/tests/Attribute/GetTest.php @@ -12,15 +12,19 @@ class GetTest extends TestCase { public function testRoute(): void { - $route = new Get('/post'); + $attribute = new Get('/post'); + + $route = $attribute->getRoute(); $this->assertSame('/post', $route->getData('pattern')); - $this->assertEquals([Method::GET], $route->getData('methods')); + $this->assertSame([Method::GET], $route->getData('methods')); } public function testOverride(): void { - $route = new Get('/', override: true); + $attribute = new Get('/', override: true); + + $route = $attribute->getRoute(); $this->assertTrue($route->getData('override')); } diff --git a/tests/Attribute/HeadTest.php b/tests/Attribute/HeadTest.php index 9943acc2..6e092f5f 100644 --- a/tests/Attribute/HeadTest.php +++ b/tests/Attribute/HeadTest.php @@ -8,19 +8,23 @@ use Yiisoft\Http\Method; use Yiisoft\Router\Attribute\Head; -class HeadTest extends TestCase +final class HeadTest extends TestCase { public function testRoute(): void { - $route = new Head('/post'); + $attribute = new Head('/post'); + + $route = $attribute->getRoute(); $this->assertSame('/post', $route->getData('pattern')); - $this->assertEquals([Method::HEAD], $route->getData('methods')); + $this->assertSame([Method::HEAD], $route->getData('methods')); } public function testOverride(): void { - $route = new Head('/', override: true); + $attribute = new Head('/', override: true); + + $route = $attribute->getRoute(); $this->assertTrue($route->getData('override')); } diff --git a/tests/Attribute/OptionsTest.php b/tests/Attribute/OptionsTest.php index fa44dbfe..fa38d0e8 100644 --- a/tests/Attribute/OptionsTest.php +++ b/tests/Attribute/OptionsTest.php @@ -12,15 +12,19 @@ class OptionsTest extends TestCase { public function testRoute(): void { - $route = new Options('/post'); + $attribute = new Options('/post'); + + $route = $attribute->getRoute(); $this->assertSame('/post', $route->getData('pattern')); - $this->assertEquals([Method::OPTIONS], $route->getData('methods')); + $this->assertSame([Method::OPTIONS], $route->getData('methods')); } public function testOverride(): void { - $route = new Options('/', override: true); + $attribute = new Options('/', override: true); + + $route = $attribute->getRoute(); $this->assertTrue($route->getData('override')); } diff --git a/tests/Attribute/PatchTest.php b/tests/Attribute/PatchTest.php index fca22ea1..37121a26 100644 --- a/tests/Attribute/PatchTest.php +++ b/tests/Attribute/PatchTest.php @@ -12,15 +12,19 @@ class PatchTest extends TestCase { public function testRoute(): void { - $route = new Patch('/post'); + $attribute = new Patch('/post'); + + $route = $attribute->getRoute(); $this->assertSame('/post', $route->getData('pattern')); - $this->assertEquals([Method::PATCH], $route->getData('methods')); + $this->assertSame([Method::PATCH], $route->getData('methods')); } public function testOverride(): void { - $route = new Patch('/', override: true); + $attribute = new Patch('/', override: true); + + $route = $attribute->getRoute(); $this->assertTrue($route->getData('override')); } diff --git a/tests/Attribute/PostTest.php b/tests/Attribute/PostTest.php index e4011e0f..87f3c9bb 100644 --- a/tests/Attribute/PostTest.php +++ b/tests/Attribute/PostTest.php @@ -12,15 +12,19 @@ class PostTest extends TestCase { public function testRoute(): void { - $route = new Post('/post'); + $attribute = new Post('/post'); + + $route = $attribute->getRoute(); $this->assertSame('/post', $route->getData('pattern')); - $this->assertEquals([Method::POST], $route->getData('methods')); + $this->assertSame([Method::POST], $route->getData('methods')); } public function testOverride(): void { - $route = new Post('/', override: true); + $attribute = new Post('/', override: true); + + $route = $attribute->getRoute(); $this->assertTrue($route->getData('override')); } diff --git a/tests/Attribute/PutTest.php b/tests/Attribute/PutTest.php index 8d2c43f7..4c6ebd14 100644 --- a/tests/Attribute/PutTest.php +++ b/tests/Attribute/PutTest.php @@ -12,15 +12,19 @@ class PutTest extends TestCase { public function testRoute(): void { - $route = new Put('/post'); + $attribute = new Put('/post'); + + $route = $attribute->getRoute(); $this->assertSame('/post', $route->getData('pattern')); - $this->assertEquals([Method::PUT], $route->getData('methods')); + $this->assertSame([Method::PUT], $route->getData('methods')); } public function testOverride(): void { - $route = new Put('/', override: true); + $attribute = new Put('/', override: true); + + $route = $attribute->getRoute(); $this->assertTrue($route->getData('override')); } From ab467d8aa09298204c66ff55b18af8784b0b0277 Mon Sep 17 00:00:00 2001 From: Rustam Date: Mon, 16 Oct 2023 11:04:41 +0500 Subject: [PATCH 20/63] Adjust naming --- .phpstorm.meta.php/Group.php | 2 +- .phpstorm.meta.php/Route.php | 2 +- src/Attribute/Delete.php | 4 +- src/Attribute/Get.php | 4 +- src/Attribute/Head.php | 4 +- src/Attribute/Options.php | 4 +- src/Attribute/Patch.php | 4 +- src/Attribute/Post.php | 4 +- src/Attribute/Put.php | 4 +- src/Attribute/Route.php | 4 +- src/Group.php | 56 +++++++++++++------------- src/Middleware/Router.php | 2 +- src/Route.php | 74 +++++++++++++++++------------------ src/RouteCollection.php | 2 +- tests/GroupTest.php | 34 ++++++++-------- tests/RouteCollectionTest.php | 8 ++-- tests/RouteTest.php | 22 +++++------ 17 files changed, 117 insertions(+), 117 deletions(-) diff --git a/.phpstorm.meta.php/Group.php b/.phpstorm.meta.php/Group.php index 0facbb6a..ead6379a 100644 --- a/.phpstorm.meta.php/Group.php +++ b/.phpstorm.meta.php/Group.php @@ -12,7 +12,7 @@ 'hosts', 'corsMiddleware', 'routes', - 'middlewares', + 'middlewareDefinitions', 'hasCorsMiddleware' ); } diff --git a/.phpstorm.meta.php/Route.php b/.phpstorm.meta.php/Route.php index 1b0dad0b..2ddb23e5 100644 --- a/.phpstorm.meta.php/Route.php +++ b/.phpstorm.meta.php/Route.php @@ -14,6 +14,6 @@ 'override', 'defaults', 'hasMiddlewares', - 'builtMiddlewares' + 'builtMiddlewareDefinitions' ); } diff --git a/src/Attribute/Delete.php b/src/Attribute/Delete.php index f05af35e..b06b29bb 100644 --- a/src/Attribute/Delete.php +++ b/src/Attribute/Delete.php @@ -34,11 +34,11 @@ public function __construct( methods: [Method::DELETE], pattern: $pattern, name: $name, - middlewares: $middlewares, + middlewareDefinitions: $middlewares, defaults: $defaults, hosts: $hosts, override: $override, - disabledMiddlewares: $disabledMiddlewares + disabledMiddlewareDefinitions: $disabledMiddlewares ); } diff --git a/src/Attribute/Get.php b/src/Attribute/Get.php index 82f3ead3..032fe580 100644 --- a/src/Attribute/Get.php +++ b/src/Attribute/Get.php @@ -34,11 +34,11 @@ public function __construct( methods: [Method::GET], pattern: $pattern, name: $name, - middlewares: $middlewares, + middlewareDefinitions: $middlewares, defaults: $defaults, hosts: $hosts, override: $override, - disabledMiddlewares: $disabledMiddlewares + disabledMiddlewareDefinitions: $disabledMiddlewares ); } diff --git a/src/Attribute/Head.php b/src/Attribute/Head.php index 9ec6fdfd..6ef954a4 100644 --- a/src/Attribute/Head.php +++ b/src/Attribute/Head.php @@ -34,11 +34,11 @@ public function __construct( methods: [Method::HEAD], pattern: $pattern, name: $name, - middlewares: $middlewares, + middlewareDefinitions: $middlewares, defaults: $defaults, hosts: $hosts, override: $override, - disabledMiddlewares: $disabledMiddlewares + disabledMiddlewareDefinitions: $disabledMiddlewares ); } diff --git a/src/Attribute/Options.php b/src/Attribute/Options.php index 3bd889ac..ed823fa8 100644 --- a/src/Attribute/Options.php +++ b/src/Attribute/Options.php @@ -34,11 +34,11 @@ public function __construct( methods: [Method::OPTIONS], pattern: $pattern, name: $name, - middlewares: $middlewares, + middlewareDefinitions: $middlewares, defaults: $defaults, hosts: $hosts, override: $override, - disabledMiddlewares: $disabledMiddlewares + disabledMiddlewareDefinitions: $disabledMiddlewares ); } diff --git a/src/Attribute/Patch.php b/src/Attribute/Patch.php index 5095c744..377130dd 100644 --- a/src/Attribute/Patch.php +++ b/src/Attribute/Patch.php @@ -34,11 +34,11 @@ public function __construct( methods: [Method::PATCH], pattern: $pattern, name: $name, - middlewares: $middlewares, + middlewareDefinitions: $middlewares, defaults: $defaults, hosts: $hosts, override: $override, - disabledMiddlewares: $disabledMiddlewares + disabledMiddlewareDefinitions: $disabledMiddlewares ); } diff --git a/src/Attribute/Post.php b/src/Attribute/Post.php index df798195..9e58cdf8 100644 --- a/src/Attribute/Post.php +++ b/src/Attribute/Post.php @@ -34,11 +34,11 @@ public function __construct( methods: [Method::POST], pattern: $pattern, name: $name, - middlewares: $middlewares, + middlewareDefinitions: $middlewares, defaults: $defaults, hosts: $hosts, override: $override, - disabledMiddlewares: $disabledMiddlewares + disabledMiddlewareDefinitions: $disabledMiddlewares ); } diff --git a/src/Attribute/Put.php b/src/Attribute/Put.php index 83dab1d2..e5404dfb 100644 --- a/src/Attribute/Put.php +++ b/src/Attribute/Put.php @@ -34,11 +34,11 @@ public function __construct( methods: [Method::PUT], pattern: $pattern, name: $name, - middlewares: $middlewares, + middlewareDefinitions: $middlewares, defaults: $defaults, hosts: $hosts, override: $override, - disabledMiddlewares: $disabledMiddlewares + disabledMiddlewareDefinitions: $disabledMiddlewares ); } diff --git a/src/Attribute/Route.php b/src/Attribute/Route.php index 76409248..29d668cf 100644 --- a/src/Attribute/Route.php +++ b/src/Attribute/Route.php @@ -34,11 +34,11 @@ public function __construct( methods: $methods, pattern: $pattern, name: $name, - middlewares: $middlewares, + middlewareDefinitions: $middlewares, defaults: $defaults, hosts: $hosts, override: $override, - disabledMiddlewares: $disabledMiddlewares + disabledMiddlewareDefinitions: $disabledMiddlewares ); } diff --git a/src/Group.php b/src/Group.php index baddaf3c..f0b6412d 100644 --- a/src/Group.php +++ b/src/Group.php @@ -19,7 +19,7 @@ final class Group private array $routes = []; private bool $routesAdded = false; private bool $middlewareAdded = false; - private array $builtMiddlewares = []; + private array $builtMiddlewareDefinitions = []; /** * @var array|callable|string|null Middleware definition for CORS requests. */ @@ -31,24 +31,24 @@ final class Group /** * @var array[]|callable[]|string[] */ - private array $middlewares = []; + private array $middlewareDefinitions = []; /** - * @param array $disabledMiddlewares Excludes middleware from being invoked when action is handled. + * @param array $disabledMiddlewareDefinitions Excludes middleware from being invoked when action is handled. * It is useful to avoid invoking one of the parent group middleware for * a certain route. */ public function __construct( private ?string $prefix = null, - array $middlewares = [], + array $middlewareDefinitions = [], array $hosts = [], private ?string $namePrefix = null, - private array $disabledMiddlewares = [], + private array $disabledMiddlewareDefinitions = [], array|callable|string|null $corsMiddleware = null ) { - $this->assertMiddlewares($middlewares); + $this->assertMiddlewares($middlewareDefinitions); $this->assertHosts($hosts); - $this->middlewares = $middlewares; + $this->middlewareDefinitions = $middlewareDefinitions; $this->hosts = $hosts; $this->corsMiddleware = $corsMiddleware; } @@ -100,10 +100,10 @@ public function middleware(array|callable|string ...$middlewareDefinition): self } $new = clone $this; array_push( - $new->middlewares, + $new->middlewareDefinitions, ...array_values($middlewareDefinition) ); - $new->builtMiddlewares = []; + $new->builtMiddlewareDefinitions = []; return $new; } @@ -115,11 +115,11 @@ public function prependMiddleware(array|callable|string ...$middlewareDefinition { $new = clone $this; array_unshift( - $new->middlewares, + $new->middlewareDefinitions, ...array_values($middlewareDefinition) ); $new->middlewareAdded = true; - $new->builtMiddlewares = []; + $new->builtMiddlewareDefinitions = []; return $new; } @@ -159,10 +159,10 @@ public function disableMiddleware(mixed ...$middlewareDefinition): self { $new = clone $this; array_push( - $new->disabledMiddlewares, + $new->disabledMiddlewareDefinitions, ...array_values($middlewareDefinition), ); - $new->builtMiddlewares = []; + $new->builtMiddlewareDefinitions = []; return $new; } @@ -176,7 +176,7 @@ public function disableMiddleware(mixed ...$middlewareDefinition): self * (T is 'routes' ? Group[]|Route[] : * (T is 'hosts' ? array : * (T is 'hasCorsMiddleware' ? bool : - * (T is 'middlewares' ? list : + * (T is 'middlewareDefinitions' ? list : * (T is 'corsMiddleware' ? array|callable|string|null : mixed) * ) * ) @@ -194,27 +194,27 @@ public function getData(string $key): mixed 'corsMiddleware' => $this->corsMiddleware, 'routes' => $this->routes, 'hasCorsMiddleware' => $this->corsMiddleware !== null, - 'middlewares' => $this->getBuiltMiddlewares(), + 'middlewareDefinitions' => $this->getBuiltMiddlewares(), default => throw new InvalidArgumentException('Unknown data key: ' . $key), }; } private function getBuiltMiddlewares(): array { - if (!empty($this->builtMiddlewares)) { - return $this->builtMiddlewares; + if (!empty($this->builtMiddlewareDefinitions)) { + return $this->builtMiddlewareDefinitions; } - $builtMiddlewares = $this->middlewares; + $builtMiddlewareDefinitions = $this->middlewareDefinitions; /** @var mixed $definition */ - foreach ($builtMiddlewares as $index => $definition) { - if (in_array($definition, $this->disabledMiddlewares, true)) { - unset($builtMiddlewares[$index]); + foreach ($builtMiddlewareDefinitions as $index => $definition) { + if (in_array($definition, $this->disabledMiddlewareDefinitions, true)) { + unset($builtMiddlewareDefinitions[$index]); } } - return $this->builtMiddlewares = array_values($builtMiddlewares); + return $this->builtMiddlewareDefinitions = array_values($builtMiddlewareDefinitions); } /** @@ -230,18 +230,18 @@ private function assertHosts(array $hosts): void } /** - * @psalm-assert array $middlewares + * @psalm-assert array $middlewareDefinitions */ - private function assertMiddlewares(array $middlewares): void + private function assertMiddlewares(array $middlewareDefinitions): void { - /** @var mixed $middleware */ - foreach ($middlewares as $middleware) { - if (is_string($middleware) || is_callable($middleware) || is_array($middleware)) { + /** @var mixed $middlewareDefinition */ + foreach ($middlewareDefinitions as $middlewareDefinition) { + if (is_string($middlewareDefinition) || is_callable($middlewareDefinition) || is_array($middlewareDefinition)) { continue; } throw new \InvalidArgumentException( - 'Invalid $middlewares provided, list of string or array or callable expected.' + 'Invalid $middlewareDefinitions provided, list of string or array or callable expected.' ); } } diff --git a/src/Middleware/Router.php b/src/Middleware/Router.php index e882151b..a1afc59a 100644 --- a/src/Middleware/Router.php +++ b/src/Middleware/Router.php @@ -55,7 +55,7 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface $this->currentRoute->setRouteWithArguments($result->route(), $result->arguments()); return $this->dispatcher - ->withMiddlewares($result->route()->getData('builtMiddlewares')) + ->withMiddlewares($result->route()->getData('builtMiddlewareDefinitions')) ->dispatch($request, $handler); } } diff --git a/src/Route.php b/src/Route.php index e58fc445..40e491cd 100644 --- a/src/Route.php +++ b/src/Route.php @@ -20,11 +20,11 @@ final class Route implements Stringable /** * @var array[]|callable[]|string[] */ - private array $builtMiddlewares = []; + private array $builtMiddlewareDefinitions = []; /** * @var array[]|callable[]|string[] */ - private array $middlewares = []; + private array $middlewareDefinitions = []; /** * @var string[] */ @@ -43,7 +43,7 @@ final class Route implements Stringable * should be invoked last for a matched route. * @param array $defaults Parameter default values indexed by parameter names. * @param bool $override Marks route as override. When added it will replace existing route with the same name. - * @param array $disabledMiddlewares Excludes middleware from being invoked when action is handled. + * @param array $disabledMiddlewareDefinitions Excludes middleware from being invoked when action is handled. * It is useful to avoid invoking one of the parent group middleware for * a certain route. */ @@ -52,21 +52,21 @@ public function __construct( private string $pattern, private ?string $name = null, array|callable|string $action = null, - array $middlewares = [], + array $middlewareDefinitions = [], array $defaults = [], array $hosts = [], private bool $override = false, - private array $disabledMiddlewares = [], + private array $disabledMiddlewareDefinitions = [], ) { $this->assertListOfStrings($methods, 'methods'); - $this->assertMiddlewares($middlewares); + $this->assertMiddlewares($middlewareDefinitions); $this->assertListOfStrings($hosts, 'hosts'); $this->methods = $methods; - $this->middlewares = $middlewares; + $this->middlewareDefinitions = $middlewareDefinitions; $this->hosts = $hosts; $this->defaults = array_map('\strval', $defaults); if (!empty($action)) { - $this->middlewares[] = $action; + $this->middlewareDefinitions[] = $action; $this->actionAdded = true; } } @@ -185,10 +185,10 @@ public function middleware(array|callable|string ...$middlewareDefinition): self } $route = clone $this; array_push( - $route->middlewares, + $route->middlewareDefinitions, ...array_values($middlewareDefinition) ); - $route->builtMiddlewares = []; + $route->builtMiddlewareDefinitions = []; return $route; } @@ -203,10 +203,10 @@ public function prependMiddleware(array|callable|string ...$middlewareDefinition } $route = clone $this; array_unshift( - $route->middlewares, + $route->middlewareDefinitions, ...array_values($middlewareDefinition) ); - $route->builtMiddlewares = []; + $route->builtMiddlewareDefinitions = []; return $route; } @@ -216,9 +216,9 @@ public function prependMiddleware(array|callable|string ...$middlewareDefinition public function action(array|callable|string $middlewareDefinition): self { $route = clone $this; - $route->middlewares[] = $middlewareDefinition; + $route->middlewareDefinitions[] = $middlewareDefinition; $route->actionAdded = true; - $route->builtMiddlewares = []; + $route->builtMiddlewareDefinitions = []; return $route; } @@ -231,10 +231,10 @@ public function disableMiddleware(mixed ...$middlewareDefinition): self { $route = clone $this; array_push( - $route->disabledMiddlewares, + $route->disabledMiddlewareDefinitions, ...array_values($middlewareDefinition) ); - $route->builtMiddlewares = []; + $route->builtMiddlewareDefinitions = []; return $route; } @@ -250,7 +250,7 @@ public function disableMiddleware(mixed ...$middlewareDefinition): self * (T is 'methods' ? array : * (T is 'defaults' ? array : * (T is ('override'|'hasMiddlewares') ? bool : - * (T is 'builtMiddlewares' ? array : mixed) + * (T is 'builtMiddlewareDefinitions' ? array : mixed) * ) * ) * ) @@ -269,8 +269,8 @@ public function getData(string $key): mixed 'methods' => $this->methods, 'defaults' => $this->defaults, 'override' => $this->override, - 'hasMiddlewares' => !empty($this->middlewares), - 'builtMiddlewares' => $this->getBuiltMiddlewares(), + 'hasMiddlewares' => !empty($this->middlewareDefinitions), + 'builtMiddlewareDefinitions' => $this->getBuiltMiddlewares(), default => throw new InvalidArgumentException('Unknown data key: ' . $key), }; } @@ -308,9 +308,9 @@ public function __debugInfo() 'defaults' => $this->defaults, 'override' => $this->override, 'actionAdded' => $this->actionAdded, - 'middlewares' => $this->middlewares, - 'builtMiddlewares' => $this->builtMiddlewares, - 'disabledMiddlewares' => $this->disabledMiddlewares, + 'middlewareDefinitions' => $this->middlewareDefinitions, + 'builtMiddlewareDefinitions' => $this->builtMiddlewareDefinitions, + 'disabledMiddlewareDefinitions' => $this->disabledMiddlewareDefinitions, ]; } @@ -319,21 +319,21 @@ public function __debugInfo() */ private function getBuiltMiddlewares(): array { - // Don't build middlewares if we did it earlier. + // Don't build middlewareDefinitions if we did it earlier. // This improves performance in event-loop applications. - if (!empty($this->builtMiddlewares)) { - return $this->builtMiddlewares; + if (!empty($this->builtMiddlewareDefinitions)) { + return $this->builtMiddlewareDefinitions; } - $builtMiddlewares = $this->middlewares; + $builtMiddlewareDefinitions = $this->middlewareDefinitions; - foreach ($builtMiddlewares as $index => $definition) { - if (in_array($definition, $this->disabledMiddlewares, true)) { - unset($builtMiddlewares[$index]); + foreach ($builtMiddlewareDefinitions as $index => $definition) { + if (in_array($definition, $this->disabledMiddlewareDefinitions, true)) { + unset($builtMiddlewareDefinitions[$index]); } } - return $this->builtMiddlewares = $builtMiddlewares; + return $this->builtMiddlewareDefinitions = $builtMiddlewareDefinitions; } /** @@ -349,22 +349,22 @@ private function assertListOfStrings(array $items, string $argument): void } /** - * @psalm-assert array $middlewares + * @psalm-assert array $middlewareDefinitions */ - private function assertMiddlewares(array $middlewares): void + private function assertMiddlewares(array $middlewareDefinitions): void { - /** @var mixed $middleware */ - foreach ($middlewares as $middleware) { - if (is_string($middleware)) { + /** @var mixed $middlewareDefinition */ + foreach ($middlewareDefinitions as $middlewareDefinition) { + if (is_string($middlewareDefinition)) { continue; } - if (is_callable($middleware) || is_array($middleware)) { + if (is_callable($middlewareDefinition) || is_array($middlewareDefinition)) { continue; } throw new \InvalidArgumentException( - 'Invalid $middlewares provided, list of string or array or callable expected.' + 'Invalid $middlewareDefinitions provided, list of string or array or callable expected.' ); } } diff --git a/src/RouteCollection.php b/src/RouteCollection.php index a753547f..fa3aa130 100644 --- a/src/RouteCollection.php +++ b/src/RouteCollection.php @@ -109,7 +109,7 @@ private function injectGroup(Group $group, array &$tree, string $prefix = '', st $hosts = []; foreach ($items as $item) { if (!$this->isStaticRoute($item)) { - $item = $item->prependMiddleware(...$group->getData('middlewares')); + $item = $item->prependMiddleware(...$group->getData('middlewareDefinitions')); } if (!empty($group->getData('hosts')) && empty($item->getData('hosts'))) { diff --git a/tests/GroupTest.php b/tests/GroupTest.php index 5057c332..3d1c75cf 100644 --- a/tests/GroupTest.php +++ b/tests/GroupTest.php @@ -36,15 +36,15 @@ public function testAddMiddleware(): void $group = $group ->middleware($middleware1) ->middleware($middleware2); - $this->assertCount(2, $group->getData('middlewares')); - $this->assertSame($middleware1, $group->getData('middlewares')[0]); - $this->assertSame($middleware2, $group->getData('middlewares')[1]); + $this->assertCount(2, $group->getData('middlewareDefinitions')); + $this->assertSame($middleware1, $group->getData('middlewareDefinitions')[0]); + $this->assertSame($middleware2, $group->getData('middlewareDefinitions')[1]); } public function testInvalidMiddlewares(): void { $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Invalid $middlewares provided, list of string or array or callable expected.'); + $this->expectExceptionMessage('Invalid $middlewareDefinitions provided, list of string or array or callable expected.'); $middleware = static fn () => new Response(); $group = new Group('/api', [$middleware, new \stdClass()]); @@ -57,8 +57,8 @@ public function testDisabledMiddlewareDefinitions(): void ->prependMiddleware(TestMiddleware1::class, TestMiddleware2::class) ->disableMiddleware(TestMiddleware1::class, TestMiddleware3::class); - $this->assertCount(1, $group->getData('middlewares')); - $this->assertSame(TestMiddleware2::class, $group->getData('middlewares')[0]); + $this->assertCount(1, $group->getData('middlewareDefinitions')); + $this->assertSame(TestMiddleware2::class, $group->getData('middlewareDefinitions')[0]); } public function testNamedArgumentsInMiddlewareMethods(): void @@ -68,8 +68,8 @@ public function testNamedArgumentsInMiddlewareMethods(): void ->prependMiddleware(middleware1: TestMiddleware1::class, middleware2: TestMiddleware2::class) ->disableMiddleware(middleware1: TestMiddleware1::class, middleware2: TestMiddleware3::class); - $this->assertCount(1, $group->getData('middlewares')); - $this->assertSame(TestMiddleware2::class, $group->getData('middlewares')[0]); + $this->assertCount(1, $group->getData('middlewareDefinitions')); + $this->assertSame(TestMiddleware2::class, $group->getData('middlewareDefinitions')[0]); } public function testRoutesAfterMiddleware(): void @@ -120,7 +120,7 @@ public function testAddNestedMiddleware(): void $routeCollection = new RouteCollection($collector); $route = $routeCollection->getRoute('request1'); $response = $this->getDispatcher() - ->withMiddlewares($route->getData('builtMiddlewares')) + ->withMiddlewares($route->getData('builtMiddlewareDefinitions')) ->dispatch($request, $this->getRequestHandler()); $this->assertSame(200, $response->getStatusCode()); $this->assertSame('middleware2', $response->getReasonPhrase()); @@ -155,7 +155,7 @@ public function testGroupMiddlewareFullStackCalled(): void $routeCollection = new RouteCollection($collector); $route = $routeCollection->getRoute('request1'); $response = $this->getDispatcher() - ->withMiddlewares($route->getData('builtMiddlewares')) + ->withMiddlewares($route->getData('builtMiddlewareDefinitions')) ->dispatch($request, $this->getRequestHandler()); $this->assertSame(200, $response->getStatusCode()); $this->assertSame('middleware2', $response->getReasonPhrase()); @@ -184,7 +184,7 @@ public function testGroupMiddlewareStackInterrupted(): void $routeCollection = new RouteCollection($collector); $route = $routeCollection->getRoute('request1'); $response = $this->getDispatcher() - ->withMiddlewares($route->getData('builtMiddlewares')) + ->withMiddlewares($route->getData('builtMiddlewareDefinitions')) ->dispatch($request, $this->getRequestHandler()); $this->assertSame(403, $response->getStatusCode()); } @@ -225,15 +225,15 @@ public function testAddGroup(): void /** @var Group $postGroup */ $postGroup = $api->getData('routes')[1]; $this->assertInstanceOf(Group::class, $postGroup); - $this->assertCount(2, $api->getData('middlewares')); - $this->assertSame($middleware1, $api->getData('middlewares')[0]); - $this->assertSame($middleware2, $api->getData('middlewares')[1]); + $this->assertCount(2, $api->getData('middlewareDefinitions')); + $this->assertSame($middleware1, $api->getData('middlewareDefinitions')[0]); + $this->assertSame($middleware2, $api->getData('middlewareDefinitions')[1]); $this->assertSame('/post', $postGroup->getData('prefix')); $this->assertCount(2, $postGroup->getData('routes')); $this->assertSame($listRoute, $postGroup->getData('routes')[0]); $this->assertSame($viewRoute, $postGroup->getData('routes')[1]); - $this->assertEmpty($postGroup->getData('middlewares')); + $this->assertEmpty($postGroup->getData('middlewareDefinitions')); } public function testHost(): void @@ -425,9 +425,9 @@ public function testBuiltMiddlewares(): void ->middleware(static fn () => new Response(200)) ->prependMiddleware(TestMiddleware1::class); - $builtMiddlewares = $group->getData('middlewares'); + $builtMiddlewareDefinitions = $group->getData('middlewareDefinitions'); - $this->assertSame($builtMiddlewares, $group->getData('middlewares')); + $this->assertSame($builtMiddlewareDefinitions, $group->getData('middlewareDefinitions')); } private function getRequestHandler(): RequestHandlerInterface diff --git a/tests/RouteCollectionTest.php b/tests/RouteCollectionTest.php index 86eb2171..a0d788f9 100644 --- a/tests/RouteCollectionTest.php +++ b/tests/RouteCollectionTest.php @@ -277,10 +277,10 @@ public function testCollectorMiddlewareFullstackCalled(): void $route2 = $routeCollection->getRoute('view'); $request = new ServerRequest('GET', '/'); $response1 = $this->getDispatcher() - ->withMiddlewares($route1->getData('builtMiddlewares')) + ->withMiddlewares($route1->getData('builtMiddlewareDefinitions')) ->dispatch($request, $this->getRequestHandler()); $response2 = $this->getDispatcher() - ->withMiddlewares($route2->getData('builtMiddlewares')) + ->withMiddlewares($route2->getData('builtMiddlewareDefinitions')) ->dispatch($request, $this->getRequestHandler()); $this->assertEquals('middleware1', $response1->getReasonPhrase()); @@ -328,7 +328,7 @@ public function testMiddlewaresOrder(bool $groupWrapped): void $route = (new RouteCollection($collector))->getRoute('main'); - $dispatcher = $injectDispatcher->withMiddlewares($route->getData('builtMiddlewares')); + $dispatcher = $injectDispatcher->withMiddlewares($route->getData('builtMiddlewareDefinitions')); $response = $dispatcher->dispatch($request, $this->getRequestHandler()); $this->assertSame(200, $response->getStatusCode()); @@ -354,7 +354,7 @@ public function testStaticRouteWithCollectorMiddlewares(): void $route = (new RouteCollection($collector))->getRoute('image'); - $dispatcher = $injectDispatcher->withMiddlewares($route->getData('builtMiddlewares')); + $dispatcher = $injectDispatcher->withMiddlewares($route->getData('builtMiddlewareDefinitions')); $this->expectException(RuntimeException::class); $this->expectExceptionMessage('Stack is empty.'); diff --git a/tests/RouteTest.php b/tests/RouteTest.php index 6e0f146f..898b8770 100644 --- a/tests/RouteTest.php +++ b/tests/RouteTest.php @@ -34,12 +34,12 @@ public function testSimpleInstance(): void methods: [Method::GET], pattern: '/', action: [TestController::class, 'index'], - middlewares: [TestMiddleware1::class], + middlewareDefinitions: [TestMiddleware1::class], override: true, ); $this->assertInstanceOf(Route::class, $route); - $this->assertCount(2, $route->getData('builtMiddlewares')); + $this->assertCount(2, $route->getData('builtMiddlewareDefinitions')); $this->assertTrue($route->getData('override')); } @@ -247,9 +247,9 @@ public function testPrependMiddlewareBeforeAction(): void public function testInvalidMiddlewares(): void { $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Invalid $middlewares provided, list of string or array or callable expected.'); + $this->expectExceptionMessage('Invalid $middlewareDefinitions provided, list of string or array or callable expected.'); - $route = new Route([Method::GET], '/', middlewares: [static fn () => new Response(), (object) ['test' => 1]]); + $route = new Route([Method::GET], '/', middlewareDefinitions: [static fn () => new Response(), (object) ['test' => 1]]); } public function testDisabledMiddlewareDefinitions(): void @@ -270,7 +270,7 @@ public function testDisabledMiddlewareDefinitions(): void ->action([TestController::class, 'index']) ->disableMiddleware(TestMiddleware1::class, TestMiddleware3::class); - $dispatcher = $injectDispatcher->withMiddlewares($route->getData('builtMiddlewares')); + $dispatcher = $injectDispatcher->withMiddlewares($route->getData('builtMiddlewareDefinitions')); $response = $dispatcher->dispatch($request, $this->getRequestHandler()); $this->assertSame(200, $response->getStatusCode()); @@ -295,7 +295,7 @@ public function testPrependMiddlewareDefinitions(): void ->action([TestController::class, 'index']) ->prependMiddleware(TestMiddleware1::class, TestMiddleware2::class); - $dispatcher = $injectDispatcher->withMiddlewares($route->getData('builtMiddlewares')); + $dispatcher = $injectDispatcher->withMiddlewares($route->getData('builtMiddlewareDefinitions')); $response = $dispatcher->dispatch($request, $this->getRequestHandler()); $this->assertSame(200, $response->getStatusCode()); @@ -336,18 +336,18 @@ public function testDebugInfo(): void [override] => 1 [actionAdded] => 1 - [middlewares] => Array + [middlewareDefinitions] => Array ( [0] => Yiisoft\Router\Tests\Support\TestMiddleware3 [1] => Yiisoft\Router\Tests\Support\TestMiddleware1 [2] => go ) - [builtMiddlewares] => Array + [builtMiddlewareDefinitions] => Array ( ) - [disabledMiddlewares] => Array + [disabledMiddlewareDefinitions] => Array ( [0] => Yiisoft\Router\Tests\Support\TestMiddleware2 ) @@ -396,9 +396,9 @@ public function testBuiltMiddlewares(): void ->middleware(TestMiddleware1::class) ->action(static fn () => new Response(200)); - $builtMiddlewares = $route->getData('builtMiddlewares'); + $builtMiddlewareDefinitions = $route->getData('builtMiddlewareDefinitions'); - $this->assertSame($builtMiddlewares, $route->getData('builtMiddlewares')); + $this->assertSame($builtMiddlewareDefinitions, $route->getData('builtMiddlewareDefinitions')); } private function getRequestHandler(): RequestHandlerInterface From 61cbc83292b47a0b0d08464d897d99f9a79d28a6 Mon Sep 17 00:00:00 2001 From: Rustam Date: Mon, 16 Oct 2023 12:46:16 +0500 Subject: [PATCH 21/63] Minor improvements --- composer-require-checker.json | 1 + src/Provider/FileRoutesProvider.php | 4 ++-- src/Route.php | 3 +++ tests/Attribute/RouteTest.php | 31 +++++++++++++++++++++++++++++ 4 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 tests/Attribute/RouteTest.php diff --git a/composer-require-checker.json b/composer-require-checker.json index 34286ea5..289c401d 100644 --- a/composer-require-checker.json +++ b/composer-require-checker.json @@ -1,5 +1,6 @@ { "symbol-whitelist" : [ + "olvlvl\\ComposerAttributeCollector\\Attributes", "Psr\\Container\\ContainerInterface", "Yiisoft\\VarDumper\\VarDumper", "Yiisoft\\Yii\\Debug\\Debugger", diff --git a/src/Provider/FileRoutesProvider.php b/src/Provider/FileRoutesProvider.php index ee130eed..42c34e92 100644 --- a/src/Provider/FileRoutesProvider.php +++ b/src/Provider/FileRoutesProvider.php @@ -18,13 +18,13 @@ public function __construct(private string $file, private array $scope = []) public function getRoutes(): array { - $scopeRequire = static function (string $file, array $scope): mixed { + $scopeRequire = \Closure::bind(static function (string $file, array $scope): mixed { extract($scope, EXTR_SKIP); /** * @psalm-suppress UnresolvableInclude */ return require $file; - }; + }, null, null); if (!file_exists($this->file)) { throw new \RuntimeException( 'Failed to provide routes from "' . $this->file . '". File or directory not found.' diff --git a/src/Route.php b/src/Route.php index 40e491cd..95d4564b 100644 --- a/src/Route.php +++ b/src/Route.php @@ -58,6 +58,9 @@ public function __construct( private bool $override = false, private array $disabledMiddlewareDefinitions = [], ) { + if (empty($methods)) { + throw new InvalidArgumentException('$methods cannot be empty.'); + } $this->assertListOfStrings($methods, 'methods'); $this->assertMiddlewares($middlewareDefinitions); $this->assertListOfStrings($hosts, 'hosts'); diff --git a/tests/Attribute/RouteTest.php b/tests/Attribute/RouteTest.php new file mode 100644 index 00000000..930e9164 --- /dev/null +++ b/tests/Attribute/RouteTest.php @@ -0,0 +1,31 @@ +getRoute(); + + $this->assertSame('/post', $route->getData('pattern')); + $this->assertSame([Method::GET, Method::HEAD], $route->getData('methods')); + } + + public function testOverride(): void + { + $attribute = new Route([Method::GET, Method::HEAD], '/', override: true); + + $route = $attribute->getRoute(); + + $this->assertTrue($route->getData('override')); + } +} From d85e6670c0f16d013bf215b303299e1456ccf796 Mon Sep 17 00:00:00 2001 From: Rustam Date: Mon, 16 Oct 2023 14:00:29 +0500 Subject: [PATCH 22/63] Minor --- src/Provider/FileRoutesProvider.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Provider/FileRoutesProvider.php b/src/Provider/FileRoutesProvider.php index 42c34e92..e0c17c0f 100644 --- a/src/Provider/FileRoutesProvider.php +++ b/src/Provider/FileRoutesProvider.php @@ -4,6 +4,7 @@ namespace Yiisoft\Router\Provider; +use Closure; use Yiisoft\Router\Group; use Yiisoft\Router\Route; @@ -18,13 +19,14 @@ public function __construct(private string $file, private array $scope = []) public function getRoutes(): array { - $scopeRequire = \Closure::bind(static function (string $file, array $scope): mixed { + /** @var Closure $scopeRequire */ + $scopeRequire = Closure::bind(static function (string $file, array $scope): mixed { extract($scope, EXTR_SKIP); /** * @psalm-suppress UnresolvableInclude */ return require $file; - }, null, null); + }, null); if (!file_exists($this->file)) { throw new \RuntimeException( 'Failed to provide routes from "' . $this->file . '". File or directory not found.' From 0ebcb5a068b93a16e3489fb02090da6432329eb5 Mon Sep 17 00:00:00 2001 From: Rustam Date: Mon, 23 Oct 2023 11:53:48 +0500 Subject: [PATCH 23/63] Move attribute collector it's own package --- composer.json | 1 - src/Provider/AttributeRoutesProvider.php | 62 ------------------------ tests/RouteTest.php | 8 +++ 3 files changed, 8 insertions(+), 63 deletions(-) delete mode 100644 src/Provider/AttributeRoutesProvider.php diff --git a/composer.json b/composer.json index 826b44a2..acaf35e4 100644 --- a/composer.json +++ b/composer.json @@ -31,7 +31,6 @@ "require-dev": { "maglnet/composer-require-checker": "^4.4", "nyholm/psr7": "^1.5", - "olvlvl/composer-attribute-collector": "^2.0", "phpunit/phpunit": "^9.5", "psr/container": "^1.1|^2.0", "rector/rector": "^0.18.3", diff --git a/src/Provider/AttributeRoutesProvider.php b/src/Provider/AttributeRoutesProvider.php deleted file mode 100644 index 575f63d0..00000000 --- a/src/Provider/AttributeRoutesProvider.php +++ /dev/null @@ -1,62 +0,0 @@ - - */ - private static array $reflectionsCache = []; - - public function getRoutes(): array - { - $routes = []; - $groupRoutes = []; - $routePredicate = Attributes::predicateForAttributeInstanceOf(RouteAttributeInterface::class); - $targetMethods = Attributes::filterTargetMethods($routePredicate); - foreach ($targetMethods as $targetMethod) { - /** @var RouteAttributeInterface $routeAttribute */ - $routeAttribute = $targetMethod->attribute; - $route = $routeAttribute->getRoute(); - $targetMethodReflection = self::$reflectionsCache[$targetMethod->class] ??= new ReflectionClass( - $targetMethod->class - ); - /** @var Group[] $groupAttributes */ - $groupAttributes = $targetMethodReflection->getAttributes( - Group::class, - ReflectionAttribute::IS_INSTANCEOF - ); - if (!empty($groupAttributes)) { - $groupRoutes[$targetMethod->class][] = $route->action([$targetMethod->class, $targetMethod->name]); - } else { - $routes[] = $route->action([$targetMethod->class, $targetMethod->name]); - } - } - $groupPredicate = static fn (string $attribute): bool => is_a($attribute, Route::class, true) - || is_a($attribute, Group::class, true); - $targetClasses = Attributes::filterTargetClasses($groupPredicate); - foreach ($targetClasses as $targetClass) { - $group = $targetClass->attribute; - if ($group instanceof Group && isset($groupRoutes[$targetClass->name])) { - $routes[] = $group->routes(...$groupRoutes[$targetClass->name]); - } elseif ($group instanceof Route) { - $routes[] = $group->action($targetClass->name); - } - } - return $routes; - } -} diff --git a/tests/RouteTest.php b/tests/RouteTest.php index 898b8770..50236662 100644 --- a/tests/RouteTest.php +++ b/tests/RouteTest.php @@ -43,6 +43,14 @@ public function testSimpleInstance(): void $this->assertTrue($route->getData('override')); } + public function testEmptyMethods(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('$methods cannot be empty.'); + + new Route([], ''); + } + public function testName(): void { $route = Route::get('/')->name('test.route'); From 3ff6921b56399ab22e3e0621a4debc37597be56f Mon Sep 17 00:00:00 2001 From: Rustam Date: Mon, 23 Oct 2023 12:17:30 +0500 Subject: [PATCH 24/63] Cleanup --- composer-require-checker.json | 1 - composer.json | 6 ++---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/composer-require-checker.json b/composer-require-checker.json index 289c401d..34286ea5 100644 --- a/composer-require-checker.json +++ b/composer-require-checker.json @@ -1,6 +1,5 @@ { "symbol-whitelist" : [ - "olvlvl\\ComposerAttributeCollector\\Attributes", "Psr\\Container\\ContainerInterface", "Yiisoft\\VarDumper\\VarDumper", "Yiisoft\\Yii\\Debug\\Debugger", diff --git a/composer.json b/composer.json index acaf35e4..0bf73fdc 100644 --- a/composer.json +++ b/composer.json @@ -53,8 +53,7 @@ } }, "suggest": { - "yiisoft/router-fastroute": "Router implementation based on nikic/FastRoute", - "olvlvl/composer-attribute-collector": "Required to register routes using PHP attributes" + "yiisoft/router-fastroute": "Router implementation based on nikic/FastRoute" }, "extra": { "config-plugin-options": { @@ -70,8 +69,7 @@ "allow-plugins": { "infection/extension-installer": true, "composer/package-versions-deprecated": true, - "yiisoft/config": false, - "olvlvl/composer-attribute-collector": true + "yiisoft/config": false } }, "scripts": { From a81c91ab126d9f50a279a6f84415adf8d75a912f Mon Sep 17 00:00:00 2001 From: Rustam Date: Mon, 23 Oct 2023 12:29:47 +0500 Subject: [PATCH 25/63] Minor --- src/Route.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Route.php b/src/Route.php index 95d4564b..cf137e4e 100644 --- a/src/Route.php +++ b/src/Route.php @@ -288,7 +288,7 @@ public function __toString(): string $result .= implode(',', $this->methods) . ' '; } - if ($this->hosts) { + if (!empty($this->hosts)) { $quoted = array_map(static fn ($host) => preg_quote($host, '/'), $this->hosts); if (!preg_match('/' . implode('|', $quoted) . '/', $this->pattern)) { From 5be62b4e33a97e16f9266d4cd0f79c4d7e2e54de Mon Sep 17 00:00:00 2001 From: Rustam Date: Thu, 2 Nov 2023 16:12:36 +0500 Subject: [PATCH 26/63] Add changelog --- CHANGELOG.md | 6 ++++++ README.md | 1 + 2 files changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fda7b263..8b7cac56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ - New #195: Add debug collector for `yiisoft/yii-debug` (@xepozz) - Chg #207: Replace two `RouteCollectorInterface` methods `addRoute()` and `addGroup()` to single `addRoute()` (@vjik) - Enh #202: Add support for `psr/http-message` version `^2.0` (@vjik) +- New #196: Add PHP Attributes support (@rustamwin) +- New #196: Add `RoutesProviderInterface` interface providing routes from various resources (@rustamwin) +- Enh #196: The `Group` and `Route` classes have been refactored to be DTO objects & dispatcher-independent. (@rustamwin) +- Enh #196: The `MatchingResult` class has been improved to be dispatcher-independent (@rustamwin) +- Chg #196: The implementation of `MatchingResult` from `MiddlewareInterface` has been removed, so + it is no longer middleware. (@rustamwin) ## 3.0.0 February 17, 2023 diff --git a/README.md b/README.md index c4792c51..06e2a572 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ with an adapter package. Currently, the only adapter available is [FastRoute](ht - Ready to use middleware for route matching. - Convenient `CurrentRoute` service that holds information about last matched route. - Out of the box CORS middleware support. +- Declaring routes using PHP attributes. ## Requirements From 63467dc6061f6c68d06efc98c1a46618eb62e4cd Mon Sep 17 00:00:00 2001 From: rustamwin Date: Thu, 2 Nov 2023 11:13:31 +0000 Subject: [PATCH 27/63] Apply Rector changes (CI) --- tests/GroupTest.php | 4 ++-- tests/RouteCollectionTest.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/GroupTest.php b/tests/GroupTest.php index 3d1c75cf..55c7d6d3 100644 --- a/tests/GroupTest.php +++ b/tests/GroupTest.php @@ -90,7 +90,7 @@ public function testAddNestedMiddleware(): void { $request = new ServerRequest('GET', '/outergroup/innergroup/test1'); - $action = static fn (ServerRequestInterface $request) => new Response(200, [], null, '1.1', implode($request->getAttributes())); + $action = static fn (ServerRequestInterface $request) => new Response(200, [], null, '1.1', implode('', $request->getAttributes())); $middleware1 = static function (ServerRequestInterface $request, RequestHandlerInterface $handler) { $request = $request->withAttribute('middleware', 'middleware1'); @@ -130,7 +130,7 @@ public function testGroupMiddlewareFullStackCalled(): void { $request = new ServerRequest('GET', '/group/test1'); - $action = static fn (ServerRequestInterface $request) => new Response(200, [], null, '1.1', implode($request->getAttributes())); + $action = static fn (ServerRequestInterface $request) => new Response(200, [], null, '1.1', implode('', $request->getAttributes())); $middleware1 = function (ServerRequestInterface $request, RequestHandlerInterface $handler) { $request = $request->withAttribute('middleware', 'middleware1'); return $handler->handle($request); diff --git a/tests/RouteCollectionTest.php b/tests/RouteCollectionTest.php index a0d788f9..7b462899 100644 --- a/tests/RouteCollectionTest.php +++ b/tests/RouteCollectionTest.php @@ -252,7 +252,7 @@ public function testCollectorMiddlewareFullstackCalled(): void [], null, '1.1', - implode($request->getAttributes()) + implode('', $request->getAttributes()) ); $listRoute = Route::get('/') ->action($action) From 81a58581818289485d2e08c0a8b4dcba4cab847c Mon Sep 17 00:00:00 2001 From: Rustam Date: Wed, 22 Oct 2025 10:33:07 +0500 Subject: [PATCH 28/63] Fixes --- src/Attribute/Delete.php | 4 +-- src/Attribute/Get.php | 4 +-- src/Attribute/Head.php | 4 +-- src/Attribute/Options.php | 4 +-- src/Attribute/Patch.php | 4 +-- src/Attribute/Post.php | 4 +-- src/Attribute/Put.php | 4 +-- src/Attribute/Route.php | 4 +-- src/Group.php | 47 +++++++++++++++++++++++++--- src/Route.php | 66 ++++++++++++++++++++++++++++++++++----- tests/GroupTest.php | 4 +-- tests/RouteTest.php | 10 +++--- 12 files changed, 125 insertions(+), 34 deletions(-) diff --git a/src/Attribute/Delete.php b/src/Attribute/Delete.php index b06b29bb..f05af35e 100644 --- a/src/Attribute/Delete.php +++ b/src/Attribute/Delete.php @@ -34,11 +34,11 @@ public function __construct( methods: [Method::DELETE], pattern: $pattern, name: $name, - middlewareDefinitions: $middlewares, + middlewares: $middlewares, defaults: $defaults, hosts: $hosts, override: $override, - disabledMiddlewareDefinitions: $disabledMiddlewares + disabledMiddlewares: $disabledMiddlewares ); } diff --git a/src/Attribute/Get.php b/src/Attribute/Get.php index 032fe580..82f3ead3 100644 --- a/src/Attribute/Get.php +++ b/src/Attribute/Get.php @@ -34,11 +34,11 @@ public function __construct( methods: [Method::GET], pattern: $pattern, name: $name, - middlewareDefinitions: $middlewares, + middlewares: $middlewares, defaults: $defaults, hosts: $hosts, override: $override, - disabledMiddlewareDefinitions: $disabledMiddlewares + disabledMiddlewares: $disabledMiddlewares ); } diff --git a/src/Attribute/Head.php b/src/Attribute/Head.php index 6ef954a4..9ec6fdfd 100644 --- a/src/Attribute/Head.php +++ b/src/Attribute/Head.php @@ -34,11 +34,11 @@ public function __construct( methods: [Method::HEAD], pattern: $pattern, name: $name, - middlewareDefinitions: $middlewares, + middlewares: $middlewares, defaults: $defaults, hosts: $hosts, override: $override, - disabledMiddlewareDefinitions: $disabledMiddlewares + disabledMiddlewares: $disabledMiddlewares ); } diff --git a/src/Attribute/Options.php b/src/Attribute/Options.php index ed823fa8..3bd889ac 100644 --- a/src/Attribute/Options.php +++ b/src/Attribute/Options.php @@ -34,11 +34,11 @@ public function __construct( methods: [Method::OPTIONS], pattern: $pattern, name: $name, - middlewareDefinitions: $middlewares, + middlewares: $middlewares, defaults: $defaults, hosts: $hosts, override: $override, - disabledMiddlewareDefinitions: $disabledMiddlewares + disabledMiddlewares: $disabledMiddlewares ); } diff --git a/src/Attribute/Patch.php b/src/Attribute/Patch.php index 377130dd..5095c744 100644 --- a/src/Attribute/Patch.php +++ b/src/Attribute/Patch.php @@ -34,11 +34,11 @@ public function __construct( methods: [Method::PATCH], pattern: $pattern, name: $name, - middlewareDefinitions: $middlewares, + middlewares: $middlewares, defaults: $defaults, hosts: $hosts, override: $override, - disabledMiddlewareDefinitions: $disabledMiddlewares + disabledMiddlewares: $disabledMiddlewares ); } diff --git a/src/Attribute/Post.php b/src/Attribute/Post.php index 9e58cdf8..df798195 100644 --- a/src/Attribute/Post.php +++ b/src/Attribute/Post.php @@ -34,11 +34,11 @@ public function __construct( methods: [Method::POST], pattern: $pattern, name: $name, - middlewareDefinitions: $middlewares, + middlewares: $middlewares, defaults: $defaults, hosts: $hosts, override: $override, - disabledMiddlewareDefinitions: $disabledMiddlewares + disabledMiddlewares: $disabledMiddlewares ); } diff --git a/src/Attribute/Put.php b/src/Attribute/Put.php index e5404dfb..83dab1d2 100644 --- a/src/Attribute/Put.php +++ b/src/Attribute/Put.php @@ -34,11 +34,11 @@ public function __construct( methods: [Method::PUT], pattern: $pattern, name: $name, - middlewareDefinitions: $middlewares, + middlewares: $middlewares, defaults: $defaults, hosts: $hosts, override: $override, - disabledMiddlewareDefinitions: $disabledMiddlewares + disabledMiddlewares: $disabledMiddlewares ); } diff --git a/src/Attribute/Route.php b/src/Attribute/Route.php index 29d668cf..76409248 100644 --- a/src/Attribute/Route.php +++ b/src/Attribute/Route.php @@ -34,11 +34,11 @@ public function __construct( methods: $methods, pattern: $pattern, name: $name, - middlewareDefinitions: $middlewares, + middlewares: $middlewares, defaults: $defaults, hosts: $hosts, override: $override, - disabledMiddlewareDefinitions: $disabledMiddlewares + disabledMiddlewares: $disabledMiddlewares ); } diff --git a/src/Group.php b/src/Group.php index bb09524d..18d616c1 100644 --- a/src/Group.php +++ b/src/Group.php @@ -29,10 +29,8 @@ final class Group * @var string[] */ private array $hosts = []; - private ?string $namePrefix = null; private bool $routesAdded = false; private bool $middlewareAdded = false; - private array $disabledMiddlewares = []; /** * @psalm-var list|null @@ -44,9 +42,24 @@ final class Group */ private $corsMiddleware = null; - private function __construct( - private ?string $prefix = null + /** + * @param array $disabledMiddlewares Excludes middleware from being invoked when action is handled. + * It is useful to avoid invoking one of the parent group middleware for + * a certain route. + */ + public function __construct( + private ?string $prefix = null, + array $middlewares = [], + array $hosts = [], + private ?string $namePrefix = null, + private array $disabledMiddlewares = [], + array|callable|string|null $corsMiddleware = null ) { + $this->assertMiddlewares($middlewares); + $this->assertHosts($hosts); + $this->middlewares = $middlewares; + $this->hosts = $hosts; + $this->corsMiddleware = $corsMiddleware; } /** @@ -203,6 +216,32 @@ public function getData(string $key): mixed }; } + private function assertHosts(array $hosts): void + { + foreach ($hosts as $host) { + if (!is_string($host)) { + throw new \InvalidArgumentException('Invalid $hosts provided, list of string expected.'); + } + } + } + + /** + * @psalm-assert array $middlewareDefinitions + */ + private function assertMiddlewares(array $middlewareDefinitions): void + { + /** @var mixed $middlewareDefinition */ + foreach ($middlewareDefinitions as $middlewareDefinition) { + if (is_string($middlewareDefinition) || is_callable($middlewareDefinition) || is_array($middlewareDefinition)) { + continue; + } + + throw new \InvalidArgumentException( + 'Invalid $middlewareDefinitions provided, list of string or array or callable expected.' + ); + } + } + /** * @return array[]|callable[]|string[] * @psalm-return list diff --git a/src/Route.php b/src/Route.php index 0a51c871..8a32496b 100644 --- a/src/Route.php +++ b/src/Route.php @@ -18,23 +18,21 @@ final class Route implements Stringable { private bool $actionAdded = false; - /** - * @var array[]|callable[]|string[] - */ - private array $builtMiddlewareDefinitions = []; /** * @var array[]|callable[]|string[] * @psalm-var list */ private array $middlewares = []; - private array $disabledMiddlewares = []; - /** * @psalm-var list|null */ private ?array $enabledMiddlewaresCache = null; + /** + * @var string[] + */ + private array $methods; /** * @var string[] */ @@ -56,7 +54,28 @@ final class Route implements Stringable public function __construct( array $methods, private string $pattern, + private ?string $name = null, + array|callable|string|null $action = null, + array $middlewares = [], + array $defaults = [], + array $hosts = [], + private bool $override = false, + private array $disabledMiddlewares = [], ) { + if (empty($methods)) { + throw new InvalidArgumentException('$methods cannot be empty.'); + } + $this->assertListOfStrings($methods, 'methods'); + $this->assertMiddlewares($middlewares); + $this->assertListOfStrings($hosts, 'hosts'); + $this->middlewares = $middlewares; + $this->methods = $methods; + $this->hosts = $hosts; + $this->defaults = array_map('\strval', $defaults); + if (!empty($action)) { + $this->middlewares[] = $action; + $this->actionAdded = true; + } } public static function get(string $pattern): self @@ -218,7 +237,7 @@ public function action(array|callable|string $middlewareDefinition): self $route = clone $this; $route->middlewares[] = $middlewareDefinition; $route->actionAdded = true; - $route->builtMiddlewareDefinitions = []; + $route->enabledMiddlewaresCache = null; return $route; } @@ -316,6 +335,39 @@ public function __debugInfo() ]; } + /** + * @psalm-assert array $items + */ + private function assertListOfStrings(array $items, string $argument): void + { + foreach ($items as $item) { + if (!is_string($item)) { + throw new \InvalidArgumentException('Invalid $' . $argument . ' provided, list of string expected.'); + } + } + } + + /** + * @psalm-assert array $middlewareDefinitions + */ + private function assertMiddlewares(array $middlewareDefinitions): void + { + /** @var mixed $middlewareDefinition */ + foreach ($middlewareDefinitions as $middlewareDefinition) { + if (is_string($middlewareDefinition)) { + continue; + } + + if (is_callable($middlewareDefinition) || is_array($middlewareDefinition)) { + continue; + } + + throw new \InvalidArgumentException( + 'Invalid $middlewareDefinitions provided, list of string or array or callable expected.' + ); + } + } + /** * @return array[]|callable[]|string[] * @psalm-return list diff --git a/tests/GroupTest.php b/tests/GroupTest.php index 9f585f98..835ecb92 100644 --- a/tests/GroupTest.php +++ b/tests/GroupTest.php @@ -488,9 +488,9 @@ public function testBuiltMiddlewares(): void ->middleware(static fn () => new Response(200)) ->prependMiddleware(TestMiddleware1::class); - $builtMiddlewareDefinitions = $group->getData('middlewareDefinitions'); + $builtMiddlewareDefinitions = $group->getData('enabledMiddlewares'); - $this->assertSame($builtMiddlewareDefinitions, $group->getData('middlewareDefinitions')); + $this->assertSame($builtMiddlewareDefinitions, $group->getData('enabledMiddlewares')); } private function getRequestHandler(): RequestHandlerInterface diff --git a/tests/RouteTest.php b/tests/RouteTest.php index cab3be6e..7a349f62 100644 --- a/tests/RouteTest.php +++ b/tests/RouteTest.php @@ -35,12 +35,12 @@ public function testSimpleInstance(): void methods: [Method::GET], pattern: '/', action: [TestController::class, 'index'], - middlewareDefinitions: [TestMiddleware1::class], + middlewares: [TestMiddleware1::class], override: true, ); $this->assertInstanceOf(Route::class, $route); - $this->assertCount(2, $route->getData('builtMiddlewareDefinitions')); + $this->assertCount(2, $route->getData('enabledMiddlewares')); $this->assertTrue($route->getData('override')); } @@ -275,7 +275,7 @@ public function testInvalidMiddlewares(): void $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('Invalid $middlewareDefinitions provided, list of string or array or callable expected.'); - $route = new Route([Method::GET], '/', middlewareDefinitions: [static fn () => new Response(), (object) ['test' => 1]]); + $route = new Route([Method::GET], '/', middlewares: [static fn () => new Response(), (object) ['test' => 1]]); } public function testDisabledMiddlewareDefinitions(): void @@ -499,9 +499,9 @@ public function testBuiltMiddlewares(): void ->middleware(TestMiddleware1::class) ->action(static fn () => new Response(200)); - $builtMiddlewareDefinitions = $route->getData('builtMiddlewareDefinitions'); + $builtMiddlewareDefinitions = $route->getData('enabledMiddlewares'); - $this->assertSame($builtMiddlewareDefinitions, $route->getData('builtMiddlewareDefinitions')); + $this->assertSame($builtMiddlewareDefinitions, $route->getData('enabledMiddlewares')); } private function getRequestHandler(): RequestHandlerInterface From b44cd315e661fb76d1d0bc05a81cd0cfd82f2782 Mon Sep 17 00:00:00 2001 From: rustamwin <16498265+rustamwin@users.noreply.github.com> Date: Wed, 22 Oct 2025 05:34:30 +0000 Subject: [PATCH 29/63] Apply Rector changes (CI) --- src/Attribute/Delete.php | 2 +- src/Attribute/Get.php | 2 +- src/Attribute/Head.php | 2 +- src/Attribute/Options.php | 2 +- src/Attribute/Patch.php | 2 +- src/Attribute/Post.php | 2 +- src/Attribute/Put.php | 2 +- src/Attribute/Route.php | 2 +- src/Provider/ArrayRoutesProvider.php | 2 +- src/Provider/FileRoutesProvider.php | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Attribute/Delete.php b/src/Attribute/Delete.php index f05af35e..62e1ca9b 100644 --- a/src/Attribute/Delete.php +++ b/src/Attribute/Delete.php @@ -12,7 +12,7 @@ #[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] final class Delete implements RouteAttributeInterface { - private Route $route; + private readonly Route $route; /** * @param array $defaults Parameter default values indexed by parameter names. diff --git a/src/Attribute/Get.php b/src/Attribute/Get.php index 82f3ead3..ef376dd8 100644 --- a/src/Attribute/Get.php +++ b/src/Attribute/Get.php @@ -12,7 +12,7 @@ #[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] final class Get implements RouteAttributeInterface { - private Route $route; + private readonly Route $route; /** * @param array $defaults Parameter default values indexed by parameter names. diff --git a/src/Attribute/Head.php b/src/Attribute/Head.php index 9ec6fdfd..cdcc83f7 100644 --- a/src/Attribute/Head.php +++ b/src/Attribute/Head.php @@ -12,7 +12,7 @@ #[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] final class Head implements RouteAttributeInterface { - private Route $route; + private readonly Route $route; /** * @param array $defaults Parameter default values indexed by parameter names. diff --git a/src/Attribute/Options.php b/src/Attribute/Options.php index 3bd889ac..f3aa4dea 100644 --- a/src/Attribute/Options.php +++ b/src/Attribute/Options.php @@ -12,7 +12,7 @@ #[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] final class Options implements RouteAttributeInterface { - private Route $route; + private readonly Route $route; /** * @param array $defaults Parameter default values indexed by parameter names. diff --git a/src/Attribute/Patch.php b/src/Attribute/Patch.php index 5095c744..e2a57157 100644 --- a/src/Attribute/Patch.php +++ b/src/Attribute/Patch.php @@ -12,7 +12,7 @@ #[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] final class Patch implements RouteAttributeInterface { - private Route $route; + private readonly Route $route; /** * @param array $defaults Parameter default values indexed by parameter names. diff --git a/src/Attribute/Post.php b/src/Attribute/Post.php index df798195..0f06aa2d 100644 --- a/src/Attribute/Post.php +++ b/src/Attribute/Post.php @@ -12,7 +12,7 @@ #[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] final class Post implements RouteAttributeInterface { - private Route $route; + private readonly Route $route; /** * @param array $defaults Parameter default values indexed by parameter names. diff --git a/src/Attribute/Put.php b/src/Attribute/Put.php index 83dab1d2..7168632a 100644 --- a/src/Attribute/Put.php +++ b/src/Attribute/Put.php @@ -12,7 +12,7 @@ #[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] final class Put implements RouteAttributeInterface { - private Route $route; + private readonly Route $route; /** * @param array $defaults Parameter default values indexed by parameter names. diff --git a/src/Attribute/Route.php b/src/Attribute/Route.php index 76409248..5006d2f6 100644 --- a/src/Attribute/Route.php +++ b/src/Attribute/Route.php @@ -11,7 +11,7 @@ #[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] final class Route implements RouteAttributeInterface { - private RouteObject $route; + private readonly RouteObject $route; /** * @param array $defaults Parameter default values indexed by parameter names. diff --git a/src/Provider/ArrayRoutesProvider.php b/src/Provider/ArrayRoutesProvider.php index 83abd222..3c2f20ef 100644 --- a/src/Provider/ArrayRoutesProvider.php +++ b/src/Provider/ArrayRoutesProvider.php @@ -12,7 +12,7 @@ final class ArrayRoutesProvider implements RoutesProviderInterface /** * @param Group[]|Route[] $routes */ - public function __construct(private array $routes) + public function __construct(private readonly array $routes) { } diff --git a/src/Provider/FileRoutesProvider.php b/src/Provider/FileRoutesProvider.php index e0c17c0f..a05b16d7 100644 --- a/src/Provider/FileRoutesProvider.php +++ b/src/Provider/FileRoutesProvider.php @@ -13,7 +13,7 @@ */ final class FileRoutesProvider implements RoutesProviderInterface { - public function __construct(private string $file, private array $scope = []) + public function __construct(private readonly string $file, private readonly array $scope = []) { } From d3bc8e406e54becdb17102e6521fbce0a61a0278 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Wed, 22 Oct 2025 05:34:46 +0000 Subject: [PATCH 30/63] Apply fixes from StyleCI --- src/Group.php | 4 ++-- src/MatchingResult.php | 1 + src/Route.php | 4 ++-- tests/Support/resources/foo.php | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Group.php b/src/Group.php index 18d616c1..1e4afff4 100644 --- a/src/Group.php +++ b/src/Group.php @@ -220,7 +220,7 @@ private function assertHosts(array $hosts): void { foreach ($hosts as $host) { if (!is_string($host)) { - throw new \InvalidArgumentException('Invalid $hosts provided, list of string expected.'); + throw new InvalidArgumentException('Invalid $hosts provided, list of string expected.'); } } } @@ -236,7 +236,7 @@ private function assertMiddlewares(array $middlewareDefinitions): void continue; } - throw new \InvalidArgumentException( + throw new InvalidArgumentException( 'Invalid $middlewareDefinitions provided, list of string or array or callable expected.' ); } diff --git a/src/MatchingResult.php b/src/MatchingResult.php index 8f31d0a4..be152695 100644 --- a/src/MatchingResult.php +++ b/src/MatchingResult.php @@ -23,6 +23,7 @@ final class MatchingResult private function __construct(private readonly ?Route $route) { } + /** * @param string[] $arguments * @psalm-param array $arguments diff --git a/src/Route.php b/src/Route.php index 8a32496b..65773dc7 100644 --- a/src/Route.php +++ b/src/Route.php @@ -342,7 +342,7 @@ private function assertListOfStrings(array $items, string $argument): void { foreach ($items as $item) { if (!is_string($item)) { - throw new \InvalidArgumentException('Invalid $' . $argument . ' provided, list of string expected.'); + throw new InvalidArgumentException('Invalid $' . $argument . ' provided, list of string expected.'); } } } @@ -362,7 +362,7 @@ private function assertMiddlewares(array $middlewareDefinitions): void continue; } - throw new \InvalidArgumentException( + throw new InvalidArgumentException( 'Invalid $middlewareDefinitions provided, list of string or array or callable expected.' ); } diff --git a/tests/Support/resources/foo.php b/tests/Support/resources/foo.php index 30129aa1..4bf0067c 100644 --- a/tests/Support/resources/foo.php +++ b/tests/Support/resources/foo.php @@ -4,5 +4,5 @@ return [ - new \stdClass(), + new stdClass(), ]; From f4b3020fc9f44a20e59b4cb4825ad35c36aa960d Mon Sep 17 00:00:00 2001 From: Rustam Date: Wed, 22 Oct 2025 12:04:35 +0500 Subject: [PATCH 31/63] Fix psalm issues --- composer.json | 2 +- src/Attribute/Delete.php | 5 +++++ src/Attribute/Get.php | 5 +++++ src/Attribute/Head.php | 5 +++++ src/Attribute/Options.php | 5 +++++ src/Attribute/Patch.php | 5 +++++ src/Attribute/Post.php | 5 +++++ src/Attribute/Put.php | 5 +++++ src/Attribute/Route.php | 7 +++++++ src/Group.php | 5 +++++ src/Route.php | 5 ++++- 11 files changed, 52 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index ddd318ba..a3bdb345 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,7 @@ } ], "require": { - "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0", + "php": "8.1 - 8.4", "psr/event-dispatcher": "^1.0", "psr/http-factory": "^1.0", "psr/http-message": "^1.0 || ^2.0", diff --git a/src/Attribute/Delete.php b/src/Attribute/Delete.php index 62e1ca9b..71229a5a 100644 --- a/src/Attribute/Delete.php +++ b/src/Attribute/Delete.php @@ -15,11 +15,16 @@ final class Delete implements RouteAttributeInterface private readonly Route $route; /** + * @param string[] $hosts Hosts that the route should match. + * @param array[]|callable[]|string[] $middlewares Middlewares to be added to the route. + * @param string|null $name Route name. If not set, it will be generated automatically. * @param array $defaults Parameter default values indexed by parameter names. * @param bool $override Marks route as override. When added it will replace existing route with the same name. * @param array $disabledMiddlewares Excludes middleware from being invoked when action is handled. * It is useful to avoid invoking one of the parent group middleware for * a certain route. + * + * @psalm-param list $middlewares */ public function __construct( string $pattern, diff --git a/src/Attribute/Get.php b/src/Attribute/Get.php index ef376dd8..f655e415 100644 --- a/src/Attribute/Get.php +++ b/src/Attribute/Get.php @@ -15,11 +15,16 @@ final class Get implements RouteAttributeInterface private readonly Route $route; /** + * @param string[] $hosts Hosts that the route should match. + * @param array[]|callable[]|string[] $middlewares Middlewares to be added to the route. + * @param string|null $name Route name. If not set, it will be generated automatically. * @param array $defaults Parameter default values indexed by parameter names. * @param bool $override Marks route as override. When added it will replace existing route with the same name. * @param array $disabledMiddlewares Excludes middleware from being invoked when action is handled. * It is useful to avoid invoking one of the parent group middleware for * a certain route. + * + * @psalm-param list $middlewares */ public function __construct( string $pattern, diff --git a/src/Attribute/Head.php b/src/Attribute/Head.php index cdcc83f7..d6b55ca3 100644 --- a/src/Attribute/Head.php +++ b/src/Attribute/Head.php @@ -15,11 +15,16 @@ final class Head implements RouteAttributeInterface private readonly Route $route; /** + * @param string[] $hosts Hosts that the route should match. + * @param array[]|callable[]|string[] $middlewares Middlewares to be added to the route. + * @param string|null $name Route name. If not set, it will be generated automatically. * @param array $defaults Parameter default values indexed by parameter names. * @param bool $override Marks route as override. When added it will replace existing route with the same name. * @param array $disabledMiddlewares Excludes middleware from being invoked when action is handled. * It is useful to avoid invoking one of the parent group middleware for * a certain route. + * + * @psalm-param list $middlewares */ public function __construct( string $pattern, diff --git a/src/Attribute/Options.php b/src/Attribute/Options.php index f3aa4dea..f59c4f98 100644 --- a/src/Attribute/Options.php +++ b/src/Attribute/Options.php @@ -15,11 +15,16 @@ final class Options implements RouteAttributeInterface private readonly Route $route; /** + * @param string[] $hosts Hosts that the route should match. + * @param array[]|callable[]|string[] $middlewares Middlewares to be added to the route. + * @param string|null $name Route name. If not set, it will be generated automatically. * @param array $defaults Parameter default values indexed by parameter names. * @param bool $override Marks route as override. When added it will replace existing route with the same name. * @param array $disabledMiddlewares Excludes middleware from being invoked when action is handled. * It is useful to avoid invoking one of the parent group middleware for * a certain route. + * + * @psalm-param list $middlewares */ public function __construct( string $pattern, diff --git a/src/Attribute/Patch.php b/src/Attribute/Patch.php index e2a57157..69b0c590 100644 --- a/src/Attribute/Patch.php +++ b/src/Attribute/Patch.php @@ -15,11 +15,16 @@ final class Patch implements RouteAttributeInterface private readonly Route $route; /** + * @param string[] $hosts Hosts that the route should match. + * @param array[]|callable[]|string[] $middlewares Middlewares to be added to the route. + * @param string|null $name Route name. If not set, it will be generated automatically. * @param array $defaults Parameter default values indexed by parameter names. * @param bool $override Marks route as override. When added it will replace existing route with the same name. * @param array $disabledMiddlewares Excludes middleware from being invoked when action is handled. * It is useful to avoid invoking one of the parent group middleware for * a certain route. + * + * @psalm-param list $middlewares */ public function __construct( string $pattern, diff --git a/src/Attribute/Post.php b/src/Attribute/Post.php index 0f06aa2d..10785a08 100644 --- a/src/Attribute/Post.php +++ b/src/Attribute/Post.php @@ -15,11 +15,16 @@ final class Post implements RouteAttributeInterface private readonly Route $route; /** + * @param string[] $hosts Hosts that the route should match. + * @param array[]|callable[]|string[] $middlewares Middlewares to be added to the route. + * @param string|null $name Route name. If not set, it will be generated automatically. * @param array $defaults Parameter default values indexed by parameter names. * @param bool $override Marks route as override. When added it will replace existing route with the same name. * @param array $disabledMiddlewares Excludes middleware from being invoked when action is handled. * It is useful to avoid invoking one of the parent group middleware for * a certain route. + * + * @psalm-param list $middlewares */ public function __construct( string $pattern, diff --git a/src/Attribute/Put.php b/src/Attribute/Put.php index 7168632a..ba4512c9 100644 --- a/src/Attribute/Put.php +++ b/src/Attribute/Put.php @@ -15,11 +15,16 @@ final class Put implements RouteAttributeInterface private readonly Route $route; /** + * @param string[] $hosts Hosts that the route should match. + * @param array[]|callable[]|string[] $middlewares Middlewares to be added to the route. + * @param string|null $name Route name. If not set, it will be generated automatically. * @param array $defaults Parameter default values indexed by parameter names. * @param bool $override Marks route as override. When added it will replace existing route with the same name. * @param array $disabledMiddlewares Excludes middleware from being invoked when action is handled. * It is useful to avoid invoking one of the parent group middleware for * a certain route. + * + * @psalm-param list $middlewares */ public function __construct( string $pattern, diff --git a/src/Attribute/Route.php b/src/Attribute/Route.php index 5006d2f6..a7e1d640 100644 --- a/src/Attribute/Route.php +++ b/src/Attribute/Route.php @@ -14,11 +14,18 @@ final class Route implements RouteAttributeInterface private readonly RouteObject $route; /** + * @param string[] $methods HTTP methods that the route should match. + * @param string $pattern Route pattern. + * @param string|null $name Route name. If not set, it will be generated automatically. + * @param array $middlewares Middlewares to be added to the route. + * @param string[] $hosts Hosts that the route should match. * @param array $defaults Parameter default values indexed by parameter names. * @param bool $override Marks route as override. When added it will replace existing route with the same name. * @param array $disabledMiddlewares Excludes middleware from being invoked when action is handled. * It is useful to avoid invoking one of the parent group middleware for * a certain route. + * + * @psalm-param list $middlewares */ public function __construct( array $methods, diff --git a/src/Group.php b/src/Group.php index 1e4afff4..0acb80a7 100644 --- a/src/Group.php +++ b/src/Group.php @@ -43,9 +43,14 @@ final class Group private $corsMiddleware = null; /** + * @param array[]|callable[]|string[] $middlewares Middleware definitions. + * @param string[] $hosts List of host names. + * @param string|null $namePrefix Prefix for route names. * @param array $disabledMiddlewares Excludes middleware from being invoked when action is handled. * It is useful to avoid invoking one of the parent group middleware for * a certain route. + * + * @psalm-param list $middlewares */ public function __construct( private ?string $prefix = null, diff --git a/src/Route.php b/src/Route.php index 65773dc7..9bf73f0f 100644 --- a/src/Route.php +++ b/src/Route.php @@ -45,11 +45,14 @@ final class Route implements Stringable /** * @param array|callable|string|null $action Action handler. It is a primary middleware definition that * should be invoked last for a matched route. + * @param array[]|callable[]|string[] $middlewares Middleware definitions. * @param array $defaults Parameter default values indexed by parameter names. * @param bool $override Marks route as override. When added it will replace existing route with the same name. - * @param array $disabledMiddlewareDefinitions Excludes middleware from being invoked when action is handled. + * @param array $disabledMiddlewares Excludes middleware from being invoked when action is handled. * It is useful to avoid invoking one of the parent group middleware for * a certain route. + * + * @psalm-param list $middlewares */ public function __construct( array $methods, From 5a0221d68ba6d7f8f20d3f51b3638d14f3a48dd1 Mon Sep 17 00:00:00 2001 From: rustamwin <16498265+rustamwin@users.noreply.github.com> Date: Wed, 11 Feb 2026 09:08:28 +0000 Subject: [PATCH 32/63] Apply PHP CS Fixer and Rector changes (CI) --- src/Attribute/Delete.php | 4 +- src/Attribute/Get.php | 4 +- src/Attribute/Head.php | 4 +- src/Attribute/Options.php | 4 +- src/Attribute/Patch.php | 4 +- src/Attribute/Post.php | 4 +- src/Attribute/Put.php | 4 +- src/Attribute/Route.php | 4 +- src/Group.php | 10 ++- src/Provider/ArrayRoutesProvider.php | 4 +- src/Provider/FileRoutesProvider.php | 28 +++++--- src/Route.php | 85 ++++++++++++----------- src/RouteCollector.php | 4 +- tests/GroupTest.php | 7 +- tests/Provider/FileRoutesProviderTest.php | 5 +- tests/RouteCollectorTest.php | 6 +- tests/RouteTest.php | 10 +-- 17 files changed, 103 insertions(+), 88 deletions(-) diff --git a/src/Attribute/Delete.php b/src/Attribute/Delete.php index 71229a5a..4121b10a 100644 --- a/src/Attribute/Delete.php +++ b/src/Attribute/Delete.php @@ -33,7 +33,7 @@ public function __construct( array $defaults = [], array $hosts = [], bool $override = false, - array $disabledMiddlewares = [] + array $disabledMiddlewares = [], ) { $this->route = new Route( methods: [Method::DELETE], @@ -43,7 +43,7 @@ public function __construct( defaults: $defaults, hosts: $hosts, override: $override, - disabledMiddlewares: $disabledMiddlewares + disabledMiddlewares: $disabledMiddlewares, ); } diff --git a/src/Attribute/Get.php b/src/Attribute/Get.php index f655e415..351e258c 100644 --- a/src/Attribute/Get.php +++ b/src/Attribute/Get.php @@ -33,7 +33,7 @@ public function __construct( array $defaults = [], array $hosts = [], bool $override = false, - array $disabledMiddlewares = [] + array $disabledMiddlewares = [], ) { $this->route = new Route( methods: [Method::GET], @@ -43,7 +43,7 @@ public function __construct( defaults: $defaults, hosts: $hosts, override: $override, - disabledMiddlewares: $disabledMiddlewares + disabledMiddlewares: $disabledMiddlewares, ); } diff --git a/src/Attribute/Head.php b/src/Attribute/Head.php index d6b55ca3..5885bcfc 100644 --- a/src/Attribute/Head.php +++ b/src/Attribute/Head.php @@ -33,7 +33,7 @@ public function __construct( array $defaults = [], array $hosts = [], bool $override = false, - array $disabledMiddlewares = [] + array $disabledMiddlewares = [], ) { $this->route = new Route( methods: [Method::HEAD], @@ -43,7 +43,7 @@ public function __construct( defaults: $defaults, hosts: $hosts, override: $override, - disabledMiddlewares: $disabledMiddlewares + disabledMiddlewares: $disabledMiddlewares, ); } diff --git a/src/Attribute/Options.php b/src/Attribute/Options.php index f59c4f98..e50e364c 100644 --- a/src/Attribute/Options.php +++ b/src/Attribute/Options.php @@ -33,7 +33,7 @@ public function __construct( array $defaults = [], array $hosts = [], bool $override = false, - array $disabledMiddlewares = [] + array $disabledMiddlewares = [], ) { $this->route = new Route( methods: [Method::OPTIONS], @@ -43,7 +43,7 @@ public function __construct( defaults: $defaults, hosts: $hosts, override: $override, - disabledMiddlewares: $disabledMiddlewares + disabledMiddlewares: $disabledMiddlewares, ); } diff --git a/src/Attribute/Patch.php b/src/Attribute/Patch.php index 69b0c590..ac9b62bc 100644 --- a/src/Attribute/Patch.php +++ b/src/Attribute/Patch.php @@ -33,7 +33,7 @@ public function __construct( array $defaults = [], array $hosts = [], bool $override = false, - array $disabledMiddlewares = [] + array $disabledMiddlewares = [], ) { $this->route = new Route( methods: [Method::PATCH], @@ -43,7 +43,7 @@ public function __construct( defaults: $defaults, hosts: $hosts, override: $override, - disabledMiddlewares: $disabledMiddlewares + disabledMiddlewares: $disabledMiddlewares, ); } diff --git a/src/Attribute/Post.php b/src/Attribute/Post.php index 10785a08..be5a7757 100644 --- a/src/Attribute/Post.php +++ b/src/Attribute/Post.php @@ -33,7 +33,7 @@ public function __construct( array $defaults = [], array $hosts = [], bool $override = false, - array $disabledMiddlewares = [] + array $disabledMiddlewares = [], ) { $this->route = new Route( methods: [Method::POST], @@ -43,7 +43,7 @@ public function __construct( defaults: $defaults, hosts: $hosts, override: $override, - disabledMiddlewares: $disabledMiddlewares + disabledMiddlewares: $disabledMiddlewares, ); } diff --git a/src/Attribute/Put.php b/src/Attribute/Put.php index ba4512c9..5c3b3be7 100644 --- a/src/Attribute/Put.php +++ b/src/Attribute/Put.php @@ -33,7 +33,7 @@ public function __construct( array $defaults = [], array $hosts = [], bool $override = false, - array $disabledMiddlewares = [] + array $disabledMiddlewares = [], ) { $this->route = new Route( methods: [Method::PUT], @@ -43,7 +43,7 @@ public function __construct( defaults: $defaults, hosts: $hosts, override: $override, - disabledMiddlewares: $disabledMiddlewares + disabledMiddlewares: $disabledMiddlewares, ); } diff --git a/src/Attribute/Route.php b/src/Attribute/Route.php index a7e1d640..8dd50fef 100644 --- a/src/Attribute/Route.php +++ b/src/Attribute/Route.php @@ -35,7 +35,7 @@ public function __construct( array $defaults = [], array $hosts = [], bool $override = false, - array $disabledMiddlewares = [] + array $disabledMiddlewares = [], ) { $this->route = new RouteObject( methods: $methods, @@ -45,7 +45,7 @@ public function __construct( defaults: $defaults, hosts: $hosts, override: $override, - disabledMiddlewares: $disabledMiddlewares + disabledMiddlewares: $disabledMiddlewares, ); } diff --git a/src/Group.php b/src/Group.php index 60b7b732..0e9cbc82 100644 --- a/src/Group.php +++ b/src/Group.php @@ -9,6 +9,9 @@ use Yiisoft\Router\Internal\MiddlewareFilter; use function in_array; +use function is_array; +use function is_callable; +use function is_string; #[Attribute(Attribute::TARGET_CLASS)] final class Group @@ -57,13 +60,14 @@ public function __construct( array $hosts = [], private ?string $namePrefix = null, private array $disabledMiddlewares = [], - array|callable|string|null $corsMiddleware = null + array|callable|string|null $corsMiddleware = null, ) { $this->assertMiddlewares($middlewares); $this->assertHosts($hosts); $this->middlewares = $middlewares; $this->hosts = $hosts; - $this->corsMiddleware = $corsMiddleware;} + $this->corsMiddleware = $corsMiddleware; + } /** * Create a new group instance. @@ -230,7 +234,7 @@ private function assertMiddlewares(array $middlewareDefinitions): void } throw new InvalidArgumentException( - 'Invalid $middlewareDefinitions provided, list of string or array or callable expected.' + 'Invalid $middlewareDefinitions provided, list of string or array or callable expected.', ); } } diff --git a/src/Provider/ArrayRoutesProvider.php b/src/Provider/ArrayRoutesProvider.php index 3c2f20ef..3c52d546 100644 --- a/src/Provider/ArrayRoutesProvider.php +++ b/src/Provider/ArrayRoutesProvider.php @@ -12,9 +12,7 @@ final class ArrayRoutesProvider implements RoutesProviderInterface /** * @param Group[]|Route[] $routes */ - public function __construct(private readonly array $routes) - { - } + public function __construct(private readonly array $routes) {} public function getRoutes(): array { diff --git a/src/Provider/FileRoutesProvider.php b/src/Provider/FileRoutesProvider.php index a05b16d7..c7363bd4 100644 --- a/src/Provider/FileRoutesProvider.php +++ b/src/Provider/FileRoutesProvider.php @@ -7,15 +7,21 @@ use Closure; use Yiisoft\Router\Group; use Yiisoft\Router\Route; +use CallbackFilterIterator; +use FilesystemIterator; +use RuntimeException; +use SplFileInfo; + +use function is_array; + +use const EXTR_SKIP; /** * A file provider provides routes from a file or directory of files. */ final class FileRoutesProvider implements RoutesProviderInterface { - public function __construct(private readonly string $file, private readonly array $scope = []) - { - } + public function __construct(private readonly string $file, private readonly array $scope = []) {} public function getRoutes(): array { @@ -28,27 +34,27 @@ public function getRoutes(): array return require $file; }, null); if (!file_exists($this->file)) { - throw new \RuntimeException( - 'Failed to provide routes from "' . $this->file . '". File or directory not found.' + throw new RuntimeException( + 'Failed to provide routes from "' . $this->file . '". File or directory not found.', ); } if (is_dir($this->file) && !is_file($this->file)) { $directoryRoutes = []; - $files = new \CallbackFilterIterator( - new \FilesystemIterator( + $files = new CallbackFilterIterator( + new FilesystemIterator( $this->file, - \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS + FilesystemIterator::SKIP_DOTS | FilesystemIterator::FOLLOW_SYMLINKS, ), - fn (\SplFileInfo $fileInfo) => $fileInfo->isFile() && $fileInfo->getExtension() === 'php' + fn(SplFileInfo $fileInfo) => $fileInfo->isFile() && $fileInfo->getExtension() === 'php', ); - /** @var \SplFileInfo[] $files */ + /** @var SplFileInfo[] $files */ foreach ($files as $file) { /** @var mixed $fileRoutes */ $fileRoutes = $scopeRequire($file->getRealPath(), $this->scope); if (is_array($fileRoutes) && $this->isRoutesAreValid($fileRoutes)) { array_push( $directoryRoutes, - ...$fileRoutes + ...$fileRoutes, ); } } diff --git a/src/Route.php b/src/Route.php index afd6554b..0c47d2c5 100644 --- a/src/Route.php +++ b/src/Route.php @@ -12,6 +12,9 @@ use function array_slice; use function count; use function in_array; +use function is_array; +use function is_callable; +use function is_string; /** * Route defines a mapping from URL to callback / name and vice versa. @@ -75,13 +78,52 @@ public function __construct( $this->middlewares = $middlewares; $this->methods = $methods; $this->hosts = $hosts; - $this->defaults = array_map('\strval', $defaults); + $this->defaults = array_map(\strval(...), $defaults); if (!empty($action)) { $this->middlewares[] = $action; $this->actionAdded = true; } } + public function __toString(): string + { + $result = $this->name === null + ? '' + : '[' . $this->name . '] '; + + if ($this->methods !== []) { + $result .= implode(',', $this->methods) . ' '; + } + + if (!empty($this->hosts)) { + $quoted = array_map(static fn($host) => preg_quote($host, '/'), $this->hosts); + + if (!preg_match('/' . implode('|', $quoted) . '/', $this->pattern)) { + $result .= implode('|', $this->hosts); + } + } + + $result .= $this->pattern; + + return $result; + } + + public function __debugInfo() + { + return [ + 'name' => $this->name, + 'methods' => $this->methods, + 'pattern' => $this->pattern, + 'hosts' => $this->hosts, + 'defaults' => $this->defaults, + 'override' => $this->override, + 'actionAdded' => $this->actionAdded, + 'middlewares' => $this->middlewares, + 'disabledMiddlewares' => $this->disabledMiddlewares, + 'enabledMiddlewares' => $this->getEnabledMiddlewares(), + ]; + } + public static function get(string $pattern): self { return self::methods([Method::GET], $pattern); @@ -302,45 +344,6 @@ public function getData(string $key): mixed }; } - public function __toString(): string - { - $result = $this->name === null - ? '' - : '[' . $this->name . '] '; - - if ($this->methods !== []) { - $result .= implode(',', $this->methods) . ' '; - } - - if (!empty($this->hosts)) { - $quoted = array_map(static fn ($host) => preg_quote($host, '/'), $this->hosts); - - if (!preg_match('/' . implode('|', $quoted) . '/', $this->pattern)) { - $result .= implode('|', $this->hosts); - } - } - - $result .= $this->pattern; - - return $result; - } - - public function __debugInfo() - { - return [ - 'name' => $this->name, - 'methods' => $this->methods, - 'pattern' => $this->pattern, - 'hosts' => $this->hosts, - 'defaults' => $this->defaults, - 'override' => $this->override, - 'actionAdded' => $this->actionAdded, - 'middlewares' => $this->middlewares, - 'disabledMiddlewares' => $this->disabledMiddlewares, - 'enabledMiddlewares' => $this->getEnabledMiddlewares(), - ]; - } - /** * @psalm-assert array $items */ @@ -369,7 +372,7 @@ private function assertMiddlewares(array $middlewareDefinitions): void } throw new InvalidArgumentException( - 'Invalid $middlewareDefinitions provided, list of string or array or callable expected.' + 'Invalid $middlewareDefinitions provided, list of string or array or callable expected.', ); } } diff --git a/src/RouteCollector.php b/src/RouteCollector.php index 04dd0cc2..0b47f86b 100644 --- a/src/RouteCollector.php +++ b/src/RouteCollector.php @@ -36,7 +36,7 @@ public function addProvider(RoutesProviderInterface ...$provider): RouteCollecto { array_push( $this->providers, - ...array_values($provider) + ...array_values($provider), ); return $this; } @@ -64,7 +64,7 @@ public function getItems(): array foreach ($this->providers as $provider) { array_push( $this->items, - ...$provider->getRoutes() + ...$provider->getRoutes(), ); } return $this->items; diff --git a/tests/GroupTest.php b/tests/GroupTest.php index b422d68e..f9c28259 100644 --- a/tests/GroupTest.php +++ b/tests/GroupTest.php @@ -23,6 +23,7 @@ use Yiisoft\Router\Tests\Support\TestMiddleware2; use Yiisoft\Router\Tests\Support\TestMiddleware3; use Yiisoft\Router\Tests\Support\TestController; +use stdClass; final class GroupTest extends TestCase { @@ -46,8 +47,8 @@ public function testInvalidMiddlewares(): void $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Invalid $middlewareDefinitions provided, list of string or array or callable expected.'); - $middleware = static fn () => new Response(); - $group = new Group('/api', [$middleware, new \stdClass()]); + $middleware = static fn() => new Response(); + $group = new Group('/api', [$middleware, new stdClass()]); } public function testDisabledMiddlewareDefinitions(): void @@ -493,7 +494,7 @@ public function testImmutability(): void public function testBuiltMiddlewares(): void { $group = Group::create() - ->middleware(static fn () => new Response(200)) + ->middleware(static fn() => new Response(200)) ->prependMiddleware(TestMiddleware1::class); $builtMiddlewareDefinitions = $group->getData('enabledMiddlewares'); diff --git a/tests/Provider/FileRoutesProviderTest.php b/tests/Provider/FileRoutesProviderTest.php index c346991f..0d869fd7 100644 --- a/tests/Provider/FileRoutesProviderTest.php +++ b/tests/Provider/FileRoutesProviderTest.php @@ -6,6 +6,9 @@ use PHPUnit\Framework\TestCase; use Yiisoft\Router\Provider\FileRoutesProvider; +use RuntimeException; + +use function dirname; class FileRoutesProviderTest extends TestCase { @@ -35,7 +38,7 @@ public function testGetRoutesInDirectory(): void public function testGetRoutesWithNotExistFile(): void { $file = __DIR__ . '/wrong.php'; - $this->expectException(\RuntimeException::class); + $this->expectException(RuntimeException::class); $this->expectExceptionMessage('Failed to provide routes from "' . $file . '". File or directory not found.'); $provider = new FileRoutesProvider($file); diff --git a/tests/RouteCollectorTest.php b/tests/RouteCollectorTest.php index 9e44453b..2491e79e 100644 --- a/tests/RouteCollectorTest.php +++ b/tests/RouteCollectorTest.php @@ -68,7 +68,7 @@ public function testAddProvider(): void $postGroup = Group::create('/post') ->routes( $listRoute, - $viewRoute + $viewRoute, ); $rootGroup = Group::create() @@ -76,13 +76,13 @@ public function testAddProvider(): void Group::create('/api') ->routes( $logoutRoute, - $postGroup + $postGroup, ), ); $testGroup = Group::create() ->routes( - Route::get('test/') + Route::get('test/'), ); $collector = new RouteCollector(); diff --git a/tests/RouteTest.php b/tests/RouteTest.php index 2eba0176..ac9ff9e1 100644 --- a/tests/RouteTest.php +++ b/tests/RouteTest.php @@ -46,7 +46,7 @@ public function testSimpleInstance(): void public function testEmptyMethods(): void { - $this->expectException(\InvalidArgumentException::class); + $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('$methods cannot be empty.'); new Route([], ''); @@ -280,10 +280,10 @@ public function testMiddlewareAfterAction(): void public function testInvalidMiddlewares(): void { - $this->expectException(\InvalidArgumentException::class); + $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Invalid $middlewareDefinitions provided, list of string or array or callable expected.'); - $route = new Route([Method::GET], '/', middlewares: [static fn () => new Response(), (object) ['test' => 1]]); + $route = new Route([Method::GET], '/', middlewares: [static fn() => new Response(), (object) ['test' => 1]]); } public function testDisabledMiddlewareDefinitions(): void @@ -478,7 +478,7 @@ public function testDuplicateHosts(): void public function testInvalidHosts(): void { - $this->expectException(\InvalidArgumentException::class); + $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Invalid $hosts provided, list of string expected.'); $route = new Route([Method::GET], '/', hosts: ['b.com', 123]); @@ -505,7 +505,7 @@ public function testBuiltMiddlewares(): void { $route = Route::get('') ->middleware(TestMiddleware1::class) - ->action(static fn () => new Response(200)); + ->action(static fn() => new Response(200)); $builtMiddlewareDefinitions = $route->getData('enabledMiddlewares'); From 00bf1559b6318791a9fab8e3a101999b98679311 Mon Sep 17 00:00:00 2001 From: Rustam Date: Mon, 16 Mar 2026 18:22:53 +0500 Subject: [PATCH 33/63] Fix: BC --- src/RouteCollectorInterface.php | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/RouteCollectorInterface.php b/src/RouteCollectorInterface.php index c64246d5..38c5861c 100644 --- a/src/RouteCollectorInterface.php +++ b/src/RouteCollectorInterface.php @@ -4,8 +4,6 @@ namespace Yiisoft\Router; -use Yiisoft\Router\Provider\RoutesProviderInterface; - interface RouteCollectorInterface { /** @@ -13,11 +11,6 @@ interface RouteCollectorInterface */ public function addRoute(Route|Group ...$routes): self; - /** - * Add a provider of routes - */ - public function addProvider(RoutesProviderInterface ...$provider): self; - /** * Appends a handler middleware definition that should be invoked for a matched route. * First added handler will be executed first. From 3326413daa2f6c3688b93d051b6f9430d950d967 Mon Sep 17 00:00:00 2001 From: Rustam Date: Sun, 22 Mar 2026 14:20:42 +0500 Subject: [PATCH 34/63] Add tests for default override and middleware behavior; enhance file route provider handling; increase MSI --- src/Provider/FileRoutesProvider.php | 2 + src/RouteCollection.php | 2 +- tests/Attribute/DeleteTest.php | 9 ++++ tests/Attribute/GetTest.php | 9 ++++ tests/Attribute/HeadTest.php | 9 ++++ tests/Attribute/OptionsTest.php | 9 ++++ tests/Attribute/PatchTest.php | 9 ++++ tests/Attribute/PostTest.php | 9 ++++ tests/Attribute/PutTest.php | 9 ++++ tests/Attribute/RouteTest.php | 9 ++++ tests/GroupTest.php | 15 ++++++ tests/Provider/FileRoutesProviderTest.php | 22 +++++++++ tests/RouteCollectorTest.php | 3 +- tests/RouteTest.php | 47 +++++++++++++++++-- .../resources/mixed_dir/not_routes.txt | 1 + .../resources/mixed_dir/valid_routes.php | 9 ++++ .../Support/resources/scope/scope_routes.php | 11 +++++ 17 files changed, 178 insertions(+), 6 deletions(-) create mode 100644 tests/Support/resources/mixed_dir/not_routes.txt create mode 100644 tests/Support/resources/mixed_dir/valid_routes.php create mode 100644 tests/Support/resources/scope/scope_routes.php diff --git a/src/Provider/FileRoutesProvider.php b/src/Provider/FileRoutesProvider.php index c7363bd4..2d97052b 100644 --- a/src/Provider/FileRoutesProvider.php +++ b/src/Provider/FileRoutesProvider.php @@ -38,11 +38,13 @@ public function getRoutes(): array 'Failed to provide routes from "' . $this->file . '". File or directory not found.', ); } + /** @infection-ignore-all Equivalent: is_dir implies !is_file for valid paths after file_exists check */ if (is_dir($this->file) && !is_file($this->file)) { $directoryRoutes = []; $files = new CallbackFilterIterator( new FilesystemIterator( $this->file, + /** @infection-ignore-all Bitwise flags; CallbackFilterIterator already filters by extension */ FilesystemIterator::SKIP_DOTS | FilesystemIterator::FOLLOW_SYMLINKS, ), fn(SplFileInfo $fileInfo) => $fileInfo->isFile() && $fileInfo->getExtension() === 'php', diff --git a/src/RouteCollection.php b/src/RouteCollection.php index ac456b0b..5af3ef6b 100644 --- a/src/RouteCollection.php +++ b/src/RouteCollection.php @@ -94,7 +94,7 @@ private function injectItem(Group|Route $route): void } /** - * Inject a Group instance into route and item arrays. + * Inject a Group instance into the route and the item arrays. * * @psalm-param Items $tree */ diff --git a/tests/Attribute/DeleteTest.php b/tests/Attribute/DeleteTest.php index 66acb374..52456137 100644 --- a/tests/Attribute/DeleteTest.php +++ b/tests/Attribute/DeleteTest.php @@ -20,6 +20,15 @@ public function testRoute(): void $this->assertSame([Method::DELETE], $route->getData('methods')); } + public function testOverrideDefaultIsFalse(): void + { + $attribute = new Delete('/'); + + $route = $attribute->getRoute(); + + $this->assertFalse($route->getData('override')); + } + public function testOverride(): void { $attribute = new Delete('/', override: true); diff --git a/tests/Attribute/GetTest.php b/tests/Attribute/GetTest.php index 855f1568..3441acfa 100644 --- a/tests/Attribute/GetTest.php +++ b/tests/Attribute/GetTest.php @@ -20,6 +20,15 @@ public function testRoute(): void $this->assertSame([Method::GET], $route->getData('methods')); } + public function testOverrideDefaultIsFalse(): void + { + $attribute = new Get('/'); + + $route = $attribute->getRoute(); + + $this->assertFalse($route->getData('override')); + } + public function testOverride(): void { $attribute = new Get('/', override: true); diff --git a/tests/Attribute/HeadTest.php b/tests/Attribute/HeadTest.php index 6e092f5f..956d6f30 100644 --- a/tests/Attribute/HeadTest.php +++ b/tests/Attribute/HeadTest.php @@ -20,6 +20,15 @@ public function testRoute(): void $this->assertSame([Method::HEAD], $route->getData('methods')); } + public function testOverrideDefaultIsFalse(): void + { + $attribute = new Head('/'); + + $route = $attribute->getRoute(); + + $this->assertFalse($route->getData('override')); + } + public function testOverride(): void { $attribute = new Head('/', override: true); diff --git a/tests/Attribute/OptionsTest.php b/tests/Attribute/OptionsTest.php index fa38d0e8..4cef38c5 100644 --- a/tests/Attribute/OptionsTest.php +++ b/tests/Attribute/OptionsTest.php @@ -20,6 +20,15 @@ public function testRoute(): void $this->assertSame([Method::OPTIONS], $route->getData('methods')); } + public function testOverrideDefaultIsFalse(): void + { + $attribute = new Options('/'); + + $route = $attribute->getRoute(); + + $this->assertFalse($route->getData('override')); + } + public function testOverride(): void { $attribute = new Options('/', override: true); diff --git a/tests/Attribute/PatchTest.php b/tests/Attribute/PatchTest.php index 37121a26..130f7c0c 100644 --- a/tests/Attribute/PatchTest.php +++ b/tests/Attribute/PatchTest.php @@ -20,6 +20,15 @@ public function testRoute(): void $this->assertSame([Method::PATCH], $route->getData('methods')); } + public function testOverrideDefaultIsFalse(): void + { + $attribute = new Patch('/'); + + $route = $attribute->getRoute(); + + $this->assertFalse($route->getData('override')); + } + public function testOverride(): void { $attribute = new Patch('/', override: true); diff --git a/tests/Attribute/PostTest.php b/tests/Attribute/PostTest.php index 87f3c9bb..cfefedae 100644 --- a/tests/Attribute/PostTest.php +++ b/tests/Attribute/PostTest.php @@ -20,6 +20,15 @@ public function testRoute(): void $this->assertSame([Method::POST], $route->getData('methods')); } + public function testOverrideDefaultIsFalse(): void + { + $attribute = new Post('/'); + + $route = $attribute->getRoute(); + + $this->assertFalse($route->getData('override')); + } + public function testOverride(): void { $attribute = new Post('/', override: true); diff --git a/tests/Attribute/PutTest.php b/tests/Attribute/PutTest.php index 4c6ebd14..d8ee1530 100644 --- a/tests/Attribute/PutTest.php +++ b/tests/Attribute/PutTest.php @@ -20,6 +20,15 @@ public function testRoute(): void $this->assertSame([Method::PUT], $route->getData('methods')); } + public function testOverrideDefaultIsFalse(): void + { + $attribute = new Put('/'); + + $route = $attribute->getRoute(); + + $this->assertFalse($route->getData('override')); + } + public function testOverride(): void { $attribute = new Put('/', override: true); diff --git a/tests/Attribute/RouteTest.php b/tests/Attribute/RouteTest.php index 930e9164..b566674e 100644 --- a/tests/Attribute/RouteTest.php +++ b/tests/Attribute/RouteTest.php @@ -20,6 +20,15 @@ public function testRoute(): void $this->assertSame([Method::GET, Method::HEAD], $route->getData('methods')); } + public function testOverrideDefaultIsFalse(): void + { + $attribute = new Route([Method::GET, Method::HEAD], '/'); + + $route = $attribute->getRoute(); + + $this->assertFalse($route->getData('override')); + } + public function testOverride(): void { $attribute = new Route([Method::GET, Method::HEAD], '/', override: true); diff --git a/tests/GroupTest.php b/tests/GroupTest.php index f9c28259..36708351 100644 --- a/tests/GroupTest.php +++ b/tests/GroupTest.php @@ -502,6 +502,21 @@ public function testBuiltMiddlewares(): void $this->assertSame($builtMiddlewareDefinitions, $group->getData('enabledMiddlewares')); } + public function testValidHostsInConstructor(): void + { + $group = new Group(hosts: ['example.com', 'test.com']); + + $this->assertSame(['example.com', 'test.com'], $group->getData('hosts')); + } + + public function testValidMiddlewaresInConstructor(): void + { + $callable = static fn() => new Response(); + $group = new Group(middlewares: ['SomeClass', $callable, ['Class', 'method']]); + + $this->assertCount(3, $group->getData('enabledMiddlewares')); + } + private function getRequestHandler(): RequestHandlerInterface { return new class implements RequestHandlerInterface { diff --git a/tests/Provider/FileRoutesProviderTest.php b/tests/Provider/FileRoutesProviderTest.php index 0d869fd7..ae61c9f2 100644 --- a/tests/Provider/FileRoutesProviderTest.php +++ b/tests/Provider/FileRoutesProviderTest.php @@ -53,4 +53,26 @@ public function testGetRoutesWithEmptyRoutes(): void $this->assertEmpty($provider->getRoutes()); } + + public function testGetRoutesWithScope(): void + { + $file = dirname(__DIR__) . '/Support/resources/scope/scope_routes.php'; + + $provider = new FileRoutesProvider($file, ['prefix' => '/api']); + $routes = $provider->getRoutes(); + + $this->assertCount(1, $routes); + $this->assertSame('/api/test', $routes[0]->getData('pattern')); + } + + public function testGetRoutesInDirectoryWithNonPhpFiles(): void + { + $dir = dirname(__DIR__) . '/Support/resources/mixed_dir'; + + $provider = new FileRoutesProvider($dir); + $routes = $provider->getRoutes(); + + $this->assertCount(1, $routes); + $this->assertSame('/mixed', $routes[0]->getData('pattern')); + } } diff --git a/tests/RouteCollectorTest.php b/tests/RouteCollectorTest.php index 2491e79e..84825e2f 100644 --- a/tests/RouteCollectorTest.php +++ b/tests/RouteCollectorTest.php @@ -8,6 +8,7 @@ use PHPUnit\Framework\TestCase; use Yiisoft\Router\Group; use Yiisoft\Router\Provider\ArrayRoutesProvider; +use Yiisoft\Router\Provider\FileRoutesProvider; use Yiisoft\Router\Route; use Yiisoft\Router\RouteCollector; @@ -86,7 +87,7 @@ public function testAddProvider(): void ); $collector = new RouteCollector(); - $collector->addProvider(new ArrayRoutesProvider([$rootGroup, $postGroup, $testGroup])); + $collector->addProvider(new ArrayRoutesProvider([$rootGroup, $postGroup, $testGroup]), file: new FileRoutesProvider(__DIR__ . '/Support/resources/foo.php')); $this->assertCount(3, $collector->getItems()); $this->assertContainsOnlyInstancesOf(Group::class, $collector->getItems()); diff --git a/tests/RouteTest.php b/tests/RouteTest.php index ac9ff9e1..ff1bde62 100644 --- a/tests/RouteTest.php +++ b/tests/RouteTest.php @@ -19,6 +19,7 @@ use Yiisoft\Router\Route; use Yiisoft\Router\Tests\Support\AssertTrait; use Yiisoft\Router\Tests\Support\Container; +use Yiisoft\Router\Tests\Support\CustomResponseMiddleware; use Yiisoft\Router\Tests\Support\TestController; use Yiisoft\Router\Tests\Support\TestMiddleware1; use Yiisoft\Router\Tests\Support\TestMiddleware2; @@ -35,12 +36,12 @@ public function testSimpleInstance(): void methods: [Method::GET], pattern: '/', action: [TestController::class, 'index'], - middlewares: [TestMiddleware1::class], + middlewares: [TestMiddleware1::class, fn() => new Response(), TestMiddleware2::class], override: true, ); $this->assertInstanceOf(Route::class, $route); - $this->assertCount(2, $route->getData('enabledMiddlewares')); + $this->assertCount(4, $route->getData('enabledMiddlewares')); $this->assertTrue($route->getData('override')); } @@ -278,6 +279,44 @@ public function testMiddlewareAfterAction(): void ); } + public function testDefaultsConvertedToStringInConstructor(): void + { + $route = new Route( + methods: [Method::GET], + pattern: '/{language}', + defaults: ['language' => 'en', 'age' => 42], + ); + + $this->assertSame([ + 'language' => 'en', + 'age' => '42', + ], $route->getData('defaults')); + } + + public function testActionAddedViaConstructorMiddlewareInsertedBefore(): void + { + $route = new Route( + methods: [Method::GET], + pattern: '/', + action: [TestController::class, 'index'], + ); + + $route = $route->middleware(TestMiddleware1::class); + + $this->assertSame( + [TestMiddleware1::class, [TestController::class, 'index']], + $route->getData('enabledMiddlewares'), + ); + } + + public function testInvalidMiddlewareAfterString(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid $middlewareDefinitions provided, list of string or array or callable expected.'); + + new Route([Method::GET], '/', middlewares: ['ValidString', (object) ['test' => 1]]); + } + public function testInvalidMiddlewares(): void { $this->expectException(InvalidArgumentException::class); @@ -398,13 +437,13 @@ public function testGetEnabledMiddlewaresTwice(): void public function testMiddlewaresWithKeys(): void { $route = Route::get('/') - ->middleware(m3: TestMiddleware3::class) + ->middleware(m3: TestMiddleware3::class, custom: $custom = ['class' => CustomResponseMiddleware::class, '__construct()' => ['code' => 500]]) ->action([TestController::class, 'index']) ->prependMiddleware(m1: TestMiddleware1::class, m2: TestMiddleware2::class) ->disableMiddleware(m1: TestMiddleware1::class); $this->assertSame( - [TestMiddleware2::class, TestMiddleware3::class, [TestController::class, 'index']], + [TestMiddleware2::class, TestMiddleware3::class, $custom, [TestController::class, 'index']], $route->getData('enabledMiddlewares'), ); } diff --git a/tests/Support/resources/mixed_dir/not_routes.txt b/tests/Support/resources/mixed_dir/not_routes.txt new file mode 100644 index 00000000..9fd86c9a --- /dev/null +++ b/tests/Support/resources/mixed_dir/not_routes.txt @@ -0,0 +1 @@ +This is not a PHP file and should not be included. diff --git a/tests/Support/resources/mixed_dir/valid_routes.php b/tests/Support/resources/mixed_dir/valid_routes.php new file mode 100644 index 00000000..ddf62370 --- /dev/null +++ b/tests/Support/resources/mixed_dir/valid_routes.php @@ -0,0 +1,9 @@ + Date: Tue, 24 Mar 2026 21:43:27 +0500 Subject: [PATCH 35/63] Refactor: remove unused properties in Group; add return type to middleware callable in GroupTest --- src/Group.php | 2 -- tests/GroupTest.php | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Group.php b/src/Group.php index 0e9cbc82..64616992 100644 --- a/src/Group.php +++ b/src/Group.php @@ -31,8 +31,6 @@ final class Group * @var string[] */ private array $hosts = []; - private bool $routesAdded = false; - private bool $middlewareAdded = false; /** * @psalm-var list|null diff --git a/tests/GroupTest.php b/tests/GroupTest.php index 36708351..c33060c7 100644 --- a/tests/GroupTest.php +++ b/tests/GroupTest.php @@ -511,7 +511,7 @@ public function testValidHostsInConstructor(): void public function testValidMiddlewaresInConstructor(): void { - $callable = static fn() => new Response(); + $callable = static fn(): ResponseInterface => new Response(); $group = new Group(middlewares: ['SomeClass', $callable, ['Class', 'method']]); $this->assertCount(3, $group->getData('enabledMiddlewares')); From 302fae54308ab2dbb23045e36f8f6cacfaf60ec6 Mon Sep 17 00:00:00 2001 From: Rustam Date: Thu, 26 Mar 2026 18:35:59 +0500 Subject: [PATCH 36/63] Refactor: extract host normalization logic to a shared private method; improve handling of route providers and test cases --- src/Group.php | 30 +++++++++++++++++-------- src/Provider/FileRoutesProvider.php | 15 ++++++++----- src/Route.php | 32 +++++++++++++++++---------- src/RouteCollector.php | 34 ++++++++++++++++------------- tests/RouteCollectorTest.php | 16 +++++++++++--- 5 files changed, 84 insertions(+), 43 deletions(-) diff --git a/src/Group.php b/src/Group.php index 64616992..be63940f 100644 --- a/src/Group.php +++ b/src/Group.php @@ -63,7 +63,7 @@ public function __construct( $this->assertMiddlewares($middlewares); $this->assertHosts($hosts); $this->middlewares = $middlewares; - $this->hosts = $hosts; + $this->hosts = $this->normalizeHosts($hosts); $this->corsMiddleware = $corsMiddleware; } @@ -148,14 +148,7 @@ public function host(string $host): self public function hosts(string ...$hosts): self { $new = clone $this; - - foreach ($hosts as $host) { - $host = rtrim($host, '/'); - - if ($host !== '' && !in_array($host, $new->hosts, true)) { - $new->hosts[] = $host; - } - } + $new->hosts = $this->normalizeHosts($hosts); return $new; } @@ -252,4 +245,23 @@ private function getEnabledMiddlewares(): array return $this->enabledMiddlewaresCache; } + + /** + * @param string[] $hosts + * + * @return array + */ + private function normalizeHosts(array $hosts): array + { + $normalizedHosts = []; + foreach ($hosts as $host) { + $host = rtrim($host, '/'); + + if ($host !== '' && !in_array($host, $normalizedHosts, true)) { + $normalizedHosts[] = $host; + } + } + + return $normalizedHosts; + } } diff --git a/src/Provider/FileRoutesProvider.php b/src/Provider/FileRoutesProvider.php index 2d97052b..beda868e 100644 --- a/src/Provider/FileRoutesProvider.php +++ b/src/Provider/FileRoutesProvider.php @@ -13,6 +13,7 @@ use SplFileInfo; use function is_array; +use function iterator_to_array; use const EXTR_SKIP; @@ -45,14 +46,20 @@ public function getRoutes(): array new FilesystemIterator( $this->file, /** @infection-ignore-all Bitwise flags; CallbackFilterIterator already filters by extension */ - FilesystemIterator::SKIP_DOTS | FilesystemIterator::FOLLOW_SYMLINKS, + FilesystemIterator::SKIP_DOTS, ), fn(SplFileInfo $fileInfo) => $fileInfo->isFile() && $fileInfo->getExtension() === 'php', ); + $files = iterator_to_array($files, false); /** @var SplFileInfo[] $files */ + usort($files, static fn(SplFileInfo $a, SplFileInfo $b) => $a->getFilename() <=> $b->getFilename()); foreach ($files as $file) { + $realPath = $file->getRealPath(); + if ($realPath === false) { + continue; + } /** @var mixed $fileRoutes */ - $fileRoutes = $scopeRequire($file->getRealPath(), $this->scope); + $fileRoutes = $scopeRequire($realPath, $this->scope); if (is_array($fileRoutes) && $this->isRoutesAreValid($fileRoutes)) { array_push( $directoryRoutes, @@ -78,9 +85,7 @@ public function getRoutes(): array private function isRoutesAreValid(array $routes): bool { foreach ($routes as $route) { - if ( - !is_a($route, Route::class, true) && !is_a($route, Group::class, true) - ) { + if (!$route instanceof Route && !$route instanceof Group) { return false; } } diff --git a/src/Route.php b/src/Route.php index fd5b667a..5faa63f5 100644 --- a/src/Route.php +++ b/src/Route.php @@ -77,9 +77,9 @@ public function __construct( $this->assertListOfStrings($hosts, 'hosts'); $this->middlewares = $middlewares; $this->methods = $methods; - $this->hosts = $hosts; + $this->hosts = $this->normalizeHosts($hosts); $this->defaults = array_map(\strval(...), $defaults); - if (!empty($action)) { + if ($action !== null) { $this->middlewares[] = $action; $this->actionAdded = true; } @@ -189,15 +189,7 @@ public function host(string $host): self public function hosts(string ...$hosts): self { $route = clone $this; - $route->hosts = []; - - foreach ($hosts as $host) { - $host = rtrim($host, '/'); - - if ($host !== '' && !in_array($host, $route->hosts, true)) { - $route->hosts[] = $host; - } - } + $route->hosts = $this->normalizeHosts($hosts); return $route; } @@ -348,6 +340,24 @@ public function getData(string $key): mixed }; } + /** + * @param string[] $hosts + * + * @return array + */ + private function normalizeHosts(array $hosts): array + { + $normalizedHosts = []; + foreach ($hosts as $host) { + $host = rtrim($host, '/'); + + if ($host !== '' && !in_array($host, $normalizedHosts, true)) { + $normalizedHosts[] = $host; + } + } + return $normalizedHosts; + } + /** * @psalm-assert array $items */ diff --git a/src/RouteCollector.php b/src/RouteCollector.php index 0b47f86b..42b860a2 100644 --- a/src/RouteCollector.php +++ b/src/RouteCollector.php @@ -16,27 +16,28 @@ final class RouteCollector implements RouteCollectorInterface /** * @var RoutesProviderInterface[] */ - private array $providers = []; + private array $providers; /** * @var array[]|callable[]|string[] */ private array $middlewareDefinitions = []; - public function addRoute(Route|Group ...$routes): RouteCollectorInterface + private bool $providersAreInjected = false; + + /** + * @param RoutesProviderInterface[] $providers + */ + public function __construct(array $providers = []) { - array_push( - $this->items, - ...array_values($routes), - ); - return $this; + $this->providers = $providers; } - public function addProvider(RoutesProviderInterface ...$provider): RouteCollectorInterface + public function addRoute(Route|Group ...$routes): RouteCollectorInterface { array_push( - $this->providers, - ...array_values($provider), + $this->items, + ...array_values($routes), ); return $this; } @@ -61,11 +62,14 @@ public function prependMiddleware(array|callable|string ...$middlewareDefinition public function getItems(): array { - foreach ($this->providers as $provider) { - array_push( - $this->items, - ...$provider->getRoutes(), - ); + if (!$this->providersAreInjected) { + foreach ($this->providers as $provider) { + array_push( + $this->items, + ...$provider->getRoutes(), + ); + } + $this->providersAreInjected = true; } return $this->items; } diff --git a/tests/RouteCollectorTest.php b/tests/RouteCollectorTest.php index 84825e2f..f89ddee7 100644 --- a/tests/RouteCollectorTest.php +++ b/tests/RouteCollectorTest.php @@ -61,7 +61,7 @@ public function testAddGroup(): void $this->assertContainsOnlyInstancesOf(Group::class, $collector->getItems()); } - public function testAddProvider(): void + public function testWithProvider(): void { $logoutRoute = Route::post('/logout'); $listRoute = Route::get('/'); @@ -86,13 +86,23 @@ public function testAddProvider(): void Route::get('test/'), ); - $collector = new RouteCollector(); - $collector->addProvider(new ArrayRoutesProvider([$rootGroup, $postGroup, $testGroup]), file: new FileRoutesProvider(__DIR__ . '/Support/resources/foo.php')); + $collector = new RouteCollector([new ArrayRoutesProvider([$rootGroup, $postGroup, $testGroup]), new FileRoutesProvider(__DIR__ . '/Support/resources/foo.php')]); $this->assertCount(3, $collector->getItems()); $this->assertContainsOnlyInstancesOf(Group::class, $collector->getItems()); } + public function testEnsureProvidersCollectedOnce(): void + { + $collector = new RouteCollector([new ArrayRoutesProvider([Route::get('/')])]); + $collector->addRoute(Route::get('/test')); + $this->assertCount(2, $collector->getItems()); + $this->assertContainsOnlyInstancesOf(Route::class, $collector->getItems()); + + $collector->addRoute(Route::get('/test2')); + $this->assertCount(3, $collector->getItems()); + } + public function testAddMiddleware(): void { $collector = new RouteCollector(); From f38b6dad4632913c3d4e77ddee9431df4a2846a1 Mon Sep 17 00:00:00 2001 From: rustamwin <16498265+rustamwin@users.noreply.github.com> Date: Thu, 26 Mar 2026 13:36:55 +0000 Subject: [PATCH 37/63] Apply PHP CS Fixer and Rector changes (CI) --- src/RouteCollector.php | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/RouteCollector.php b/src/RouteCollector.php index 42b860a2..473006b9 100644 --- a/src/RouteCollector.php +++ b/src/RouteCollector.php @@ -13,11 +13,6 @@ final class RouteCollector implements RouteCollectorInterface */ private array $items = []; - /** - * @var RoutesProviderInterface[] - */ - private array $providers; - /** * @var array[]|callable[]|string[] */ @@ -28,9 +23,8 @@ final class RouteCollector implements RouteCollectorInterface /** * @param RoutesProviderInterface[] $providers */ - public function __construct(array $providers = []) + public function __construct(private readonly array $providers = []) { - $this->providers = $providers; } public function addRoute(Route|Group ...$routes): RouteCollectorInterface From b7f262f0a3e3e67670fec3074771adbf1ba9aa8c Mon Sep 17 00:00:00 2001 From: samdark <47294+samdark@users.noreply.github.com> Date: Thu, 26 Mar 2026 13:37:27 +0000 Subject: [PATCH 38/63] Apply PHP CS Fixer and Rector changes (CI) --- src/RouteCollector.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/RouteCollector.php b/src/RouteCollector.php index 473006b9..c44479ba 100644 --- a/src/RouteCollector.php +++ b/src/RouteCollector.php @@ -23,9 +23,7 @@ final class RouteCollector implements RouteCollectorInterface /** * @param RoutesProviderInterface[] $providers */ - public function __construct(private readonly array $providers = []) - { - } + public function __construct(private readonly array $providers = []) {} public function addRoute(Route|Group ...$routes): RouteCollectorInterface { From bb2a33344df9d854ed2a3b4d6edd9b43731dae08 Mon Sep 17 00:00:00 2001 From: Rustam Date: Thu, 26 Mar 2026 22:52:27 +0500 Subject: [PATCH 39/63] Update CHANGELOG: consolidate duplicate entries for #196 --- CHANGELOG.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b61aa43..b6bbff2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## 4.0.3 under development +- New #196: Add PHP Attributes support (@rustamwin) +- New #196: Add `RoutesProviderInterface` interface providing routes from various resources (@rustamwin) - Enh #276: Explicitly import classes, functions, and constants in the "use" section (@rustamwin) - Enh #277, #281: Remove restrictions from `prependMiddleware()` and `middleware()` methods (@klsoft-web, @vjik) @@ -33,12 +35,6 @@ - New #203, #237: Add `RouteArgument` attribute for Yii Hydrator (@vjik) - Enh #202: Add support for `psr/http-message` version `^2.0` (@vjik) -- New #196: Add PHP Attributes support (@rustamwin) -- New #196: Add `RoutesProviderInterface` interface providing routes from various resources (@rustamwin) -- Enh #196: The `Group` and `Route` classes have been refactored to be DTO objects & dispatcher-independent. (@rustamwin) -- Enh #196: The `MatchingResult` class has been improved to be dispatcher-independent (@rustamwin) -- Chg #196: The implementation of `MatchingResult` from `MiddlewareInterface` has been removed, so - it is no longer middleware. (@rustamwin) ## 3.0.0 February 17, 2023 From 0b981f1b4716ec6d6a1528c30fcbe99ca3be2100 Mon Sep 17 00:00:00 2001 From: Rustam Date: Thu, 2 Apr 2026 21:20:43 +0500 Subject: [PATCH 40/63] Refactor: improve PHPDoc for route attributes and update validation method names in Group and FileRoutesProvider --- src/Attribute/Delete.php | 7 ++++--- src/Attribute/Get.php | 5 +++-- src/Attribute/Head.php | 7 ++++--- src/Attribute/Options.php | 7 ++++--- src/Attribute/Patch.php | 7 ++++--- src/Attribute/Post.php | 7 ++++--- src/Attribute/Put.php | 5 +++-- src/Attribute/Route.php | 10 +++++----- src/Group.php | 10 +++++----- src/Provider/FileRoutesProvider.php | 6 +++--- src/Provider/RoutesProviderInterface.php | 2 +- tests/Support/TestController.php | 2 -- 12 files changed, 40 insertions(+), 35 deletions(-) diff --git a/src/Attribute/Delete.php b/src/Attribute/Delete.php index 4121b10a..443a3812 100644 --- a/src/Attribute/Delete.php +++ b/src/Attribute/Delete.php @@ -15,11 +15,12 @@ final class Delete implements RouteAttributeInterface private readonly Route $route; /** - * @param string[] $hosts Hosts that the route should match. - * @param array[]|callable[]|string[] $middlewares Middlewares to be added to the route. + * @param string $pattern Route pattern. * @param string|null $name Route name. If not set, it will be generated automatically. + * @param array[]|callable[]|string[] $middlewares Middlewares to be added to the route. * @param array $defaults Parameter default values indexed by parameter names. - * @param bool $override Marks route as override. When added it will replace existing route with the same name. + * @param string[] $hosts Hosts that the route should match. + * @param bool $override Marks route as override. When added, it will replace the existing route with the same name. * @param array $disabledMiddlewares Excludes middleware from being invoked when action is handled. * It is useful to avoid invoking one of the parent group middleware for * a certain route. diff --git a/src/Attribute/Get.php b/src/Attribute/Get.php index 351e258c..8fe2ed84 100644 --- a/src/Attribute/Get.php +++ b/src/Attribute/Get.php @@ -15,10 +15,11 @@ final class Get implements RouteAttributeInterface private readonly Route $route; /** - * @param string[] $hosts Hosts that the route should match. - * @param array[]|callable[]|string[] $middlewares Middlewares to be added to the route. + * @param string $pattern Route pattern. * @param string|null $name Route name. If not set, it will be generated automatically. + * @param array[]|callable[]|string[] $middlewares Middlewares to be added to the route. * @param array $defaults Parameter default values indexed by parameter names. + * @param string[] $hosts Hosts that the route should match. * @param bool $override Marks route as override. When added it will replace existing route with the same name. * @param array $disabledMiddlewares Excludes middleware from being invoked when action is handled. * It is useful to avoid invoking one of the parent group middleware for diff --git a/src/Attribute/Head.php b/src/Attribute/Head.php index 5885bcfc..834b87a5 100644 --- a/src/Attribute/Head.php +++ b/src/Attribute/Head.php @@ -15,11 +15,12 @@ final class Head implements RouteAttributeInterface private readonly Route $route; /** - * @param string[] $hosts Hosts that the route should match. - * @param array[]|callable[]|string[] $middlewares Middlewares to be added to the route. + * @param string $pattern Route pattern. * @param string|null $name Route name. If not set, it will be generated automatically. + * @param array[]|callable[]|string[] $middlewares Middlewares to be added to the route. * @param array $defaults Parameter default values indexed by parameter names. - * @param bool $override Marks route as override. When added it will replace existing route with the same name. + * @param string[] $hosts Hosts that the route should match. + * @param bool $override Marks route as override. When added, it will replace the existing route with the same name. * @param array $disabledMiddlewares Excludes middleware from being invoked when action is handled. * It is useful to avoid invoking one of the parent group middleware for * a certain route. diff --git a/src/Attribute/Options.php b/src/Attribute/Options.php index e50e364c..e62c71f3 100644 --- a/src/Attribute/Options.php +++ b/src/Attribute/Options.php @@ -15,11 +15,12 @@ final class Options implements RouteAttributeInterface private readonly Route $route; /** - * @param string[] $hosts Hosts that the route should match. - * @param array[]|callable[]|string[] $middlewares Middlewares to be added to the route. + * @param string $pattern Route pattern. * @param string|null $name Route name. If not set, it will be generated automatically. + * @param array[]|callable[]|string[] $middlewares Middlewares to be added to the route. * @param array $defaults Parameter default values indexed by parameter names. - * @param bool $override Marks route as override. When added it will replace existing route with the same name. + * @param string[] $hosts Hosts that the route should match. + * @param bool $override Marks route as override. When added, it will replace the existing route with the same name. * @param array $disabledMiddlewares Excludes middleware from being invoked when action is handled. * It is useful to avoid invoking one of the parent group middleware for * a certain route. diff --git a/src/Attribute/Patch.php b/src/Attribute/Patch.php index ac9b62bc..9af032c3 100644 --- a/src/Attribute/Patch.php +++ b/src/Attribute/Patch.php @@ -15,11 +15,12 @@ final class Patch implements RouteAttributeInterface private readonly Route $route; /** - * @param string[] $hosts Hosts that the route should match. - * @param array[]|callable[]|string[] $middlewares Middlewares to be added to the route. + * @param string $pattern Route pattern. * @param string|null $name Route name. If not set, it will be generated automatically. + * @param array[]|callable[]|string[] $middlewares Middlewares to be added to the route. * @param array $defaults Parameter default values indexed by parameter names. - * @param bool $override Marks route as override. When added it will replace existing route with the same name. + * @param string[] $hosts Hosts that the route should match. + * @param bool $override Marks route as override. When added, it will replace the existing route with the same name. * @param array $disabledMiddlewares Excludes middleware from being invoked when action is handled. * It is useful to avoid invoking one of the parent group middleware for * a certain route. diff --git a/src/Attribute/Post.php b/src/Attribute/Post.php index be5a7757..8f8bda6e 100644 --- a/src/Attribute/Post.php +++ b/src/Attribute/Post.php @@ -15,11 +15,12 @@ final class Post implements RouteAttributeInterface private readonly Route $route; /** - * @param string[] $hosts Hosts that the route should match. - * @param array[]|callable[]|string[] $middlewares Middlewares to be added to the route. + * @param string $pattern Route pattern. * @param string|null $name Route name. If not set, it will be generated automatically. + * @param array[]|callable[]|string[] $middlewares Middlewares to be added to the route. * @param array $defaults Parameter default values indexed by parameter names. - * @param bool $override Marks route as override. When added it will replace existing route with the same name. + * @param string[] $hosts Hosts that the route should match. + * @param bool $override Marks route as override. When added, it will replace the existing route with the same name. * @param array $disabledMiddlewares Excludes middleware from being invoked when action is handled. * It is useful to avoid invoking one of the parent group middleware for * a certain route. diff --git a/src/Attribute/Put.php b/src/Attribute/Put.php index 5c3b3be7..88ea1e91 100644 --- a/src/Attribute/Put.php +++ b/src/Attribute/Put.php @@ -15,10 +15,11 @@ final class Put implements RouteAttributeInterface private readonly Route $route; /** - * @param string[] $hosts Hosts that the route should match. - * @param array[]|callable[]|string[] $middlewares Middlewares to be added to the route. + * @param string $pattern Route pattern. * @param string|null $name Route name. If not set, it will be generated automatically. + * @param array[]|callable[]|string[] $middlewares Middlewares to be added to the route. * @param array $defaults Parameter default values indexed by parameter names. + * @param string[] $hosts Hosts that the route should match. * @param bool $override Marks route as override. When added it will replace existing route with the same name. * @param array $disabledMiddlewares Excludes middleware from being invoked when action is handled. * It is useful to avoid invoking one of the parent group middleware for diff --git a/src/Attribute/Route.php b/src/Attribute/Route.php index 8dd50fef..3abea6cd 100644 --- a/src/Attribute/Route.php +++ b/src/Attribute/Route.php @@ -11,16 +11,14 @@ #[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] final class Route implements RouteAttributeInterface { - private readonly RouteObject $route; - /** * @param string[] $methods HTTP methods that the route should match. * @param string $pattern Route pattern. * @param string|null $name Route name. If not set, it will be generated automatically. - * @param array $middlewares Middlewares to be added to the route. - * @param string[] $hosts Hosts that the route should match. + * @param array[]|callable[]|string[] $middlewares Middlewares to be added to the route. * @param array $defaults Parameter default values indexed by parameter names. - * @param bool $override Marks route as override. When added it will replace existing route with the same name. + * @param string[] $hosts Hosts that the route should match. + * @param bool $override Marks route as override. When added, it will replace the existing route with the same name. * @param array $disabledMiddlewares Excludes middleware from being invoked when action is handled. * It is useful to avoid invoking one of the parent group middleware for * a certain route. @@ -49,6 +47,8 @@ public function __construct( ); } + private readonly RouteObject $route; + public function getRoute(): RouteObject { return $this->route; diff --git a/src/Group.php b/src/Group.php index be63940f..7717bdef 100644 --- a/src/Group.php +++ b/src/Group.php @@ -53,15 +53,15 @@ final class Group * @psalm-param list $middlewares */ public function __construct( - private ?string $prefix = null, + private readonly ?string $prefix = null, array $middlewares = [], array $hosts = [], private ?string $namePrefix = null, private array $disabledMiddlewares = [], array|callable|string|null $corsMiddleware = null, ) { - $this->assertMiddlewares($middlewares); - $this->assertHosts($hosts); + $this->assertMiddlewaresValid($middlewares); + $this->assertHostsValid($hosts); $this->middlewares = $middlewares; $this->hosts = $this->normalizeHosts($hosts); $this->corsMiddleware = $corsMiddleware; @@ -204,7 +204,7 @@ public function getData(string $key): mixed }; } - private function assertHosts(array $hosts): void + private function assertHostsValid(array $hosts): void { foreach ($hosts as $host) { if (!is_string($host)) { @@ -216,7 +216,7 @@ private function assertHosts(array $hosts): void /** * @psalm-assert array $middlewareDefinitions */ - private function assertMiddlewares(array $middlewareDefinitions): void + private function assertMiddlewaresValid(array $middlewareDefinitions): void { /** @var mixed $middlewareDefinition */ foreach ($middlewareDefinitions as $middlewareDefinition) { diff --git a/src/Provider/FileRoutesProvider.php b/src/Provider/FileRoutesProvider.php index beda868e..173fce43 100644 --- a/src/Provider/FileRoutesProvider.php +++ b/src/Provider/FileRoutesProvider.php @@ -60,7 +60,7 @@ public function getRoutes(): array } /** @var mixed $fileRoutes */ $fileRoutes = $scopeRequire($realPath, $this->scope); - if (is_array($fileRoutes) && $this->isRoutesAreValid($fileRoutes)) { + if (is_array($fileRoutes) && $this->areRoutesValid($fileRoutes)) { array_push( $directoryRoutes, ...$fileRoutes, @@ -72,7 +72,7 @@ public function getRoutes(): array /** @var mixed $routes */ $routes = $scopeRequire($this->file, $this->scope); - if (is_array($routes) && $this->isRoutesAreValid($routes)) { + if (is_array($routes) && $this->areRoutesValid($routes)) { return $routes; } @@ -82,7 +82,7 @@ public function getRoutes(): array /** * @psalm-assert-if-true Route[]|Group[] $routes */ - private function isRoutesAreValid(array $routes): bool + private function areRoutesValid(array $routes): bool { foreach ($routes as $route) { if (!$route instanceof Route && !$route instanceof Group) { diff --git a/src/Provider/RoutesProviderInterface.php b/src/Provider/RoutesProviderInterface.php index 60dc2f3e..16b61986 100644 --- a/src/Provider/RoutesProviderInterface.php +++ b/src/Provider/RoutesProviderInterface.php @@ -8,7 +8,7 @@ use Yiisoft\Router\Route; /** - * `RoutesProviderInterface` provides routes. + * `RoutesProviderInterface` provides routes to route collector. */ interface RoutesProviderInterface { diff --git a/tests/Support/TestController.php b/tests/Support/TestController.php index 57147cfa..c39616bf 100644 --- a/tests/Support/TestController.php +++ b/tests/Support/TestController.php @@ -10,7 +10,6 @@ use Yiisoft\Router\Attribute\Get; use Yiisoft\Router\Group; -#[Group('/test')] final class TestController { public function index(ServerRequestInterface $request): ResponseInterface @@ -18,7 +17,6 @@ public function index(ServerRequestInterface $request): ResponseInterface return new Response(200, [], $request->getAttribute('content', '')); } - #[Get('/')] public function attributeAction(): Response { return new Response(200, [], 'test'); From 4ec6f08fce8f4f7fd9c9409daa1e6a266214330f Mon Sep 17 00:00:00 2001 From: rustamwin <16498265+rustamwin@users.noreply.github.com> Date: Thu, 2 Apr 2026 16:22:19 +0000 Subject: [PATCH 41/63] Apply PHP CS Fixer and Rector changes (CI) --- src/Attribute/Route.php | 4 ++-- tests/Support/TestController.php | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Attribute/Route.php b/src/Attribute/Route.php index 3abea6cd..cccdb14b 100644 --- a/src/Attribute/Route.php +++ b/src/Attribute/Route.php @@ -11,6 +11,8 @@ #[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] final class Route implements RouteAttributeInterface { + private readonly RouteObject $route; + /** * @param string[] $methods HTTP methods that the route should match. * @param string $pattern Route pattern. @@ -47,8 +49,6 @@ public function __construct( ); } - private readonly RouteObject $route; - public function getRoute(): RouteObject { return $this->route; diff --git a/tests/Support/TestController.php b/tests/Support/TestController.php index c39616bf..baccc67c 100644 --- a/tests/Support/TestController.php +++ b/tests/Support/TestController.php @@ -7,8 +7,6 @@ use Nyholm\Psr7\Response; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -use Yiisoft\Router\Attribute\Get; -use Yiisoft\Router\Group; final class TestController { From a9dffd4d8859327583913236a1f59af06d62007b Mon Sep 17 00:00:00 2001 From: Rustam Date: Fri, 3 Apr 2026 00:27:43 +0500 Subject: [PATCH 42/63] Update CHANGELOG: add bug fix entry for `Group::hosts()` method consistency with `Route::hosts()` --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6bbff2c..1862393e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - New #196: Add PHP Attributes support (@rustamwin) - New #196: Add `RoutesProviderInterface` interface providing routes from various resources (@rustamwin) +- Bug #196: Fix the behavior of `Group::hosts()` method to be consistent with `Route::hosts()` method (@rustamwin) - Enh #276: Explicitly import classes, functions, and constants in the "use" section (@rustamwin) - Enh #277, #281: Remove restrictions from `prependMiddleware()` and `middleware()` methods (@klsoft-web, @vjik) From b0e9813a5f3fc85fd3d0ea8ea101593da19aa5a1 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 3 Apr 2026 12:54:05 +0300 Subject: [PATCH 43/63] Add phpdoc diff --git c/src/Attribute/Delete.php i/src/Attribute/Delete.php index 443a381..390034b 100644 --- c/src/Attribute/Delete.php +++ i/src/Attribute/Delete.php @@ -9,6 +9,9 @@ use Stringable; use Yiisoft\Http\Method; use Yiisoft\Router\Route; +/** + * Route attribute that defines a DELETE HTTP method route. + */ #[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] final class Delete implements RouteAttributeInterface { diff --git c/src/Attribute/Get.php i/src/Attribute/Get.php index 8fe2ed8..3d5ed51 100644 --- c/src/Attribute/Get.php +++ i/src/Attribute/Get.php @@ -9,6 +9,9 @@ use Stringable; use Yiisoft\Http\Method; use Yiisoft\Router\Route; +/** + * Route attribute that defines a GET HTTP method route. + */ #[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] final class Get implements RouteAttributeInterface { diff --git c/src/Attribute/Head.php i/src/Attribute/Head.php index 834b87a..ac78fe1 100644 --- c/src/Attribute/Head.php +++ i/src/Attribute/Head.php @@ -9,6 +9,9 @@ use Stringable; use Yiisoft\Http\Method; use Yiisoft\Router\Route; +/** + * Route attribute that defines a HEAD HTTP method route. + */ #[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] final class Head implements RouteAttributeInterface { diff --git c/src/Attribute/Options.php i/src/Attribute/Options.php index e62c71f..106a851 100644 --- c/src/Attribute/Options.php +++ i/src/Attribute/Options.php @@ -9,6 +9,9 @@ use Stringable; use Yiisoft\Http\Method; use Yiisoft\Router\Route; +/** + * Route attribute that defines an OPTIONS HTTP method route. + */ #[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] final class Options implements RouteAttributeInterface { diff --git c/src/Attribute/Patch.php i/src/Attribute/Patch.php index 9af032c..c4e310a 100644 --- c/src/Attribute/Patch.php +++ i/src/Attribute/Patch.php @@ -9,6 +9,9 @@ use Stringable; use Yiisoft\Http\Method; use Yiisoft\Router\Route; +/** + * Route attribute that defines a PATCH HTTP method route. + */ #[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] final class Patch implements RouteAttributeInterface { diff --git c/src/Attribute/Post.php i/src/Attribute/Post.php index 8f8bda6..a8fa0cc 100644 --- c/src/Attribute/Post.php +++ i/src/Attribute/Post.php @@ -9,6 +9,9 @@ use Stringable; use Yiisoft\Http\Method; use Yiisoft\Router\Route; +/** + * Route attribute that defines a POST HTTP method route. + */ #[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] final class Post implements RouteAttributeInterface { diff --git c/src/Attribute/Put.php i/src/Attribute/Put.php index 88ea1e9..9e05bc8 100644 --- c/src/Attribute/Put.php +++ i/src/Attribute/Put.php @@ -9,6 +9,9 @@ use Stringable; use Yiisoft\Http\Method; use Yiisoft\Router\Route; +/** + * Route attribute that defines a PUT HTTP method route. + */ #[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] final class Put implements RouteAttributeInterface { diff --git c/src/Attribute/Route.php i/src/Attribute/Route.php index cccdb14..5826ff2 100644 --- c/src/Attribute/Route.php +++ i/src/Attribute/Route.php @@ -8,6 +8,9 @@ use Attribute; use Stringable; use Yiisoft\Router\Route as RouteObject; +/** + * Route attribute that defines a route with custom HTTP methods. + */ #[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] final class Route implements RouteAttributeInterface { diff --git c/src/Attribute/RouteAttributeInterface.php i/src/Attribute/RouteAttributeInterface.php index cecfa63..c643960 100644 --- c/src/Attribute/RouteAttributeInterface.php +++ i/src/Attribute/RouteAttributeInterface.php @@ -6,7 +6,15 @@ namespace Yiisoft\Router\Attribute; use Yiisoft\Router\Route; +/** + * Interface for route attributes that can provide a route instance. + */ interface RouteAttributeInterface { + /** + * Returns the route instance defined by this attribute. + * + * @return Route The route instance. + */ public function getRoute(): Route; } diff --git c/src/Group.php i/src/Group.php index 7717bde..6d4ea8f 100644 --- c/src/Group.php +++ i/src/Group.php @@ -13,6 +13,9 @@ use function is_array; use function is_callable; use function is_string; +/** + * Route group that allows organizing routes with common properties. + */ #[Attribute(Attribute::TARGET_CLASS)] final class Group { @@ -77,6 +80,12 @@ final class Group return new self($prefix); } + /** + * Sets the routes for this group. + * + * @param self|Route ...$routes Routes or sub-groups to include in this group. + * @return self New instance with the specified routes. + */ public function routes(self|Route ...$routes): self { $new = clone $this; @@ -133,6 +142,12 @@ final class Group return $new; } + /** + * Sets the name prefix for all routes in this group. + * + * @param string $namePrefix Prefix to prepend to route names. + * @return self New instance with the specified name prefix. + */ public function namePrefix(string $namePrefix): self { $new = clone $this; @@ -140,11 +155,23 @@ final class Group return $new; } + /** + * Adds a host requirement for all routes in this group. + * + * @param string $host Host name to match. + * @return self New instance with the specified host. + */ public function host(string $host): self { return $this->hosts($host); } + /** + * Sets host requirements for all routes in this group. + * + * @param string ...$hosts Host names to match. + * @return self New instance with the specified hosts. + */ public function hosts(string ...$hosts): self { $new = clone $this; @@ -172,22 +199,12 @@ final class Group } /** - * @psalm-template T as string + * Returns group data by key. * - * @psalm-param T $key - * - * @psalm-return ( - * T is ('prefix'|'namePrefix'|'host') ? string|null : - * (T is 'routes' ? Group[]|Route[] : - * (T is 'hosts' ? array : - * (T is ('hasCorsMiddleware') ? bool : - * (T is 'enabledMiddlewares' ? list : - * (T is 'corsMiddleware' ? array|callable|string|null : mixed) - * ) - * ) - * ) - * ) - * ) + * @param string $key Data key to retrieve (`prefix`, `namePrefix`, `host`, `hosts`, `corsMiddleware`, `routes`, + * `hasCorsMiddleware`, `enabledMiddlewares`). + * @return mixed The requested data. + * @throws InvalidArgumentException If the key is unknown. */ public function getData(string $key): mixed { diff --git c/src/MatchingResult.php i/src/MatchingResult.php index 3014823..5dfe6e1 100644 --- c/src/MatchingResult.php +++ i/src/MatchingResult.php @@ -7,6 +7,9 @@ namespace Yiisoft\Router; use RuntimeException; use Yiisoft\Http\Method; +/** + * Result of matching a request against routes. + */ final class MatchingResult { /** @@ -51,6 +54,11 @@ final class MatchingResult return $this->route !== null; } + /** + * Checks if the request method was not allowed for the matched route. + * + * @return bool True if the method was not allowed, false otherwise. + */ public function isMethodFailure(): bool { return $this->route === null && $this->methods !== Method::ALL; diff --git c/src/Middleware/Router.php i/src/Middleware/Router.php index e613d9c..6348ce7 100644 --- c/src/Middleware/Router.php +++ i/src/Middleware/Router.php @@ -17,10 +17,20 @@ use Yiisoft\Middleware\Dispatcher\MiddlewareFactory; use Yiisoft\Router\CurrentRoute; use Yiisoft\Router\UrlMatcherInterface; +/** + * Router middleware that matches the request to a route and dispatches to the matched route's middleware. + */ final class Router implements MiddlewareInterface { private readonly MiddlewareDispatcher $dispatcher; + /** + * @param UrlMatcherInterface $matcher URL matcher to find matching routes. + * @param ResponseFactoryInterface $responseFactory Factory for creating responses. + * @param MiddlewareFactory $middlewareFactory Factory for creating middleware instances. + * @param CurrentRoute $currentRoute Current route container. + * @param EventDispatcherInterface|null $eventDispatcher Optional event dispatcher. + */ public function __construct( private readonly UrlMatcherInterface $matcher, private readonly ResponseFactoryInterface $responseFactory, diff --git c/src/Provider/ArrayRoutesProvider.php i/src/Provider/ArrayRoutesProvider.php index 3c52d54..5fa5f94 100644 --- c/src/Provider/ArrayRoutesProvider.php +++ i/src/Provider/ArrayRoutesProvider.php @@ -7,6 +7,9 @@ namespace Yiisoft\Router\Provider; use Yiisoft\Router\Route; use Yiisoft\Router\Group; +/** + * Routes provider that is initialized with an array of routes and groups. + */ final class ArrayRoutesProvider implements RoutesProviderInterface { /** diff --git c/src/Provider/RoutesProviderInterface.php i/src/Provider/RoutesProviderInterface.php index 16b6198..230fa46 100644 --- c/src/Provider/RoutesProviderInterface.php +++ i/src/Provider/RoutesProviderInterface.php @@ -8,11 +8,13 @@ use Yiisoft\Router\Group; use Yiisoft\Router\Route; /** - * `RoutesProviderInterface` provides routes to route collector. + * Provides routes and route groups to route collector. */ interface RoutesProviderInterface { /** + * Returns an array of routes and/or route groups. + * * @return Group[]|Route[] */ public function getRoutes(): array; diff --git c/src/Route.php i/src/Route.php index 5faa63f..6f82f26 100644 --- c/src/Route.php +++ i/src/Route.php @@ -85,6 +85,11 @@ final class Route implements Stringable } } + /** + * Returns a string representation of the route. + * + * @return string String representation including name (if set), methods, hosts, and pattern. + */ public function __toString(): string { $result = $this->name === null @@ -108,6 +113,11 @@ final class Route implements Stringable return $result; } + /** + * Returns debug information about the route. + * + * @return array Array with route properties for debugging. + */ public function __debugInfo() { return [ @@ -124,36 +134,78 @@ final class Route implements Stringable ]; } + /** + * Creates a GET route. + * + * @param string $pattern URL pattern. + * @return self New route instance. + */ public static function get(string $pattern): self { return self::methods([Method::GET], $pattern); } + /** + * Creates a POST route. + * + * @param string $pattern URL pattern. + * @return self New route instance. + */ public static function post(string $pattern): self { return self::methods([Method::POST], $pattern); } + /** + * Creates a PUT route. + * + * @param string $pattern URL pattern. + * @return self New route instance. + */ public static function put(string $pattern): self { return self::methods([Method::PUT], $pattern); } + /** + * Creates a DELETE route. + * + * @param string $pattern URL pattern. + * @return self New route instance. + */ public static function delete(string $pattern): self { return self::methods([Method::DELETE], $pattern); } + /** + * Creates a PATCH route. + * + * @param string $pattern URL pattern. + * @return self New route instance. + */ public static function patch(string $pattern): self { return self::methods([Method::PATCH], $pattern); } + /** + * Creates a HEAD route. + * + * @param string $pattern URL pattern. + * @return self New route instance. + */ public static function head(string $pattern): self { return self::methods([Method::HEAD], $pattern); } + /** + * Creates an OPTIONS route. + * + * @param string $pattern URL pattern. + * @return self New route instance. + */ public static function options(string $pattern): self { return self::methods([Method::OPTIONS], $pattern); @@ -167,6 +219,12 @@ final class Route implements Stringable return new self($methods, $pattern); } + /** + * Sets the route name. + * + * @param string $name Route name. + * @return self New instance with the specified name. + */ public function name(string $name): self { $route = clone $this; @@ -174,6 +232,12 @@ final class Route implements Stringable return $route; } + /** + * Sets the URL pattern. + * + * @param string $pattern URL pattern. + * @return self New instance with the specified pattern. + */ public function pattern(string $pattern): self { $new = clone $this; @@ -181,11 +245,23 @@ final class Route implements Stringable return $new; } + /** + * Adds a host requirement. + * + * @param string $host Host name to match. + * @return self New instance with the specified host. + */ public function host(string $host): self { return $this->hosts($host); } + /** + * Sets host requirements. + * + * @param string ...$hosts Host names to match. + * @return self New instance with the specified hosts. + */ public function hosts(string ...$hosts): self { $route = clone $this; @@ -304,24 +380,12 @@ final class Route implements Stringable } /** - * @psalm-template T as string + * Returns route data by key. * - * @psalm-param T $key - * - * @psalm-return ( - * T is ('name'|'pattern') ? string : - * (T is 'host' ? string|null : - * (T is 'hosts' ? array : - * (T is 'methods' ? array : - * (T is 'defaults' ? array : - * (T is ('override'|'hasMiddlewares') ? bool : - * (T is 'enabledMiddlewares' ? array : mixed) - * ) - * ) - * ) - * ) - * ) - * ) + * @param string $key Data key to retrieve (`name`, `pattern`, `host`, `hosts`, `methods`, `defaults`, `override`, + * `hasMiddlewares`, `enabledMiddlewares`). + * @return mixed The requested data. + * @throws InvalidArgumentException If the key is unknown. */ public function getData(string $key): mixed { diff --git c/src/RouteCollection.php i/src/RouteCollection.php index 5af3ef6..54bebe3 100644 --- c/src/RouteCollection.php +++ i/src/RouteCollection.php @@ -13,6 +13,8 @@ use function in_array; use function is_array; /** + * Collection of routes that manages route registration and builds a route tree. + * * @psalm-type Items = array */ final class RouteCollection implements RouteCollectionInterface @@ -29,6 +31,9 @@ final class RouteCollection implements RouteCollectionInterface */ private array $routes = []; + /** + * @param RouteCollectorInterface $collector The route collector to use. + */ public function __construct(private readonly RouteCollectorInterface $collector) {} public function getRoutes(): array diff --git c/src/RouteCollectionInterface.php i/src/RouteCollectionInterface.php index fe2ff1f..107804d 100644 --- c/src/RouteCollectionInterface.php +++ i/src/RouteCollectionInterface.php @@ -4,17 +4,31 @@ declare(strict_types=1); namespace Yiisoft\Router; +/** + * Interface for route collections that provide access to registered routes. + */ interface RouteCollectionInterface { /** - * @return Route[] + * Returns all routes in the collection. + * + * @return Route[] Array of routes indexed by name. */ public function getRoutes(): array; + /** + * Returns a route by name. + * + * @param string $name Route name. + * @return Route The route instance. + * @throws RouteNotFoundException If the route is not found. + */ public function getRoute(string $name): Route; /** * Returns routes tree array. + * + * @return array Hierarchical array of routes and/or groups. */ public function getRouteTree(): array; } diff --git c/src/RouteCollector.php i/src/RouteCollector.php index c44479b..7c4b959 100644 --- c/src/RouteCollector.php +++ i/src/RouteCollector.php @@ -6,6 +6,9 @@ namespace Yiisoft\Router; use Yiisoft\Router\Provider\RoutesProviderInterface; +/** + * Simple route collector that manages routes, groups, and middleware definitions. + */ final class RouteCollector implements RouteCollectorInterface { /** @@ -25,6 +28,12 @@ final class RouteCollector implements RouteCollectorInterface */ public function __construct(private readonly array $providers = []) {} + /** + * Adds routes or groups to the collector. + * + * @param Route|Group ...$routes Routes or groups to add. + * @return RouteCollectorInterface The collector instance. + */ public function addRoute(Route|Group ...$routes): RouteCollectorInterface { array_push( @@ -52,6 +61,11 @@ final class RouteCollector implements RouteCollectorInterface return $this; } + /** + * Returns all registered items (routes and groups). + * + * @return Group[]|Route[] + */ public function getItems(): array { if (!$this->providersAreInjected) { @@ -66,6 +80,11 @@ final class RouteCollector implements RouteCollectorInterface return $this->items; } + /** + * Returns all middleware definitions. + * + * @return array[]|callable[]|string[] + */ public function getMiddlewareDefinitions(): array { return $this->middlewareDefinitions; diff --git c/src/RouteCollectorInterface.php i/src/RouteCollectorInterface.php index 38c5861..dca8742 100644 --- c/src/RouteCollectorInterface.php +++ i/src/RouteCollectorInterface.php @@ -4,6 +4,9 @@ declare(strict_types=1); namespace Yiisoft\Router; +/** + * Interface for route collectors that manage route registration. + */ interface RouteCollectorInterface { /** @@ -14,21 +17,31 @@ interface RouteCollectorInterface /** * Appends a handler middleware definition that should be invoked for a matched route. * First added handler will be executed first. + * + * @param array|callable|string ...$middlewareDefinition Middleware definitions. + * @return self New instance with the middleware appended. */ public function middleware(array|callable|string ...$middlewareDefinition): self; /** * Prepends a handler middleware definition that should be invoked for a matched route. * First added handler will be executed last. + * + * @param array|callable|string ...$middlewareDefinition Middleware definitions. + * @return self New instance with the middleware prepended. */ public function prependMiddleware(array|callable|string ...$middlewareDefinition): self; /** + * Returns all registered items (routes and groups). + * * @return Group[]|Route[] */ public function getItems(): array; /** + * Returns all middleware definitions. + * * @return array[]|callable[]|string[] */ public function getMiddlewareDefinitions(): array; diff --git c/src/UrlMatcherInterface.php i/src/UrlMatcherInterface.php index f802236..efef6c0 100644 --- c/src/UrlMatcherInterface.php +++ i/src/UrlMatcherInterface.php @@ -7,10 +7,15 @@ namespace Yiisoft\Router; use Psr\Http\Message\ServerRequestInterface; /** - * `UrlMatcherInterface` allows finding a matching route given a PSR-8 server request. It is preferred to type-hint - * against it in case you need to match URL. + * `UrlMatcherInterface` allows finding a matching route given a server request. */ interface UrlMatcherInterface { + /** + * Matches a server request against registered routes. + * + * @param ServerRequestInterface $request The server request to match. + * @return MatchingResult The result of matching, containing route and parameters if successful. + */ public function match(ServerRequestInterface $request): MatchingResult; } --- src/Attribute/Delete.php | 3 + src/Attribute/Get.php | 3 + src/Attribute/Head.php | 3 + src/Attribute/Options.php | 3 + src/Attribute/Patch.php | 3 + src/Attribute/Post.php | 3 + src/Attribute/Put.php | 3 + src/Attribute/Route.php | 3 + src/Attribute/RouteAttributeInterface.php | 8 ++ src/Group.php | 47 +++++++---- src/MatchingResult.php | 8 ++ src/Middleware/Router.php | 10 +++ src/Provider/ArrayRoutesProvider.php | 3 + src/Provider/RoutesProviderInterface.php | 4 +- src/Route.php | 98 +++++++++++++++++++---- src/RouteCollection.php | 5 ++ src/RouteCollectionInterface.php | 16 +++- src/RouteCollector.php | 19 +++++ src/RouteCollectorInterface.php | 13 +++ src/UrlMatcherInterface.php | 9 ++- 20 files changed, 228 insertions(+), 36 deletions(-) diff --git a/src/Attribute/Delete.php b/src/Attribute/Delete.php index 443a3812..390034b0 100644 --- a/src/Attribute/Delete.php +++ b/src/Attribute/Delete.php @@ -9,6 +9,9 @@ use Yiisoft\Http\Method; use Yiisoft\Router\Route; +/** + * Route attribute that defines a DELETE HTTP method route. + */ #[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] final class Delete implements RouteAttributeInterface { diff --git a/src/Attribute/Get.php b/src/Attribute/Get.php index 8fe2ed84..3d5ed511 100644 --- a/src/Attribute/Get.php +++ b/src/Attribute/Get.php @@ -9,6 +9,9 @@ use Yiisoft\Http\Method; use Yiisoft\Router\Route; +/** + * Route attribute that defines a GET HTTP method route. + */ #[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] final class Get implements RouteAttributeInterface { diff --git a/src/Attribute/Head.php b/src/Attribute/Head.php index 834b87a5..ac78fe16 100644 --- a/src/Attribute/Head.php +++ b/src/Attribute/Head.php @@ -9,6 +9,9 @@ use Yiisoft\Http\Method; use Yiisoft\Router\Route; +/** + * Route attribute that defines a HEAD HTTP method route. + */ #[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] final class Head implements RouteAttributeInterface { diff --git a/src/Attribute/Options.php b/src/Attribute/Options.php index e62c71f3..106a851e 100644 --- a/src/Attribute/Options.php +++ b/src/Attribute/Options.php @@ -9,6 +9,9 @@ use Yiisoft\Http\Method; use Yiisoft\Router\Route; +/** + * Route attribute that defines an OPTIONS HTTP method route. + */ #[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] final class Options implements RouteAttributeInterface { diff --git a/src/Attribute/Patch.php b/src/Attribute/Patch.php index 9af032c3..c4e310a6 100644 --- a/src/Attribute/Patch.php +++ b/src/Attribute/Patch.php @@ -9,6 +9,9 @@ use Yiisoft\Http\Method; use Yiisoft\Router\Route; +/** + * Route attribute that defines a PATCH HTTP method route. + */ #[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] final class Patch implements RouteAttributeInterface { diff --git a/src/Attribute/Post.php b/src/Attribute/Post.php index 8f8bda6e..a8fa0ccb 100644 --- a/src/Attribute/Post.php +++ b/src/Attribute/Post.php @@ -9,6 +9,9 @@ use Yiisoft\Http\Method; use Yiisoft\Router\Route; +/** + * Route attribute that defines a POST HTTP method route. + */ #[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] final class Post implements RouteAttributeInterface { diff --git a/src/Attribute/Put.php b/src/Attribute/Put.php index 88ea1e91..9e05bc8e 100644 --- a/src/Attribute/Put.php +++ b/src/Attribute/Put.php @@ -9,6 +9,9 @@ use Yiisoft\Http\Method; use Yiisoft\Router\Route; +/** + * Route attribute that defines a PUT HTTP method route. + */ #[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] final class Put implements RouteAttributeInterface { diff --git a/src/Attribute/Route.php b/src/Attribute/Route.php index cccdb14b..5826ff20 100644 --- a/src/Attribute/Route.php +++ b/src/Attribute/Route.php @@ -8,6 +8,9 @@ use Stringable; use Yiisoft\Router\Route as RouteObject; +/** + * Route attribute that defines a route with custom HTTP methods. + */ #[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] final class Route implements RouteAttributeInterface { diff --git a/src/Attribute/RouteAttributeInterface.php b/src/Attribute/RouteAttributeInterface.php index cecfa630..c643960a 100644 --- a/src/Attribute/RouteAttributeInterface.php +++ b/src/Attribute/RouteAttributeInterface.php @@ -6,7 +6,15 @@ use Yiisoft\Router\Route; +/** + * Interface for route attributes that can provide a route instance. + */ interface RouteAttributeInterface { + /** + * Returns the route instance defined by this attribute. + * + * @return Route The route instance. + */ public function getRoute(): Route; } diff --git a/src/Group.php b/src/Group.php index 7717bdef..6d4ea8f7 100644 --- a/src/Group.php +++ b/src/Group.php @@ -13,6 +13,9 @@ use function is_callable; use function is_string; +/** + * Route group that allows organizing routes with common properties. + */ #[Attribute(Attribute::TARGET_CLASS)] final class Group { @@ -77,6 +80,12 @@ public static function create(?string $prefix = null): self return new self($prefix); } + /** + * Sets the routes for this group. + * + * @param self|Route ...$routes Routes or sub-groups to include in this group. + * @return self New instance with the specified routes. + */ public function routes(self|Route ...$routes): self { $new = clone $this; @@ -133,6 +142,12 @@ public function prependMiddleware(array|callable|string ...$definition): self return $new; } + /** + * Sets the name prefix for all routes in this group. + * + * @param string $namePrefix Prefix to prepend to route names. + * @return self New instance with the specified name prefix. + */ public function namePrefix(string $namePrefix): self { $new = clone $this; @@ -140,11 +155,23 @@ public function namePrefix(string $namePrefix): self return $new; } + /** + * Adds a host requirement for all routes in this group. + * + * @param string $host Host name to match. + * @return self New instance with the specified host. + */ public function host(string $host): self { return $this->hosts($host); } + /** + * Sets host requirements for all routes in this group. + * + * @param string ...$hosts Host names to match. + * @return self New instance with the specified hosts. + */ public function hosts(string ...$hosts): self { $new = clone $this; @@ -172,22 +199,12 @@ public function disableMiddleware(mixed ...$definition): self } /** - * @psalm-template T as string - * - * @psalm-param T $key + * Returns group data by key. * - * @psalm-return ( - * T is ('prefix'|'namePrefix'|'host') ? string|null : - * (T is 'routes' ? Group[]|Route[] : - * (T is 'hosts' ? array : - * (T is ('hasCorsMiddleware') ? bool : - * (T is 'enabledMiddlewares' ? list : - * (T is 'corsMiddleware' ? array|callable|string|null : mixed) - * ) - * ) - * ) - * ) - * ) + * @param string $key Data key to retrieve (`prefix`, `namePrefix`, `host`, `hosts`, `corsMiddleware`, `routes`, + * `hasCorsMiddleware`, `enabledMiddlewares`). + * @return mixed The requested data. + * @throws InvalidArgumentException If the key is unknown. */ public function getData(string $key): mixed { diff --git a/src/MatchingResult.php b/src/MatchingResult.php index 3014823a..5dfe6e10 100644 --- a/src/MatchingResult.php +++ b/src/MatchingResult.php @@ -7,6 +7,9 @@ use RuntimeException; use Yiisoft\Http\Method; +/** + * Result of matching a request against routes. + */ final class MatchingResult { /** @@ -51,6 +54,11 @@ public function isSuccess(): bool return $this->route !== null; } + /** + * Checks if the request method was not allowed for the matched route. + * + * @return bool True if the method was not allowed, false otherwise. + */ public function isMethodFailure(): bool { return $this->route === null && $this->methods !== Method::ALL; diff --git a/src/Middleware/Router.php b/src/Middleware/Router.php index e613d9c3..6348ce79 100644 --- a/src/Middleware/Router.php +++ b/src/Middleware/Router.php @@ -17,10 +17,20 @@ use Yiisoft\Router\CurrentRoute; use Yiisoft\Router\UrlMatcherInterface; +/** + * Router middleware that matches the request to a route and dispatches to the matched route's middleware. + */ final class Router implements MiddlewareInterface { private readonly MiddlewareDispatcher $dispatcher; + /** + * @param UrlMatcherInterface $matcher URL matcher to find matching routes. + * @param ResponseFactoryInterface $responseFactory Factory for creating responses. + * @param MiddlewareFactory $middlewareFactory Factory for creating middleware instances. + * @param CurrentRoute $currentRoute Current route container. + * @param EventDispatcherInterface|null $eventDispatcher Optional event dispatcher. + */ public function __construct( private readonly UrlMatcherInterface $matcher, private readonly ResponseFactoryInterface $responseFactory, diff --git a/src/Provider/ArrayRoutesProvider.php b/src/Provider/ArrayRoutesProvider.php index 3c52d546..5fa5f949 100644 --- a/src/Provider/ArrayRoutesProvider.php +++ b/src/Provider/ArrayRoutesProvider.php @@ -7,6 +7,9 @@ use Yiisoft\Router\Route; use Yiisoft\Router\Group; +/** + * Routes provider that is initialized with an array of routes and groups. + */ final class ArrayRoutesProvider implements RoutesProviderInterface { /** diff --git a/src/Provider/RoutesProviderInterface.php b/src/Provider/RoutesProviderInterface.php index 16b61986..230fa468 100644 --- a/src/Provider/RoutesProviderInterface.php +++ b/src/Provider/RoutesProviderInterface.php @@ -8,11 +8,13 @@ use Yiisoft\Router\Route; /** - * `RoutesProviderInterface` provides routes to route collector. + * Provides routes and route groups to route collector. */ interface RoutesProviderInterface { /** + * Returns an array of routes and/or route groups. + * * @return Group[]|Route[] */ public function getRoutes(): array; diff --git a/src/Route.php b/src/Route.php index 5faa63f5..6f82f261 100644 --- a/src/Route.php +++ b/src/Route.php @@ -85,6 +85,11 @@ public function __construct( } } + /** + * Returns a string representation of the route. + * + * @return string String representation including name (if set), methods, hosts, and pattern. + */ public function __toString(): string { $result = $this->name === null @@ -108,6 +113,11 @@ public function __toString(): string return $result; } + /** + * Returns debug information about the route. + * + * @return array Array with route properties for debugging. + */ public function __debugInfo() { return [ @@ -124,36 +134,78 @@ public function __debugInfo() ]; } + /** + * Creates a GET route. + * + * @param string $pattern URL pattern. + * @return self New route instance. + */ public static function get(string $pattern): self { return self::methods([Method::GET], $pattern); } + /** + * Creates a POST route. + * + * @param string $pattern URL pattern. + * @return self New route instance. + */ public static function post(string $pattern): self { return self::methods([Method::POST], $pattern); } + /** + * Creates a PUT route. + * + * @param string $pattern URL pattern. + * @return self New route instance. + */ public static function put(string $pattern): self { return self::methods([Method::PUT], $pattern); } + /** + * Creates a DELETE route. + * + * @param string $pattern URL pattern. + * @return self New route instance. + */ public static function delete(string $pattern): self { return self::methods([Method::DELETE], $pattern); } + /** + * Creates a PATCH route. + * + * @param string $pattern URL pattern. + * @return self New route instance. + */ public static function patch(string $pattern): self { return self::methods([Method::PATCH], $pattern); } + /** + * Creates a HEAD route. + * + * @param string $pattern URL pattern. + * @return self New route instance. + */ public static function head(string $pattern): self { return self::methods([Method::HEAD], $pattern); } + /** + * Creates an OPTIONS route. + * + * @param string $pattern URL pattern. + * @return self New route instance. + */ public static function options(string $pattern): self { return self::methods([Method::OPTIONS], $pattern); @@ -167,6 +219,12 @@ public static function methods(array $methods, string $pattern): self return new self($methods, $pattern); } + /** + * Sets the route name. + * + * @param string $name Route name. + * @return self New instance with the specified name. + */ public function name(string $name): self { $route = clone $this; @@ -174,6 +232,12 @@ public function name(string $name): self return $route; } + /** + * Sets the URL pattern. + * + * @param string $pattern URL pattern. + * @return self New instance with the specified pattern. + */ public function pattern(string $pattern): self { $new = clone $this; @@ -181,11 +245,23 @@ public function pattern(string $pattern): self return $new; } + /** + * Adds a host requirement. + * + * @param string $host Host name to match. + * @return self New instance with the specified host. + */ public function host(string $host): self { return $this->hosts($host); } + /** + * Sets host requirements. + * + * @param string ...$hosts Host names to match. + * @return self New instance with the specified hosts. + */ public function hosts(string ...$hosts): self { $route = clone $this; @@ -304,24 +380,12 @@ public function disableMiddleware(mixed ...$definition): self } /** - * @psalm-template T as string - * - * @psalm-param T $key + * Returns route data by key. * - * @psalm-return ( - * T is ('name'|'pattern') ? string : - * (T is 'host' ? string|null : - * (T is 'hosts' ? array : - * (T is 'methods' ? array : - * (T is 'defaults' ? array : - * (T is ('override'|'hasMiddlewares') ? bool : - * (T is 'enabledMiddlewares' ? array : mixed) - * ) - * ) - * ) - * ) - * ) - * ) + * @param string $key Data key to retrieve (`name`, `pattern`, `host`, `hosts`, `methods`, `defaults`, `override`, + * `hasMiddlewares`, `enabledMiddlewares`). + * @return mixed The requested data. + * @throws InvalidArgumentException If the key is unknown. */ public function getData(string $key): mixed { diff --git a/src/RouteCollection.php b/src/RouteCollection.php index 5af3ef6b..54bebe3d 100644 --- a/src/RouteCollection.php +++ b/src/RouteCollection.php @@ -13,6 +13,8 @@ use function is_array; /** + * Collection of routes that manages route registration and builds a route tree. + * * @psalm-type Items = array */ final class RouteCollection implements RouteCollectionInterface @@ -29,6 +31,9 @@ final class RouteCollection implements RouteCollectionInterface */ private array $routes = []; + /** + * @param RouteCollectorInterface $collector The route collector to use. + */ public function __construct(private readonly RouteCollectorInterface $collector) {} public function getRoutes(): array diff --git a/src/RouteCollectionInterface.php b/src/RouteCollectionInterface.php index fe2ff1f7..107804db 100644 --- a/src/RouteCollectionInterface.php +++ b/src/RouteCollectionInterface.php @@ -4,17 +4,31 @@ namespace Yiisoft\Router; +/** + * Interface for route collections that provide access to registered routes. + */ interface RouteCollectionInterface { /** - * @return Route[] + * Returns all routes in the collection. + * + * @return Route[] Array of routes indexed by name. */ public function getRoutes(): array; + /** + * Returns a route by name. + * + * @param string $name Route name. + * @return Route The route instance. + * @throws RouteNotFoundException If the route is not found. + */ public function getRoute(string $name): Route; /** * Returns routes tree array. + * + * @return array Hierarchical array of routes and/or groups. */ public function getRouteTree(): array; } diff --git a/src/RouteCollector.php b/src/RouteCollector.php index c44479ba..7c4b959e 100644 --- a/src/RouteCollector.php +++ b/src/RouteCollector.php @@ -6,6 +6,9 @@ use Yiisoft\Router\Provider\RoutesProviderInterface; +/** + * Simple route collector that manages routes, groups, and middleware definitions. + */ final class RouteCollector implements RouteCollectorInterface { /** @@ -25,6 +28,12 @@ final class RouteCollector implements RouteCollectorInterface */ public function __construct(private readonly array $providers = []) {} + /** + * Adds routes or groups to the collector. + * + * @param Route|Group ...$routes Routes or groups to add. + * @return RouteCollectorInterface The collector instance. + */ public function addRoute(Route|Group ...$routes): RouteCollectorInterface { array_push( @@ -52,6 +61,11 @@ public function prependMiddleware(array|callable|string ...$middlewareDefinition return $this; } + /** + * Returns all registered items (routes and groups). + * + * @return Group[]|Route[] + */ public function getItems(): array { if (!$this->providersAreInjected) { @@ -66,6 +80,11 @@ public function getItems(): array return $this->items; } + /** + * Returns all middleware definitions. + * + * @return array[]|callable[]|string[] + */ public function getMiddlewareDefinitions(): array { return $this->middlewareDefinitions; diff --git a/src/RouteCollectorInterface.php b/src/RouteCollectorInterface.php index 38c5861c..dca87427 100644 --- a/src/RouteCollectorInterface.php +++ b/src/RouteCollectorInterface.php @@ -4,6 +4,9 @@ namespace Yiisoft\Router; +/** + * Interface for route collectors that manage route registration. + */ interface RouteCollectorInterface { /** @@ -14,21 +17,31 @@ public function addRoute(Route|Group ...$routes): self; /** * Appends a handler middleware definition that should be invoked for a matched route. * First added handler will be executed first. + * + * @param array|callable|string ...$middlewareDefinition Middleware definitions. + * @return self New instance with the middleware appended. */ public function middleware(array|callable|string ...$middlewareDefinition): self; /** * Prepends a handler middleware definition that should be invoked for a matched route. * First added handler will be executed last. + * + * @param array|callable|string ...$middlewareDefinition Middleware definitions. + * @return self New instance with the middleware prepended. */ public function prependMiddleware(array|callable|string ...$middlewareDefinition): self; /** + * Returns all registered items (routes and groups). + * * @return Group[]|Route[] */ public function getItems(): array; /** + * Returns all middleware definitions. + * * @return array[]|callable[]|string[] */ public function getMiddlewareDefinitions(): array; diff --git a/src/UrlMatcherInterface.php b/src/UrlMatcherInterface.php index f8022362..efef6c03 100644 --- a/src/UrlMatcherInterface.php +++ b/src/UrlMatcherInterface.php @@ -7,10 +7,15 @@ use Psr\Http\Message\ServerRequestInterface; /** - * `UrlMatcherInterface` allows finding a matching route given a PSR-8 server request. It is preferred to type-hint - * against it in case you need to match URL. + * `UrlMatcherInterface` allows finding a matching route given a server request. */ interface UrlMatcherInterface { + /** + * Matches a server request against registered routes. + * + * @param ServerRequestInterface $request The server request to match. + * @return MatchingResult The result of matching, containing route and parameters if successful. + */ public function match(ServerRequestInterface $request): MatchingResult; } From 65e1e1369857d0d9b8d5250512b6d73734025ea6 Mon Sep 17 00:00:00 2001 From: Rustam Mamadaminov Date: Thu, 9 Apr 2026 17:26:37 +0500 Subject: [PATCH 44/63] Mark `Group::create`, `RouteCollectorInterface`, `RouteCollector`, and `Route` HTTP methods as deprecated (#287) --- src/Group.php | 2 ++ src/Route.php | 16 ++++++++++++++++ src/RouteCollector.php | 2 ++ src/RouteCollectorInterface.php | 2 ++ 4 files changed, 22 insertions(+) diff --git a/src/Group.php b/src/Group.php index 6d4ea8f7..7a3df203 100644 --- a/src/Group.php +++ b/src/Group.php @@ -74,6 +74,8 @@ public function __construct( * Create a new group instance. * * @param string|null $prefix URL prefix to prepend to all routes of the group. + * + * @deprecated Use `new Group()` instead. */ public static function create(?string $prefix = null): self { diff --git a/src/Route.php b/src/Route.php index 6f82f261..c3aef82d 100644 --- a/src/Route.php +++ b/src/Route.php @@ -139,6 +139,8 @@ public function __debugInfo() * * @param string $pattern URL pattern. * @return self New route instance. + * + * @deprecated Use `new Router()` instead. */ public static function get(string $pattern): self { @@ -150,6 +152,8 @@ public static function get(string $pattern): self * * @param string $pattern URL pattern. * @return self New route instance. + * + * @deprecated Use `new Router()` instead. */ public static function post(string $pattern): self { @@ -161,6 +165,8 @@ public static function post(string $pattern): self * * @param string $pattern URL pattern. * @return self New route instance. + * + * @deprecated Use `new Router()` instead. */ public static function put(string $pattern): self { @@ -172,6 +178,8 @@ public static function put(string $pattern): self * * @param string $pattern URL pattern. * @return self New route instance. + * + * @deprecated Use `new Router()` instead. */ public static function delete(string $pattern): self { @@ -183,6 +191,8 @@ public static function delete(string $pattern): self * * @param string $pattern URL pattern. * @return self New route instance. + * + * @deprecated Use `new Router()` instead. */ public static function patch(string $pattern): self { @@ -194,6 +204,8 @@ public static function patch(string $pattern): self * * @param string $pattern URL pattern. * @return self New route instance. + * + * @deprecated Use `new Router()` instead. */ public static function head(string $pattern): self { @@ -205,6 +217,8 @@ public static function head(string $pattern): self * * @param string $pattern URL pattern. * @return self New route instance. + * + * @deprecated Use `new Router()` instead. */ public static function options(string $pattern): self { @@ -213,6 +227,8 @@ public static function options(string $pattern): self /** * @param string[] $methods + * + * @deprecated Use `new Router()` instead. */ public static function methods(array $methods, string $pattern): self { diff --git a/src/RouteCollector.php b/src/RouteCollector.php index 7c4b959e..b0d5d3db 100644 --- a/src/RouteCollector.php +++ b/src/RouteCollector.php @@ -8,6 +8,8 @@ /** * Simple route collector that manages routes, groups, and middleware definitions. + * + * @deprecated Will be removed in the next major release. */ final class RouteCollector implements RouteCollectorInterface { diff --git a/src/RouteCollectorInterface.php b/src/RouteCollectorInterface.php index dca87427..05ab3523 100644 --- a/src/RouteCollectorInterface.php +++ b/src/RouteCollectorInterface.php @@ -6,6 +6,8 @@ /** * Interface for route collectors that manage route registration. + * + * @deprecated Will be removed in the next major release. */ interface RouteCollectorInterface { From a8b95969160547f92ff16eb92c3bc6b8d83280bb Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 10 Apr 2026 00:45:13 +0300 Subject: [PATCH 45/63] Update src/Route.php Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/Route.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Route.php b/src/Route.php index c3aef82d..152a670e 100644 --- a/src/Route.php +++ b/src/Route.php @@ -137,10 +137,10 @@ public function __debugInfo() /** * Creates a GET route. * - * @param string $pattern URL pattern. - * @return self New route instance. + * `@param` string $pattern URL pattern. + * `@return` self New route instance. * - * @deprecated Use `new Router()` instead. + * `@deprecated` Use `new Route()` instead. */ public static function get(string $pattern): self { From 58a88d501a6ef6b04b094889cd7e5f5d71bbcab8 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 10 Apr 2026 01:01:06 +0300 Subject: [PATCH 46/63] Fix incorrectly backtick usage and wrong class name in @deperecated --- src/Route.php | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Route.php b/src/Route.php index 152a670e..dcf7b076 100644 --- a/src/Route.php +++ b/src/Route.php @@ -137,10 +137,10 @@ public function __debugInfo() /** * Creates a GET route. * - * `@param` string $pattern URL pattern. - * `@return` self New route instance. + * @param string $pattern URL pattern. + * @return self New route instance. * - * `@deprecated` Use `new Route()` instead. + * @deprecated Use `new Route()` instead. */ public static function get(string $pattern): self { @@ -153,7 +153,7 @@ public static function get(string $pattern): self * @param string $pattern URL pattern. * @return self New route instance. * - * @deprecated Use `new Router()` instead. + * @deprecated Use `new Route()` instead. */ public static function post(string $pattern): self { @@ -166,7 +166,7 @@ public static function post(string $pattern): self * @param string $pattern URL pattern. * @return self New route instance. * - * @deprecated Use `new Router()` instead. + * @deprecated Use `new Route()` instead. */ public static function put(string $pattern): self { @@ -179,7 +179,7 @@ public static function put(string $pattern): self * @param string $pattern URL pattern. * @return self New route instance. * - * @deprecated Use `new Router()` instead. + * @deprecated Use `new Route()` instead. */ public static function delete(string $pattern): self { @@ -192,7 +192,7 @@ public static function delete(string $pattern): self * @param string $pattern URL pattern. * @return self New route instance. * - * @deprecated Use `new Router()` instead. + * @deprecated Use `new Route()` instead. */ public static function patch(string $pattern): self { @@ -205,7 +205,7 @@ public static function patch(string $pattern): self * @param string $pattern URL pattern. * @return self New route instance. * - * @deprecated Use `new Router()` instead. + * @deprecated Use `new Route()` instead. */ public static function head(string $pattern): self { @@ -218,7 +218,7 @@ public static function head(string $pattern): self * @param string $pattern URL pattern. * @return self New route instance. * - * @deprecated Use `new Router()` instead. + * @deprecated Use `new Route()` instead. */ public static function options(string $pattern): self { @@ -228,7 +228,7 @@ public static function options(string $pattern): self /** * @param string[] $methods * - * @deprecated Use `new Router()` instead. + * @deprecated Use `new Route()` instead. */ public static function methods(array $methods, string $pattern): self { From 1c168022179f5f3d0dbc2b1b77623f10353d9c6b Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 10 Apr 2026 01:01:44 +0300 Subject: [PATCH 47/63] Add validation for single route case --- src/Provider/FileRoutesProvider.php | 19 ++++++++----------- tests/Provider/FileRoutesProviderTest.php | 7 ++++--- tests/RouteCollectorTest.php | 2 +- 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/src/Provider/FileRoutesProvider.php b/src/Provider/FileRoutesProvider.php index 173fce43..e5e9102a 100644 --- a/src/Provider/FileRoutesProvider.php +++ b/src/Provider/FileRoutesProvider.php @@ -4,7 +4,6 @@ namespace Yiisoft\Router\Provider; -use Closure; use Yiisoft\Router\Group; use Yiisoft\Router\Route; use CallbackFilterIterator; @@ -26,14 +25,11 @@ public function __construct(private readonly string $file, private readonly arra public function getRoutes(): array { - /** @var Closure $scopeRequire */ - $scopeRequire = Closure::bind(static function (string $file, array $scope): mixed { + $scopeRequire = static function (string $file, array $scope): mixed { extract($scope, EXTR_SKIP); - /** - * @psalm-suppress UnresolvableInclude - */ + /** @psalm-suppress UnresolvableInclude */ return require $file; - }, null); + }; if (!file_exists($this->file)) { throw new RuntimeException( 'Failed to provide routes from "' . $this->file . '". File or directory not found.', @@ -72,11 +68,12 @@ public function getRoutes(): array /** @var mixed $routes */ $routes = $scopeRequire($this->file, $this->scope); - if (is_array($routes) && $this->areRoutesValid($routes)) { - return $routes; + if (!is_array($routes) || !$this->areRoutesValid($routes)) { + throw new RuntimeException( + 'Failed to provide routes from "' . $this->file . '". File must return an array of Route or Group instances.', + ); } - - return []; + return $routes; } /** diff --git a/tests/Provider/FileRoutesProviderTest.php b/tests/Provider/FileRoutesProviderTest.php index ae61c9f2..cc2fc18e 100644 --- a/tests/Provider/FileRoutesProviderTest.php +++ b/tests/Provider/FileRoutesProviderTest.php @@ -45,13 +45,14 @@ public function testGetRoutesWithNotExistFile(): void $provider->getRoutes(); } - public function testGetRoutesWithEmptyRoutes(): void + public function testGetRoutesWithInvalidRoutes(): void { $file = dirname(__DIR__) . '/Support/resources/foo.php'; + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Failed to provide routes from "' . $file . '". File must return an array of Route or Group instances.'); $provider = new FileRoutesProvider($file); - - $this->assertEmpty($provider->getRoutes()); + $provider->getRoutes(); } public function testGetRoutesWithScope(): void diff --git a/tests/RouteCollectorTest.php b/tests/RouteCollectorTest.php index f89ddee7..55686101 100644 --- a/tests/RouteCollectorTest.php +++ b/tests/RouteCollectorTest.php @@ -86,7 +86,7 @@ public function testWithProvider(): void Route::get('test/'), ); - $collector = new RouteCollector([new ArrayRoutesProvider([$rootGroup, $postGroup, $testGroup]), new FileRoutesProvider(__DIR__ . '/Support/resources/foo.php')]); + $collector = new RouteCollector([new ArrayRoutesProvider([$rootGroup, $postGroup, $testGroup])]); $this->assertCount(3, $collector->getItems()); $this->assertContainsOnlyInstancesOf(Group::class, $collector->getItems()); From 35c4c40575d032922a643f6e22548f2cb636fda7 Mon Sep 17 00:00:00 2001 From: samdark <47294+samdark@users.noreply.github.com> Date: Thu, 9 Apr 2026 22:02:28 +0000 Subject: [PATCH 48/63] Apply PHP CS Fixer and Rector changes (CI) --- tests/RouteCollectorTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/RouteCollectorTest.php b/tests/RouteCollectorTest.php index 55686101..6ec021cb 100644 --- a/tests/RouteCollectorTest.php +++ b/tests/RouteCollectorTest.php @@ -8,7 +8,6 @@ use PHPUnit\Framework\TestCase; use Yiisoft\Router\Group; use Yiisoft\Router\Provider\ArrayRoutesProvider; -use Yiisoft\Router\Provider\FileRoutesProvider; use Yiisoft\Router\Route; use Yiisoft\Router\RouteCollector; From 82c8fa1d45bcb970d4d89759dd6b3e3840b744c6 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 10 Apr 2026 21:08:08 +0300 Subject: [PATCH 49/63] Allow passing method as a single value instead of an array --- src/Attribute/Delete.php | 2 +- src/Attribute/Get.php | 2 +- src/Attribute/Head.php | 2 +- src/Attribute/Options.php | 2 +- src/Attribute/Patch.php | 2 +- src/Attribute/Post.php | 2 +- src/Attribute/Put.php | 2 +- src/Attribute/Route.php | 2 +- src/Route.php | 7 +++++-- tests/RouteTest.php | 15 +++++++++++---- 10 files changed, 24 insertions(+), 14 deletions(-) diff --git a/src/Attribute/Delete.php b/src/Attribute/Delete.php index 390034b0..5b9d4f1a 100644 --- a/src/Attribute/Delete.php +++ b/src/Attribute/Delete.php @@ -40,7 +40,7 @@ public function __construct( array $disabledMiddlewares = [], ) { $this->route = new Route( - methods: [Method::DELETE], + method: [Method::DELETE], pattern: $pattern, name: $name, middlewares: $middlewares, diff --git a/src/Attribute/Get.php b/src/Attribute/Get.php index 3d5ed511..902686fd 100644 --- a/src/Attribute/Get.php +++ b/src/Attribute/Get.php @@ -40,7 +40,7 @@ public function __construct( array $disabledMiddlewares = [], ) { $this->route = new Route( - methods: [Method::GET], + method: [Method::GET], pattern: $pattern, name: $name, middlewares: $middlewares, diff --git a/src/Attribute/Head.php b/src/Attribute/Head.php index ac78fe16..4dd93287 100644 --- a/src/Attribute/Head.php +++ b/src/Attribute/Head.php @@ -40,7 +40,7 @@ public function __construct( array $disabledMiddlewares = [], ) { $this->route = new Route( - methods: [Method::HEAD], + method: [Method::HEAD], pattern: $pattern, name: $name, middlewares: $middlewares, diff --git a/src/Attribute/Options.php b/src/Attribute/Options.php index 106a851e..7b1d4fad 100644 --- a/src/Attribute/Options.php +++ b/src/Attribute/Options.php @@ -40,7 +40,7 @@ public function __construct( array $disabledMiddlewares = [], ) { $this->route = new Route( - methods: [Method::OPTIONS], + method: [Method::OPTIONS], pattern: $pattern, name: $name, middlewares: $middlewares, diff --git a/src/Attribute/Patch.php b/src/Attribute/Patch.php index c4e310a6..b6a4ce8c 100644 --- a/src/Attribute/Patch.php +++ b/src/Attribute/Patch.php @@ -40,7 +40,7 @@ public function __construct( array $disabledMiddlewares = [], ) { $this->route = new Route( - methods: [Method::PATCH], + method: [Method::PATCH], pattern: $pattern, name: $name, middlewares: $middlewares, diff --git a/src/Attribute/Post.php b/src/Attribute/Post.php index a8fa0ccb..e5efc563 100644 --- a/src/Attribute/Post.php +++ b/src/Attribute/Post.php @@ -40,7 +40,7 @@ public function __construct( array $disabledMiddlewares = [], ) { $this->route = new Route( - methods: [Method::POST], + method: [Method::POST], pattern: $pattern, name: $name, middlewares: $middlewares, diff --git a/src/Attribute/Put.php b/src/Attribute/Put.php index 9e05bc8e..26b3e928 100644 --- a/src/Attribute/Put.php +++ b/src/Attribute/Put.php @@ -40,7 +40,7 @@ public function __construct( array $disabledMiddlewares = [], ) { $this->route = new Route( - methods: [Method::PUT], + method: [Method::PUT], pattern: $pattern, name: $name, middlewares: $middlewares, diff --git a/src/Attribute/Route.php b/src/Attribute/Route.php index 5826ff20..496bcc96 100644 --- a/src/Attribute/Route.php +++ b/src/Attribute/Route.php @@ -41,7 +41,7 @@ public function __construct( array $disabledMiddlewares = [], ) { $this->route = new RouteObject( - methods: $methods, + method: $methods, pattern: $pattern, name: $name, middlewares: $middlewares, diff --git a/src/Route.php b/src/Route.php index dcf7b076..e78bc2bc 100644 --- a/src/Route.php +++ b/src/Route.php @@ -56,10 +56,11 @@ final class Route implements Stringable * It is useful to avoid invoking one of the parent group middleware for * a certain route. * + * @param string|string[] $method HTTP method or list of methods. * @psalm-param list $middlewares */ public function __construct( - array $methods, + string|array $method, private string $pattern, private ?string $name = null, array|callable|string|null $action = null, @@ -69,8 +70,10 @@ public function __construct( private bool $override = false, private array $disabledMiddlewares = [], ) { + $methods = is_string($method) ? [$method] : $method; + if (empty($methods)) { - throw new InvalidArgumentException('$methods cannot be empty.'); + throw new InvalidArgumentException('$method cannot be empty.'); } $this->assertListOfStrings($methods, 'methods'); $this->assertMiddlewares($middlewares); diff --git a/tests/RouteTest.php b/tests/RouteTest.php index ff1bde62..a4fa97dc 100644 --- a/tests/RouteTest.php +++ b/tests/RouteTest.php @@ -33,7 +33,7 @@ final class RouteTest extends TestCase public function testSimpleInstance(): void { $route = new Route( - methods: [Method::GET], + method: [Method::GET], pattern: '/', action: [TestController::class, 'index'], middlewares: [TestMiddleware1::class, fn() => new Response(), TestMiddleware2::class], @@ -48,11 +48,18 @@ public function testSimpleInstance(): void public function testEmptyMethods(): void { $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('$methods cannot be empty.'); + $this->expectExceptionMessage('$method cannot be empty.'); new Route([], ''); } + public function testStringMethodConvertedToArray(): void + { + $route = new Route(Method::POST, '/'); + + $this->assertSame([Method::POST], $route->getData('methods')); + } + public function testName(): void { $route = Route::get('/')->name('test.route'); @@ -282,7 +289,7 @@ public function testMiddlewareAfterAction(): void public function testDefaultsConvertedToStringInConstructor(): void { $route = new Route( - methods: [Method::GET], + method: [Method::GET], pattern: '/{language}', defaults: ['language' => 'en', 'age' => 42], ); @@ -296,7 +303,7 @@ public function testDefaultsConvertedToStringInConstructor(): void public function testActionAddedViaConstructorMiddlewareInsertedBefore(): void { $route = new Route( - methods: [Method::GET], + method: [Method::GET], pattern: '/', action: [TestController::class, 'index'], ); From 1206c2a09e06015f96048d466592ec4492c1e6ae Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 10 Apr 2026 21:10:30 +0300 Subject: [PATCH 50/63] Action is used more than name, swap order --- src/Route.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Route.php b/src/Route.php index e78bc2bc..e025bd2d 100644 --- a/src/Route.php +++ b/src/Route.php @@ -62,8 +62,8 @@ final class Route implements Stringable public function __construct( string|array $method, private string $pattern, - private ?string $name = null, array|callable|string|null $action = null, + private ?string $name = null, array $middlewares = [], array $defaults = [], array $hosts = [], From 652bdaebcc950ec80307a9e7d20a30d5fca82c1e Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 10 Apr 2026 21:12:33 +0300 Subject: [PATCH 51/63] Add missing phpdoc --- src/Route.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Route.php b/src/Route.php index e025bd2d..b5a5bd51 100644 --- a/src/Route.php +++ b/src/Route.php @@ -47,16 +47,19 @@ final class Route implements Stringable private array $defaults = []; /** + * @param string|string[] $method HTTP method or list of methods. + * @param string $pattern URL pattern. * @param array|callable|string|null $action Action handler. It is a primary middleware definition that * should be invoked last for a matched route. + * @param string|null $name Route name. * @param array[]|callable[]|string[] $middlewares Middleware definitions. * @param array $defaults Parameter default values indexed by parameter names. + * @param string[] $hosts Hosts that the route should match. * @param bool $override Marks route as override. When added it will replace existing route with the same name. * @param array $disabledMiddlewares Excludes middleware from being invoked when action is handled. * It is useful to avoid invoking one of the parent group middleware for * a certain route. * - * @param string|string[] $method HTTP method or list of methods. * @psalm-param list $middlewares */ public function __construct( From 69c2c62c1a74fc11d05f4c82ab867a2f943d15f3 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 10 Apr 2026 21:14:11 +0300 Subject: [PATCH 52/63] Add missing Group phpdoc --- src/Group.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Group.php b/src/Group.php index 7a3df203..6b31cdd8 100644 --- a/src/Group.php +++ b/src/Group.php @@ -46,10 +46,12 @@ final class Group private $corsMiddleware = null; /** + * @param string|null $prefix URL prefix to prepend to all routes of the group. * @param array[]|callable[]|string[] $middlewares Middleware definitions. * @param string[] $hosts List of host names. * @param string|null $namePrefix Prefix for route names. * @param array $disabledMiddlewares Excludes middleware from being invoked when action is handled. + * @param array|callable|string|null $corsMiddleware Middleware definition for CORS requests. * It is useful to avoid invoking one of the parent group middleware for * a certain route. * From 60bd8ee1f5cca014b4bb2b3e313846b6c3dd3577 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 10 Apr 2026 21:16:40 +0300 Subject: [PATCH 53/63] Remove unclear statement --- src/Attribute/Delete.php | 2 +- src/Attribute/Get.php | 2 +- src/Attribute/Head.php | 2 +- src/Attribute/Options.php | 2 +- src/Attribute/Patch.php | 2 +- src/Attribute/Post.php | 2 +- src/Attribute/Put.php | 2 +- src/Attribute/Route.php | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Attribute/Delete.php b/src/Attribute/Delete.php index 5b9d4f1a..13f5fd42 100644 --- a/src/Attribute/Delete.php +++ b/src/Attribute/Delete.php @@ -19,7 +19,7 @@ final class Delete implements RouteAttributeInterface /** * @param string $pattern Route pattern. - * @param string|null $name Route name. If not set, it will be generated automatically. + * @param string|null $name Route name. * @param array[]|callable[]|string[] $middlewares Middlewares to be added to the route. * @param array $defaults Parameter default values indexed by parameter names. * @param string[] $hosts Hosts that the route should match. diff --git a/src/Attribute/Get.php b/src/Attribute/Get.php index 902686fd..0828862d 100644 --- a/src/Attribute/Get.php +++ b/src/Attribute/Get.php @@ -19,7 +19,7 @@ final class Get implements RouteAttributeInterface /** * @param string $pattern Route pattern. - * @param string|null $name Route name. If not set, it will be generated automatically. + * @param string|null $name Route name. * @param array[]|callable[]|string[] $middlewares Middlewares to be added to the route. * @param array $defaults Parameter default values indexed by parameter names. * @param string[] $hosts Hosts that the route should match. diff --git a/src/Attribute/Head.php b/src/Attribute/Head.php index 4dd93287..60845dbc 100644 --- a/src/Attribute/Head.php +++ b/src/Attribute/Head.php @@ -19,7 +19,7 @@ final class Head implements RouteAttributeInterface /** * @param string $pattern Route pattern. - * @param string|null $name Route name. If not set, it will be generated automatically. + * @param string|null $name Route name. * @param array[]|callable[]|string[] $middlewares Middlewares to be added to the route. * @param array $defaults Parameter default values indexed by parameter names. * @param string[] $hosts Hosts that the route should match. diff --git a/src/Attribute/Options.php b/src/Attribute/Options.php index 7b1d4fad..cffa0115 100644 --- a/src/Attribute/Options.php +++ b/src/Attribute/Options.php @@ -19,7 +19,7 @@ final class Options implements RouteAttributeInterface /** * @param string $pattern Route pattern. - * @param string|null $name Route name. If not set, it will be generated automatically. + * @param string|null $name Route name. * @param array[]|callable[]|string[] $middlewares Middlewares to be added to the route. * @param array $defaults Parameter default values indexed by parameter names. * @param string[] $hosts Hosts that the route should match. diff --git a/src/Attribute/Patch.php b/src/Attribute/Patch.php index b6a4ce8c..1dbd8745 100644 --- a/src/Attribute/Patch.php +++ b/src/Attribute/Patch.php @@ -19,7 +19,7 @@ final class Patch implements RouteAttributeInterface /** * @param string $pattern Route pattern. - * @param string|null $name Route name. If not set, it will be generated automatically. + * @param string|null $name Route name. * @param array[]|callable[]|string[] $middlewares Middlewares to be added to the route. * @param array $defaults Parameter default values indexed by parameter names. * @param string[] $hosts Hosts that the route should match. diff --git a/src/Attribute/Post.php b/src/Attribute/Post.php index e5efc563..1a27f222 100644 --- a/src/Attribute/Post.php +++ b/src/Attribute/Post.php @@ -19,7 +19,7 @@ final class Post implements RouteAttributeInterface /** * @param string $pattern Route pattern. - * @param string|null $name Route name. If not set, it will be generated automatically. + * @param string|null $name Route name. * @param array[]|callable[]|string[] $middlewares Middlewares to be added to the route. * @param array $defaults Parameter default values indexed by parameter names. * @param string[] $hosts Hosts that the route should match. diff --git a/src/Attribute/Put.php b/src/Attribute/Put.php index 26b3e928..4ea7128c 100644 --- a/src/Attribute/Put.php +++ b/src/Attribute/Put.php @@ -19,7 +19,7 @@ final class Put implements RouteAttributeInterface /** * @param string $pattern Route pattern. - * @param string|null $name Route name. If not set, it will be generated automatically. + * @param string|null $name Route name. * @param array[]|callable[]|string[] $middlewares Middlewares to be added to the route. * @param array $defaults Parameter default values indexed by parameter names. * @param string[] $hosts Hosts that the route should match. diff --git a/src/Attribute/Route.php b/src/Attribute/Route.php index 496bcc96..c79fa205 100644 --- a/src/Attribute/Route.php +++ b/src/Attribute/Route.php @@ -19,7 +19,7 @@ final class Route implements RouteAttributeInterface /** * @param string[] $methods HTTP methods that the route should match. * @param string $pattern Route pattern. - * @param string|null $name Route name. If not set, it will be generated automatically. + * @param string|null $name Route name. * @param array[]|callable[]|string[] $middlewares Middlewares to be added to the route. * @param array $defaults Parameter default values indexed by parameter names. * @param string[] $hosts Hosts that the route should match. From 9626883899e0ea6781f253f64982a0b812591178 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Sat, 11 Apr 2026 00:56:14 +0300 Subject: [PATCH 54/63] Use cast instead of check to get an array from $method --- src/Route.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Route.php b/src/Route.php index b5a5bd51..8941e803 100644 --- a/src/Route.php +++ b/src/Route.php @@ -73,7 +73,7 @@ public function __construct( private bool $override = false, private array $disabledMiddlewares = [], ) { - $methods = is_string($method) ? [$method] : $method; + $methods = (array)$method; if (empty($methods)) { throw new InvalidArgumentException('$method cannot be empty.'); From e1ad7e82a882b0431c9bb5336996f81426e014c4 Mon Sep 17 00:00:00 2001 From: samdark <47294+samdark@users.noreply.github.com> Date: Fri, 10 Apr 2026 21:56:47 +0000 Subject: [PATCH 55/63] Apply PHP CS Fixer and Rector changes (CI) --- src/Route.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Route.php b/src/Route.php index 8941e803..99728503 100644 --- a/src/Route.php +++ b/src/Route.php @@ -73,7 +73,7 @@ public function __construct( private bool $override = false, private array $disabledMiddlewares = [], ) { - $methods = (array)$method; + $methods = (array) $method; if (empty($methods)) { throw new InvalidArgumentException('$method cannot be empty.'); From 8b5cdbc9db7f957e8b2a341b05c3efdadc9263c4 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Sat, 11 Apr 2026 01:02:47 +0300 Subject: [PATCH 56/63] Compare with empty array instead of using `empty()` --- src/Route.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Route.php b/src/Route.php index 99728503..4545519c 100644 --- a/src/Route.php +++ b/src/Route.php @@ -75,7 +75,7 @@ public function __construct( ) { $methods = (array) $method; - if (empty($methods)) { + if ($methods === []) { throw new InvalidArgumentException('$method cannot be empty.'); } $this->assertListOfStrings($methods, 'methods'); @@ -106,7 +106,7 @@ public function __toString(): string $result .= implode(',', $this->methods) . ' '; } - if (!empty($this->hosts)) { + if ($this->hosts !== []) { $quoted = array_map(static fn($host) => preg_quote($host, '/'), $this->hosts); if (!preg_match('/' . implode('|', $quoted) . '/', $this->pattern)) { From cc20fc283fac2adc5c5a06396cba17cc00b8d25c Mon Sep 17 00:00:00 2001 From: Rustam Date: Tue, 14 Apr 2026 21:56:19 +0500 Subject: [PATCH 57/63] Fix: suppress psalm warnings and enhance PHPDocs in Route and RouteCollector --- src/Route.php | 33 +++++++++++++++++++++++++++++++-- src/RouteCollector.php | 5 ++++- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/src/Route.php b/src/Route.php index 4545519c..3971eaf1 100644 --- a/src/Route.php +++ b/src/Route.php @@ -18,6 +18,8 @@ /** * Route defines a mapping from URL to callback / name and vice versa. + * + * @psalm-suppress DeprecatedMethod. Will be removed in the next major release. */ final class Route implements Stringable { @@ -293,7 +295,7 @@ public function hosts(string ...$hosts): self } /** - * Marks route as override. When added it will replace existing route with the same name. + * Marks route as override. When added, it will replace existing route with the same name. */ public function override(): self { @@ -406,8 +408,35 @@ public function disableMiddleware(mixed ...$definition): self * * @param string $key Data key to retrieve (`name`, `pattern`, `host`, `hosts`, `methods`, `defaults`, `override`, * `hasMiddlewares`, `enabledMiddlewares`). - * @return mixed The requested data. + * 1. `name` - route name. + * 2. `pattern` - route pattern. + * 3. `host` - first host requirement. + * 4. `hosts` - all host requirements. + * 5. `methods` - all HTTP methods. + * 6. `defaults` - all default parameter values. + * 7. `override` - whether the route is marked as override. + * 8. `hasMiddlewares` - whether the route has any middlewares. + * 9. `enabledMiddlewares` - all enabled middlewares. + * + * @psalm-template T as string + * + * @psalm-param T $key + * @psalm-return ( + * T is ('name'|'pattern') ? string : + * (T is 'host' ? string|null : + * (T is 'hosts' ? array : + * (T is 'methods' ? array : + * (T is 'defaults' ? array : + * (T is ('override'|'hasMiddlewares') ? bool : + * (T is 'enabledMiddlewares' ? array : mixed) + * ) + * ) + * ) + * ) + * ) + * ) * @throws InvalidArgumentException If the key is unknown. + * @return mixed The requested data. */ public function getData(string $key): mixed { diff --git a/src/RouteCollector.php b/src/RouteCollector.php index b0d5d3db..39d5dd94 100644 --- a/src/RouteCollector.php +++ b/src/RouteCollector.php @@ -10,6 +10,7 @@ * Simple route collector that manages routes, groups, and middleware definitions. * * @deprecated Will be removed in the next major release. + * @psalm-suppress DeprecatedInterface. Will be removed in the next major release. */ final class RouteCollector implements RouteCollectorInterface { @@ -71,12 +72,14 @@ public function prependMiddleware(array|callable|string ...$middlewareDefinition public function getItems(): array { if (!$this->providersAreInjected) { + $providerItems = []; foreach ($this->providers as $provider) { array_push( - $this->items, + $providerItems, ...$provider->getRoutes(), ); } + array_push($this->items, ...$providerItems); $this->providersAreInjected = true; } return $this->items; From 12bd3fc5df52047ba2a98ce3f0612b84c3079289 Mon Sep 17 00:00:00 2001 From: rustamwin <16498265+rustamwin@users.noreply.github.com> Date: Tue, 14 Apr 2026 16:57:00 +0000 Subject: [PATCH 58/63] Apply PHP CS Fixer and Rector changes (CI) --- src/Route.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Route.php b/src/Route.php index 3971eaf1..6f364e38 100644 --- a/src/Route.php +++ b/src/Route.php @@ -15,6 +15,7 @@ use function is_array; use function is_callable; use function is_string; +use function strval; /** * Route defines a mapping from URL to callback / name and vice versa. @@ -86,7 +87,7 @@ public function __construct( $this->middlewares = $middlewares; $this->methods = $methods; $this->hosts = $this->normalizeHosts($hosts); - $this->defaults = array_map(\strval(...), $defaults); + $this->defaults = array_map(strval(...), $defaults); if ($action !== null) { $this->middlewares[] = $action; $this->actionAdded = true; @@ -312,7 +313,7 @@ public function override(): self public function defaults(array $defaults): self { $route = clone $this; - $route->defaults = array_map(\strval(...), $defaults); + $route->defaults = array_map(strval(...), $defaults); return $route; } From 860f963a4f98abedb45c0675fdb5028c82a0e126 Mon Sep 17 00:00:00 2001 From: Rustam Date: Tue, 14 Apr 2026 22:04:39 +0500 Subject: [PATCH 59/63] Fix: suppress psalm warnings and improve PHPDocs in RouteCollection and Group --- src/Group.php | 26 +++++++++++++++++++++++++- src/RouteCollection.php | 1 + 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/Group.php b/src/Group.php index 6b31cdd8..62e0c739 100644 --- a/src/Group.php +++ b/src/Group.php @@ -207,8 +207,32 @@ public function disableMiddleware(mixed ...$definition): self * * @param string $key Data key to retrieve (`prefix`, `namePrefix`, `host`, `hosts`, `corsMiddleware`, `routes`, * `hasCorsMiddleware`, `enabledMiddlewares`). - * @return mixed The requested data. + * 1. `prefix` - URL prefix to prepend to all routes of the group. + * 2. `namePrefix` - Prefix for route names. + * 3. `host` - first host requirement. + * 4. `hosts` - all host requirements. + * 5. `corsMiddleware` - Middleware definition for CORS requests. + * 6. `routes` - routes or sub-groups to include in this group. + * 7. `hasCorsMiddleware` - whether the group has CORS middleware. + * 8. `enabledMiddlewares` - all enabled middlewares. + * + * @psalm-template T as string + * @psalm-param T $key + * * @throws InvalidArgumentException If the key is unknown. + * @return mixed The requested data. + * @psalm-return ( + * T is ('prefix'|'namePrefix'|'host') ? string|null : + * (T is 'routes' ? Group[]|Route[] : + * (T is 'hosts' ? array : + * (T is ('hasCorsMiddleware') ? bool : + * (T is 'enabledMiddlewares' ? list : + * (T is 'corsMiddleware' ? array|callable|string|null : mixed) + * ) + * ) + * ) + * ) + * ) */ public function getData(string $key): mixed { diff --git a/src/RouteCollection.php b/src/RouteCollection.php index 54bebe3d..ff834feb 100644 --- a/src/RouteCollection.php +++ b/src/RouteCollection.php @@ -33,6 +33,7 @@ final class RouteCollection implements RouteCollectionInterface /** * @param RouteCollectorInterface $collector The route collector to use. + * @psalm-suppress DeprecatedInterface. Will be removed in the next major release. */ public function __construct(private readonly RouteCollectorInterface $collector) {} From f6abda7fcb65ac6a76c9d0f361982816bdfa9e40 Mon Sep 17 00:00:00 2001 From: Rustam Date: Tue, 14 Apr 2026 22:23:41 +0500 Subject: [PATCH 60/63] Fix: suppress psalm warnings and enhance PHPDocs in RouteCollection --- src/RouteCollection.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/RouteCollection.php b/src/RouteCollection.php index ff834feb..47f85ed5 100644 --- a/src/RouteCollection.php +++ b/src/RouteCollection.php @@ -32,8 +32,9 @@ final class RouteCollection implements RouteCollectionInterface private array $routes = []; /** - * @param RouteCollectorInterface $collector The route collector to use. * @psalm-suppress DeprecatedInterface. Will be removed in the next major release. + * + * @param RouteCollectorInterface $collector The route collector to use. */ public function __construct(private readonly RouteCollectorInterface $collector) {} @@ -162,6 +163,7 @@ private function injectGroup(Group $group, array &$tree, string $prefix = '', st /** * @psalm-param Items $tree + * @psalm-suppress DeprecatedMethod. Will be removed in the next major release. */ private function processCors( Group $group, From a5b3a605144af177c8d2fa46341c801f8632632a Mon Sep 17 00:00:00 2001 From: Rustam Date: Tue, 14 Apr 2026 22:31:44 +0500 Subject: [PATCH 61/63] Fix: adjust psalm suppression placement in RouteCollection PHPDoc --- src/RouteCollection.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/RouteCollection.php b/src/RouteCollection.php index 47f85ed5..2e7fbe65 100644 --- a/src/RouteCollection.php +++ b/src/RouteCollection.php @@ -16,6 +16,7 @@ * Collection of routes that manages route registration and builds a route tree. * * @psalm-type Items = array + * @psalm-suppress DeprecatedInterface. Will be removed in the next major release. */ final class RouteCollection implements RouteCollectionInterface { @@ -32,8 +33,6 @@ final class RouteCollection implements RouteCollectionInterface private array $routes = []; /** - * @psalm-suppress DeprecatedInterface. Will be removed in the next major release. - * * @param RouteCollectorInterface $collector The route collector to use. */ public function __construct(private readonly RouteCollectorInterface $collector) {} From c080a1c5a8c216018502cc6fd61f860422cad50b Mon Sep 17 00:00:00 2001 From: Rustam Date: Wed, 15 Apr 2026 21:50:59 +0500 Subject: [PATCH 62/63] Kill mutation --- tests/RouteTest.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/RouteTest.php b/tests/RouteTest.php index a4fa97dc..79195c80 100644 --- a/tests/RouteTest.php +++ b/tests/RouteTest.php @@ -53,6 +53,14 @@ public function testEmptyMethods(): void new Route([], ''); } + public function testInvalidMethods(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid $methods provided, list of string expected.'); + + new Route([Method::GET, 1], ''); + } + public function testStringMethodConvertedToArray(): void { $route = new Route(Method::POST, '/'); From 2aa46f198636d822325c65174107dc6e4234bb90 Mon Sep 17 00:00:00 2001 From: Rustam Date: Sun, 19 Apr 2026 01:33:36 +0500 Subject: [PATCH 63/63] Refactor: mark `$container` as readonly in `RouterCollector` and update README with attributes usage example --- CHANGELOG.md | 2 ++ README.md | 32 +++++++++++++++++++++++++++++++- src/Debug/RouterCollector.php | 2 +- 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1862393e..083de625 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ - New #196: Add PHP Attributes support (@rustamwin) - New #196: Add `RoutesProviderInterface` interface providing routes from various resources (@rustamwin) - Bug #196: Fix the behavior of `Group::hosts()` method to be consistent with `Route::hosts()` method (@rustamwin) +- Chg #196: Make constructor of `Route` and `Group` classes public (@rustamwin) +- Chg #196: Deprecate static methods of `Route` and `Group` classes (@rustamwin) - Enh #276: Explicitly import classes, functions, and constants in the "use" section (@rustamwin) - Enh #277, #281: Remove restrictions from `prependMiddleware()` and `middleware()` methods (@klsoft-web, @vjik) diff --git a/README.md b/README.md index 508b5c7d..5166e74e 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ Additionally, you will need an adapter such as [FastRoute](https://github.com/yi ## Defining routes and URL matching -Common usage of the router looks like the following: +#### Common usage of the router looks like the following ```php use Yiisoft\Router\CurrentRoute; @@ -102,6 +102,36 @@ if (!$result->isSuccess()) { $response = $result->process($request, $notFoundHandler); ``` +#### Using attributes is also supported + +In controller: + +```php +use Yiisoft\Router\Attribute\Get; + +final class SiteController +{ + //... + + #[Get('/')] + public function home(ServerRequestInterface $request): ResponseInterface + { + return $this->responseFactory->createResponse()->withBody( + $this->streamFactory->createStream('You are at homepage.') + ); + } + + #[Get('/test/{id:\w+}')] + public function test(CurrentRoute $currentRoute): ResponseInterface + { + $id = $currentRoute->getArgument('id'); + + return $this->responseFactory->createResponse()->withBody( + $this->streamFactory->createStream('You are at test with argument ' . $id) + ); +} +``` + > Note: Despite `UrlGeneratorInterface` and `UrlMatcherInterface` being common for all adapters available, certain > features and, especially, pattern syntax may differ. To check usage and configuration details, please refer > to specific adapter documentation. All examples in this document are for diff --git a/src/Debug/RouterCollector.php b/src/Debug/RouterCollector.php index 24714da2..f1dfa68a 100644 --- a/src/Debug/RouterCollector.php +++ b/src/Debug/RouterCollector.php @@ -21,7 +21,7 @@ final class RouterCollector implements SummaryCollectorInterface private float $matchTime = 0; - public function __construct(private ContainerInterface $container) {} + public function __construct(private readonly ContainerInterface $container) {} public function collect(float $matchTime): void {