diff --git a/.phpstorm.meta.php/Group.php b/.phpstorm.meta.php/Group.php index 70eb3db..6b8d6a0 100644 --- a/.phpstorm.meta.php/Group.php +++ b/.phpstorm.meta.php/Group.php @@ -13,7 +13,6 @@ 'corsMiddleware', 'items', 'middlewareDefinitions', - 'hasDispatcher', '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/README.md b/README.md index c4792c5..a48789d 100644 --- a/README.md +++ b/README.md @@ -386,6 +386,30 @@ In addition to commonly used `getArgument()` method, the following methods are a - `getMethods()` - To get route methods. - `getUri()` - To get current URI. +## Attributes + +`Route` class can be used as an attribute: + +```php +use Yiisoft\Router\Route; + +class Controller +{ + #[Route( + methods: ['GET', 'POST'], + pattern: '/', + )] + public function view() + { + // ... + } +} +``` + +Only two properties are required: `methods` and `pattern`. The rest properties are optional. + +> Parsing attributes is up to user. The package does not provide a solution for parsing and constructing routes tree. + ## Testing ### Unit testing diff --git a/src/Group.php b/src/Group.php index d2a2654..9fd6fd0 100644 --- a/src/Group.php +++ b/src/Group.php @@ -6,7 +6,6 @@ use InvalidArgumentException; use RuntimeException; -use Yiisoft\Middleware\Dispatcher\MiddlewareDispatcher; use function in_array; @@ -36,7 +35,7 @@ final class Group */ private $corsMiddleware = null; - private function __construct(private ?string $prefix = null, private ?MiddlewareDispatcher $dispatcher = null) + private function __construct(private ?string $prefix = null) { } @@ -44,13 +43,11 @@ private function __construct(private ?string $prefix = null, private ?Middleware * 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 ): self { - return new self($prefix, $dispatcher); + return new self($prefix); } public function routes(self|Route ...$routes): self @@ -59,32 +56,13 @@ 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->items = $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. @@ -175,12 +153,14 @@ public function disableMiddleware(mixed ...$middlewareDefinition): self /** * @psalm-template T as string + * * @psalm-param T $key + * * @psalm-return ( * T is ('prefix'|'namePrefix'|'host') ? string|null : * (T is 'items' ? Group[]|Route[] : * (T is 'hosts' ? array : - * (T is ('hasCorsMiddleware'|'hasDispatcher') ? bool : + * (T is ('hasCorsMiddleware'') ? bool : * (T is 'middlewareDefinitions' ? list : * (T is 'corsMiddleware' ? array|callable|string|null : mixed) * ) @@ -199,7 +179,6 @@ public function getData(string $key): mixed 'corsMiddleware' => $this->corsMiddleware, 'items' => $this->items, 'hasCorsMiddleware' => $this->corsMiddleware !== null, - 'hasDispatcher' => $this->dispatcher !== null, 'middlewareDefinitions' => $this->getMiddlewareDefinitions(), default => throw new InvalidArgumentException('Unknown data key: ' . $key), }; diff --git a/src/MatchingResult.php b/src/MatchingResult.php index 30c5918..d71d7d9 100644 --- a/src/MatchingResult.php +++ b/src/MatchingResult.php @@ -97,18 +97,29 @@ public function route(): Route public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { - if (!$this->isSuccess()) { - return $handler->handle($request); + if ($this->isSuccess() && $this->dispatcher !== null) { + return $this + ->buildDispatcher($this->dispatcher, $this->route) + ->dispatch($request, $handler); } - // Inject dispatcher only if we have not previously injected. + return $handler->handle($request); + } + + private function buildDispatcher( + ?MiddlewareDispatcher $dispatcher, + Route $route, + ): MiddlewareDispatcher { + if ($dispatcher === null) { + throw new RuntimeException(sprintf('There is no dispatcher in the route %s.', $route->getData('name'))); + } + + // Don't add middlewares to dispatcher if we did it earlier. // This improves performance in event-loop applications. - if ($this->dispatcher !== null && !$this->route->getData('hasDispatcher')) { - $this->route->injectDispatcher($this->dispatcher); + if ($dispatcher->hasMiddlewares()) { + return $dispatcher; } - return $this->route - ->getData('dispatcherWithMiddlewares') - ->dispatch($request, $handler); + return $dispatcher->withMiddlewares($route->getMiddlewares()); } } diff --git a/src/Route.php b/src/Route.php index 2712769..e4a35e8 100644 --- a/src/Route.php +++ b/src/Route.php @@ -4,98 +4,83 @@ 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. */ +#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] final class Route implements Stringable { - private ?string $name = null; - - /** - * @var string[] - */ - private array $hosts = []; - private bool $override = false; private bool $actionAdded = false; + private array $builtMiddlewares = []; /** - * @var array[]|callable[]|string[] - */ - private array $middlewareDefinitions = []; - - private array $disabledMiddlewareDefinitions = []; - - /** - * @var array - */ - private array $defaults = []; - - /** - * @param string[] $methods + * @param string[] $methods HTTP method names + * @param string[] $hosts + * @param array[]|callable[]|string[] $middlewares + * @param array $disabledMiddlewares Excludes middleware from being invoked when action is handled. + * It is useful to avoid invoking one of the parent group middleware for + * a certain route. + * @param bool $override Marks route as override. When added it will replace existing route with the same name. + * @param array $defaults Parameter default values indexed by parameter names. + * + * @psalm-param array $defaults */ - 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 $disabledMiddlewares = [], + private array $hosts = [], + private bool $override = false, + private array $defaults = [], ) { + $this->methods = $this->processMethods($this->methods); + $this->hosts = $this->processHosts($this->hosts); + $this->defaults = $this->processDefaults($this->defaults); } - /** - * @psalm-assert MiddlewareDispatcher $this->dispatcher - */ - public function injectDispatcher(MiddlewareDispatcher $dispatcher): void + public static function get(string $pattern): self { - $this->dispatcher = $dispatcher; + return self::methods([Method::GET], $pattern); } - public function withDispatcher(MiddlewareDispatcher $dispatcher): self + public static function post(string $pattern): self { - $route = clone $this; - $route->dispatcher = $dispatcher; - return $route; + return self::methods([Method::POST], $pattern); } - public static function get(string $pattern, ?MiddlewareDispatcher $dispatcher = null): self + public static function put(string $pattern): self { - return self::methods([Method::GET], $pattern, $dispatcher); + return self::methods([Method::PUT], $pattern); } - public static function post(string $pattern, ?MiddlewareDispatcher $dispatcher = null): self + public static function delete(string $pattern): self { - return self::methods([Method::POST], $pattern, $dispatcher); + return self::methods([Method::DELETE], $pattern); } - public static function put(string $pattern, ?MiddlewareDispatcher $dispatcher = null): self + public static function patch(string $pattern): self { - return self::methods([Method::PUT], $pattern, $dispatcher); + return self::methods([Method::PATCH], $pattern); } - public static function delete(string $pattern, ?MiddlewareDispatcher $dispatcher = null): self + public static function head(string $pattern): self { - return self::methods([Method::DELETE], $pattern, $dispatcher); + return self::methods([Method::HEAD], $pattern); } - public static function patch(string $pattern, ?MiddlewareDispatcher $dispatcher = null): self + public static function options(string $pattern): self { - return self::methods([Method::PATCH], $pattern, $dispatcher); - } - - public static function head(string $pattern, ?MiddlewareDispatcher $dispatcher = null): self - { - return self::methods([Method::HEAD], $pattern, $dispatcher); - } - - public static function options(string $pattern, ?MiddlewareDispatcher $dispatcher = null): self - { - return self::methods([Method::OPTIONS], $pattern, $dispatcher); + return self::methods([Method::OPTIONS], $pattern); } /** @@ -104,9 +89,8 @@ public static function options(string $pattern, ?MiddlewareDispatcher $dispatche public static function methods( array $methods, string $pattern, - ?MiddlewareDispatcher $dispatcher = null ): self { - return new self($methods, $pattern, $dispatcher); + return new self($methods, $pattern); } public function name(string $name): self @@ -131,15 +115,8 @@ public function host(string $host): self public function hosts(string ...$hosts): self { $route = clone $this; - $route->hosts = []; - - foreach ($hosts as $host) { - $host = rtrim($host, '/'); - if ($host !== '' && !in_array($host, $route->hosts, true)) { - $route->hosts[] = $host; - } - } + $route->hosts = $this->processHosts($hosts); return $route; } @@ -162,7 +139,7 @@ public function override(): self public function defaults(array $defaults): self { $route = clone $this; - $route->defaults = array_map('\strval', $defaults); + $route->defaults = $this->processDefaults($defaults); return $route; } @@ -177,9 +154,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 +172,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,7 +185,8 @@ 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->builtMiddlewares = []; $route->actionAdded = true; return $route; } @@ -220,24 +200,25 @@ public function disableMiddleware(mixed ...$middlewareDefinition): self { $route = clone $this; array_push( - $route->disabledMiddlewareDefinitions, + $route->disabledMiddlewares, ...array_values($middlewareDefinition) ); + $route->builtMiddlewares = []; return $route; } /** * @psalm-template T as string + * * @psalm-param T $key + * * @psalm-return ( * T is ('name'|'pattern') ? string : * (T is 'host' ? string|null : * (T is 'hosts' ? array : * (T is 'methods' ? array : * (T is 'defaults' ? array : - * (T is ('override'|'hasMiddlewares'|'hasDispatcher') ? bool : - * (T is 'dispatcherWithMiddlewares' ? MiddlewareDispatcher : mixed) - * ) + * (T is ('override'|'hasMiddlewares') ? bool : mixed) * ) * ) * ) @@ -255,9 +236,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 +274,63 @@ public function __debugInfo() 'defaults' => $this->defaults, 'override' => $this->override, 'actionAdded' => $this->actionAdded, - 'middlewareDefinitions' => $this->middlewareDefinitions, - 'disabledMiddlewareDefinitions' => $this->disabledMiddlewareDefinitions, - 'middlewareDispatcher' => $this->dispatcher, + 'middlewareDefinitions' => $this->middlewares, + 'builtMiddlewares' => $this->builtMiddlewares, + 'disabledMiddlewareDefinitions' => $this->disabledMiddlewares, ]; } - private function getDispatcherWithMiddlewares(): MiddlewareDispatcher + public function getMiddlewares(): array { - if ($this->dispatcher === null) { - throw new RuntimeException(sprintf('There is no dispatcher in the route %s.', $this->getData('name'))); + if ($this->builtMiddlewares !== []) { + return $this->builtMiddlewares; } - - // Don't add middlewares to dispatcher if we did it earlier. - // This improves performance in event-loop applications. - if ($this->dispatcher->hasMiddlewares()) { - return $this->dispatcher; + $result = $this->middlewares; + /** @var mixed $definition */ + foreach ($result as $index => $definition) { + if (in_array($definition, $this->disabledMiddlewares, true)) { + unset($result[$index]); + } } + $this->builtMiddlewares = $result; - /** @var mixed $definition */ - foreach ($this->middlewareDefinitions as $index => $definition) { - if (in_array($definition, $this->disabledMiddlewareDefinitions, true)) { - unset($this->middlewareDefinitions[$index]); + return $result; + } + + private function processDefaults(array $defaults): array + { + return array_map('\strval', $defaults); + } + + private function processHosts(array $hosts): array + { + $result = []; + foreach ($hosts as $host) { + $host = rtrim($host, '/'); + + if ($host !== '' && !in_array($host, $result, true)) { + $result[] = $host; } } + return $result; + } - return $this->dispatcher = $this->dispatcher->withMiddlewares($this->middlewareDefinitions); + private function processMethods(array $methods): array + { + $result = []; + foreach ($methods as $method) { + $method = strtoupper($method); + if (!in_array($method, Method::ALL, true)) { + throw new \InvalidArgumentException( + sprintf( + 'Method "%s" is not supported. Possible methods: "%s".', + $method, + implode('", "', Method::ALL), + ) + ); + } + $result[] = $method; + } + return $result; } } diff --git a/tests/GroupTest.php b/tests/GroupTest.php index 9e8999c..9130acb 100644 --- a/tests/GroupTest.php +++ b/tests/GroupTest.php @@ -23,7 +23,6 @@ use Yiisoft\Router\Tests\Support\TestMiddleware1; use Yiisoft\Router\Tests\Support\TestMiddleware2; use Yiisoft\Router\Tests\Support\TestMiddleware3; -use Yiisoft\Test\Support\Container\SimpleContainer; final class GroupTest extends TestCase { @@ -80,6 +79,7 @@ public function testRoutesAfterMiddleware(): void public function testAddNestedMiddleware(): void { + $dispatcher = $this->getDispatcher(); $request = new ServerRequest('GET', '/outergroup/innergroup/test1'); $action = static fn (ServerRequestInterface $request) => new Response(200, [], null, '1.1', implode($request->getAttributes())); @@ -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 = $dispatcher + ->withMiddlewares($route->getMiddlewares()) ->dispatch($request, $this->getRequestHandler()); $this->assertSame(200, $response->getStatusCode()); $this->assertSame('middleware2', $response->getReasonPhrase()); @@ -120,6 +120,7 @@ public function testAddNestedMiddleware(): void public function testGroupMiddlewareFullStackCalled(): void { + $dispatcher = $this->getDispatcher(); $request = new ServerRequest('GET', '/group/test1'); $action = static fn (ServerRequestInterface $request) => new Response(200, [], null, '1.1', implode($request->getAttributes())); @@ -132,7 +133,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 +147,8 @@ public function testGroupMiddlewareFullStackCalled(): void $routeCollection = new RouteCollection($collector); $route = $routeCollection->getRoute('request1'); - $response = $route - ->getData('dispatcherWithMiddlewares') + $response = $dispatcher + ->withMiddlewares($route->getMiddlewares()) ->dispatch($request, $this->getRequestHandler()); $this->assertSame(200, $response->getStatusCode()); $this->assertSame('middleware2', $response->getReasonPhrase()); @@ -155,13 +156,14 @@ public function testGroupMiddlewareFullStackCalled(): void public function testGroupMiddlewareStackInterrupted(): void { + $dispatcher = $this->getDispatcher(); $request = new ServerRequest('GET', '/group/test1'); $action = static fn () => new Response(200); $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,8 +177,8 @@ public function testGroupMiddlewareStackInterrupted(): void $routeCollection = new RouteCollection($collector); $route = $routeCollection->getRoute('request1'); - $response = $route - ->getData('dispatcherWithMiddlewares') + $response = $dispatcher + ->withMiddlewares($route->getMiddlewares()) ->dispatch($request, $this->getRequestHandler()); $this->assertSame(403, $response->getStatusCode()); } @@ -259,54 +261,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() @@ -440,15 +394,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()); @@ -475,16 +423,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..578e6c1 100644 --- a/tests/MatchingResultTest.php +++ b/tests/MatchingResultTest.php @@ -55,8 +55,8 @@ public function testProcessSuccess(): void new MiddlewareFactory($container), $this->createMock(EventDispatcherInterface::class) ); - $route = Route::post('/', $dispatcher)->middleware($this->getMiddleware()); - $result = MatchingResult::fromSuccess($route, []); + $route = Route::post('/')->middleware($this->getMiddleware()); + $result = MatchingResult::fromSuccess($route, [])->withDispatcher($dispatcher); $request = new ServerRequest('POST', '/'); $response = $result->process($request, $this->getRequestHandler()); diff --git a/tests/RouteCollectionTest.php b/tests/RouteCollectionTest.php index 23967fe..6f3fd18 100644 --- a/tests/RouteCollectionTest.php +++ b/tests/RouteCollectionTest.php @@ -97,7 +97,7 @@ public function testRouteWithoutAction(): void $group = Group::create() ->middleware(fn () => 1) ->routes( - Route::get('/test', $this->getDispatcher()) + Route::get('/test') ->action(fn () => 2) ->name('test'), Route::get('/images/{sile}')->name('image') @@ -115,19 +115,19 @@ public function testGetRouterTree(): void { $group1 = Group::create('/api') ->routes( - Route::get('/test', $this->getDispatcher()) + Route::get('/test') ->action(fn () => 2) ->name('/test'), Route::get('/images/{sile}')->name('/image'), Group::create('/v1') ->routes( - Route::get('/posts', $this->getDispatcher())->name('/posts'), + Route::get('/posts')->name('/posts'), Route::get('/post/{sile}')->name('/post/view') ) ->namePrefix('/v1'), Group::create('/v1') ->routes( - Route::get('/tags', $this->getDispatcher())->name('/tags'), + Route::get('/tags')->name('/tags'), Route::get('/tag/{slug}')->name('/tag/view'), ) ->namePrefix('/v1'), @@ -135,7 +135,7 @@ public function testGetRouterTree(): void $group2 = Group::create('/api') ->routes( - Route::get('/posts', $this->getDispatcher())->name('/posts'), + Route::get('/posts')->name('/posts'), Route::get('/post/{sile}')->name('/post/view'), ) ->namePrefix('/api'); @@ -168,7 +168,7 @@ public function testGetRoutes(): void $group = Group::create() ->middleware(fn () => 1) ->routes( - Route::get('/test', $this->getDispatcher()) + Route::get('/test') ->action(fn () => 2) ->name('test'), Route::get('/images/{sile}')->name('image') @@ -247,6 +247,7 @@ public function testGroupName(): void public function testCollectorMiddlewareFullstackCalled(): void { + $dispatcher = $this->getDispatcher(); $action = fn (ServerRequestInterface $request) => new Response( 200, [], @@ -257,11 +258,11 @@ public function testCollectorMiddlewareFullstackCalled(): void $listRoute = Route::get('/') ->action($action) ->name('list'); - $viewRoute = Route::get('/{id}', $this->getDispatcher()) + $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'); @@ -276,11 +277,11 @@ public function testCollectorMiddlewareFullstackCalled(): void $route1 = $routeCollection->getRoute('list'); $route2 = $routeCollection->getRoute('view'); $request = new ServerRequest('GET', '/'); - $response1 = $route1 - ->getData('dispatcherWithMiddlewares') + $response1 = $dispatcher + ->withMiddlewares($route1->getMiddlewares()) ->dispatch($request, $this->getRequestHandler()); - $response2 = $route2 - ->getData('dispatcherWithMiddlewares') + $response2 = $dispatcher + ->withMiddlewares($route2->getMiddlewares()) ->dispatch($request, $this->getRequestHandler()); $this->assertEquals('middleware1', $response1->getReasonPhrase()); @@ -327,9 +328,8 @@ public function testMiddlewaresOrder(bool $groupWrapped): void ); $route = (new RouteCollection($collector))->getRoute('main'); - $route->injectDispatcher($injectDispatcher); - $dispatcher = $route->getData('dispatcherWithMiddlewares'); + $dispatcher = $injectDispatcher->withMiddlewares($route->getMiddlewares()); $response = $dispatcher->dispatch($request, $this->getRequestHandler()); $this->assertSame(200, $response->getStatusCode()); @@ -354,9 +354,8 @@ public function testStaticRouteWithCollectorMiddlewares(): void ); $route = (new RouteCollection($collector))->getRoute('image'); - $route->injectDispatcher($injectDispatcher); - $dispatcher = $route->getData('dispatcherWithMiddlewares'); + $dispatcher = $injectDispatcher->withMiddlewares($route->getMiddlewares()); $this->expectException(RuntimeException::class); $this->expectExceptionMessage('Stack is empty.'); diff --git a/tests/RouteTest.php b/tests/RouteTest.php index d2cc255..ebfcfe3 100644 --- a/tests/RouteTest.php +++ b/tests/RouteTest.php @@ -19,11 +19,10 @@ 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; -use Yiisoft\Test\Support\Container\SimpleContainer; final class RouteTest extends TestCase { @@ -212,23 +211,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 +246,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->getMiddlewares()); $response = $dispatcher->dispatch($request, $this->getRequestHandler()); $this->assertSame(200, $response->getStatusCode()); @@ -290,24 +271,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->getMiddlewares()); $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', '/'); @@ -326,12 +297,7 @@ public function testGetDispatcherWithMiddlewares(): void [TestController::class, 'index'], ]); - $route = Route::get('/'); - $route->injectDispatcher($injectDispatcher); - - $dispatcher = $route->getData('dispatcherWithMiddlewares'); - - $response = $dispatcher->dispatch($request, $this->getRequestHandler()); + $response = $injectDispatcher->dispatch($request, $this->getRequestHandler()); $this->assertSame(200, $response->getStatusCode()); $this->assertSame('12', (string) $response->getBody()); } @@ -377,12 +343,14 @@ public function testDebugInfo(): void [2] => go ) + [builtMiddlewares] => Array + ( + ) + [disabledMiddlewareDefinitions] => Array ( [0] => Yiisoft\Router\Tests\Support\TestMiddleware2 ) - - [middlewareDispatcher] => ) EOL; @@ -399,15 +367,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('')); @@ -420,6 +382,40 @@ public function testImmutability(): void $this->assertNotSame($route, $route->disableMiddleware('')); } + public function testAttributes(): void + { + $methods = ['GET', 'poSt']; + $pattern = 'user/{id}/{method}'; + $name = 'user-method'; + $middlewares = []; + $disabledMiddlewareDefinitions = []; + $hosts = ['localhost/', '127.0.0.*///']; + $override = true; + $defaults = ['id' => 123, 'method' => 'delete', 'flag1' => true, 'flag2' => false]; + + $route = new Route( + methods: $methods, + pattern: $pattern, + name: $name, + middlewares: $middlewares, + disabledMiddlewares: $disabledMiddlewareDefinitions, + hosts: $hosts, + override: $override, + defaults: $defaults, + ); + + $this->assertSame(['GET', 'POST'], $route->getData('methods')); + $this->assertSame($pattern, $route->getData('pattern')); + $this->assertSame($name, $route->getData('name')); + $this->assertSame(false, $route->getData('hasMiddlewares')); + $this->assertSame(['localhost', '127.0.0.*'], $route->getData('hosts')); + $this->assertSame($override, $route->getData('override')); + $this->assertSame( + ['id' => '123', 'method' => 'delete', 'flag1' => '1', 'flag2' => ''], + $route->getData('defaults'), + ); + } + private function getRequestHandler(): RequestHandlerInterface { return new class () implements RequestHandlerInterface {