From a60d03bccc8f73b776393e4f3f8ba80489c497f8 Mon Sep 17 00:00:00 2001 From: Rustam Date: Tue, 28 Mar 2023 15:20:37 +0500 Subject: [PATCH 01/27] 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 70eb3db..0facbb6 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 e30885c..ae15f02 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 0000000..acab5a4 --- /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 30c5918..bf8d7b3 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 631823f..258ff36 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 2712769..8a2af6f 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 629ed54..a753547 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 a091bc9..1453e4c 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 b75ec6b..fc4b68e 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 99b0ca1..2e8edbe 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 d2cc255..4beed80 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/27] 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 acab5a4..a9b4c55 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/27] 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 16efb62..74541b6 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/27] 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 cf67dd8..244a29f 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 34e9737..6f5d03f 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/27] 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 0000000..b94d045 --- /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 0000000..6188756 --- /dev/null +++ b/src/RouteAttributesRegistrarInterface.php @@ -0,0 +1,18 @@ + Date: Mon, 12 Jun 2023 14:28:54 +0000 Subject: [PATCH 06/27] 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 b94d045..868c10b 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/27] 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 868c10b..c6c02f9 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 0000000..567fce2 --- /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 0000000..aad7612 --- /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 0000000..ab3925b --- /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 0000000..35f6fe8 --- /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 0000000..21b902d --- /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 0000000..80c3fc0 --- /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 0000000..af5f576 --- /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 0000000..0cd4764 --- /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 44c1789..6d3c4fa 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/27] 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 a9b4c55..6a3f752 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 f802957..2d5a541 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 797537c..1c4e7a7 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 9a8797d..f452150 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 7e7675f..b587a6f 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 cd4561f..5961f40 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 eb69c02..aff8b53 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 23c11ba..38ade3c 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 5445b2f..5d5a3f5 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 c6c02f9..3e5855d 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 0cd4764..0372aee 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 6d3c4fa..57147cf 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/27] 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 6a3f752..6ed574c 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 2d5a541..007a656 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 1c4e7a7..fb5f615 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 f452150..9b35213 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 b587a6f..c16cbee 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 5961f40..6c3dfe8 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 aff8b53..1f4bde5 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 5d5a3f5..d30bb3b 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 0372aee..9ece4e3 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/27] 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 38ade3c..baddaf3 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 d30bb3b..4932970 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 c711fb8..569151b 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 6f5d03f..7600f6f 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/27] 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 569151b..3d57bbd 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/27] 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 ae15f02..1b0dad0 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 258ff36..e882151 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 4932970..9f58b92 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 3d57bbd..5057c33 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 9ece4e3..66bad2c 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 47922ae..86eb217 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 7600f6f..27c33e0 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/27] 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 9f58b92..f79bab0 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 27c33e0..d10b046 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/27] 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 f79bab0..b09d79c 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 567fce2..5eec0b1 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 aad7612..659daff 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 ab3925b..9943acc 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 35f6fe8..fa44dbf 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 21b902d..fca22ea 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 80c3fc0..e4011e0 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 af5f576..8d2c43f 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 d10b046..6e0f146 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/27] 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 0bf73fd..826b44a 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 0000000..83abd22 --- /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 0000000..373227b --- /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 0000000..ee130ee --- /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 0000000..60dc2f3 --- /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 6188756..0000000 --- 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 38c5861..c64246d 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 0000000..edbc7c1 --- /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 0000000..c346991 --- /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 66bad2c..0000000 --- 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 5fa1ea1..ffbf7ac 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 0000000..30129aa --- /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 0000000..94ec291 --- /dev/null +++ b/tests/Support/resources/test.php @@ -0,0 +1,6 @@ + Date: Wed, 11 Oct 2023 16:35:14 +0500 Subject: [PATCH 16/27] 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 373227b..829845d 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/27] 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 6ed574c..cb46448 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 007a656..0a3c555 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 fb5f615..eb5f44c 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 9b35213..206f2f3 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 c16cbee..dbd6c66 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 6c3dfe8..7cbb177 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 1f4bde5..87d15cf 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/27] 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 829845d..d9ee9ba 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/27] 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 cb46448..f05af35 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 0a3c555..82f3ead 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 eb5f44c..9ec6fdf 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 206f2f3..3bd889a 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 dbd6c66..5095c74 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 7cbb177..df79819 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 87d15cf..83dab1d 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 0000000..7640924 --- /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 0000000..cecfa63 --- /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 2ef9443..e58fc44 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 5eec0b1..66acb37 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 659daff..855f156 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 9943acc..6e092f5 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 fa44dbf..fa38d0e 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 fca22ea..37121a2 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 e4011e0..87f3c9b 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 8d2c43f..4c6ebd1 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/27] 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 0facbb6..ead6379 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 1b0dad0..2ddb23e 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 f05af35..b06b29b 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 82f3ead..032fe58 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 9ec6fdf..6ef954a 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 3bd889a..ed823fa 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 5095c74..377130d 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 df79819..9e58cdf 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 83dab1d..e5404df 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 7640924..29d668c 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 baddaf3..f0b6412 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 e882151..a1afc59 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 e58fc44..40e491c 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 a753547..fa3aa13 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 5057c33..3d1c75c 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 86eb217..a0d788f 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 6e0f146..898b877 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/27] 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 34286ea..289c401 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 ee130ee..42c34e9 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 40e491c..95d4564 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 0000000..930e916 --- /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/27] 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 42c34e9..e0c17c0 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/27] 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 826b44a..acaf35e 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 575f63d..0000000 --- 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 898b877..5023666 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/27] 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 289c401..34286ea 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 acaf35e..0bf73fd 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/27] Minor --- src/Route.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Route.php b/src/Route.php index 95d4564..cf137e4 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/27] Add changelog --- CHANGELOG.md | 6 ++++++ README.md | 1 + 2 files changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fda7b26..8b7cac5 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 c4792c5..06e2a57 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/27] 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 3d1c75c..55c7d6d 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 a0d788f..7b46289 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)