diff --git a/src/Route.php b/src/Route.php index d5940701..b13d86db 100644 --- a/src/Route.php +++ b/src/Route.php @@ -18,6 +18,7 @@ final class Route implements Stringable { private ?string $name = null; + private ?string $alias = null; /** * @var string[] @@ -103,6 +104,13 @@ public function name(string $name): self return $route; } + public function alias(string $alias): self + { + $route = clone $this; + $route->alias = $alias; + return $route; + } + public function pattern(string $pattern): self { $new = clone $this; @@ -134,10 +142,10 @@ public function hosts(string ...$hosts): self /** * Marks route as override. When added it will replace existing route with the same name. */ - public function override(): self + public function override(bool $override = true): self { $route = clone $this; - $route->override = true; + $route->override = $override; return $route; } @@ -247,6 +255,7 @@ public function disableMiddleware(mixed ...$definition): self public function getData(string $key): mixed { return match ($key) { + 'alias' => $this->alias, 'name' => $this->name ?? (implode(', ', $this->methods) . ' ' . implode('|', $this->hosts) . $this->pattern), 'pattern' => $this->pattern, diff --git a/src/RouteCollection.php b/src/RouteCollection.php index 01be0868..900e4183 100644 --- a/src/RouteCollection.php +++ b/src/RouteCollection.php @@ -17,6 +17,10 @@ */ final class RouteCollection implements RouteCollectionInterface { + /** + * @var Route[] + */ + private array $aliases = []; /** * @psalm-var Items */ @@ -73,26 +77,48 @@ private function injectItems(array $items): void if (!$this->isStaticRoute($item)) { $item = $item->prependMiddleware(...$this->collector->getMiddlewareDefinitions()); } + if ($item instanceof Group) { + $this->injectGroup($item, $this->items); + continue; + } $this->injectItem($item); } + foreach ($this->aliases as $alias) { + $referencedRouteName = $alias->getData('alias'); + if (!isset($this->routes[$referencedRouteName])) { + throw new \Exception('Referenced route for alias ' . $referencedRouteName . ' is not found.'); + } + $referencedRoute = $this->routes[$referencedRouteName]; + $referencedRoute = $referencedRoute->pattern($alias->getData('pattern')); + if ($hosts = $alias->getData('hosts')) { + $referencedRoute = $referencedRoute->hosts($hosts); + } + if ($name = $alias->getData('name')) { + $referencedRoute = $referencedRoute->name($name); + } + if ($defaults = $alias->getData('defaults')) { + $referencedRoute = $referencedRoute->defaults($defaults); + } + if ($alias->getData('override')) { + $referencedRoute = $referencedRoute->override(); + } + $routeName = $alias->getData('name'); + $this->routes[$routeName] = $referencedRoute; + } } /** * Add an item into routes array. */ - private function injectItem(Group|Route $route): void + private function injectItem(Route $route): void { - if ($route instanceof Group) { - $this->injectGroup($route, $this->items); + if ($this->isAliasRoute($route)) { + $this->aliases[] = $route; return; } - $routeName = $route->getData('name'); $this->items[] = $routeName; - if (isset($this->routes[$routeName]) && !$route->getData('override')) { - throw new InvalidArgumentException("A route with name '$routeName' already exists."); - } - $this->routes[$routeName] = $route; + $this->injectRoute($routeName, $route); } /** @@ -149,10 +175,11 @@ private function injectGroup(Group $group, array &$tree, string $prefix = '', st $routeName = $modifiedItem->getData('name'); $tree[] = $routeName; - if (isset($this->routes[$routeName]) && !$modifiedItem->getData('override')) { - throw new InvalidArgumentException("A route with name '$routeName' already exists."); + if ($this->isAliasRoute($modifiedItem)) { + $this->aliases[] = $modifiedItem; + return; } - $this->routes[$routeName] = $modifiedItem; + $this->injectRoute($routeName, $modifiedItem); } } @@ -212,4 +239,17 @@ private function isStaticRoute(Group|Route $item): bool { return $item instanceof Route && !$item->getData('hasMiddlewares'); } + + protected function injectRoute(string $routeName, Route $route): void + { + if (isset($this->routes[$routeName]) && !$route->getData('override')) { + throw new InvalidArgumentException("A route with name '$routeName' already exists."); + } + $this->routes[$routeName] = $route; + } + + private function isAliasRoute(Route $route): bool + { + return $route->getData('alias') !== null; + } } diff --git a/tests/RouteCollectionTest.php b/tests/RouteCollectionTest.php index 72d71b91..d16cef80 100644 --- a/tests/RouteCollectionTest.php +++ b/tests/RouteCollectionTest.php @@ -15,6 +15,7 @@ use RuntimeException; use Yiisoft\Middleware\Dispatcher\MiddlewareDispatcher; use Yiisoft\Middleware\Dispatcher\MiddlewareFactory; +use Yiisoft\Router\CurrentRoute; use Yiisoft\Router\Group; use Yiisoft\Router\Route; use Yiisoft\Router\RouteCollection; @@ -245,6 +246,33 @@ public function testGroupName(): void $this->assertInstanceOf(Route::class, $route4); } + public function testGroupAlias(): void + { + $group = Group::create() + ->routes( + Route::get('/user/{username}') + ->name('user/profile') + ->override() + ->hosts('google.com', 'yandex.com') + ->defaults(['username' => 'xepozz']), + Route::get('/profile/{username}') + ->alias('user/profile') + ->name('profile'), + ); + + $collector = new RouteCollector(); + $collector->addRoute($group); + + $routeCollection = new RouteCollection($collector); + $route1 = $routeCollection->getRoute('user/profile'); + $route2 = $routeCollection->getRoute('profile'); + $this->assertEquals($route1->getData('override'), $route2->getData('override')); + $this->assertEquals($route1->getData('defaults'), $route2->getData('defaults')); + $this->assertEquals($route1->getData('hosts'), $route2->getData('hosts')); + $this->assertEquals($route1->getData('methods'), $route2->getData('methods')); + $this->assertEquals($route1->getData('hasMiddlewares'), $route2->getData('hasMiddlewares')); + } + public function testCollectorMiddlewareFullstackCalled(): void { $action = fn (ServerRequestInterface $request) => new Response( @@ -361,6 +389,52 @@ public function testStaticRouteWithCollectorMiddlewares(): void $dispatcher->dispatch($request, $this->getRequestHandler()); } + public function testAliasRouteDispatch(): void + { + $action = fn (ServerRequestInterface $request, CurrentRoute $route) => new Response( + 200, + [], + implode('', [$route->getPattern(), $route->getName(), $route->getHost(), implode($route->getMethods())]), + '1.1', + implode('', $request->getAttributes()) + ); + + $collector = new RouteCollector(); + + $collector->addRoute( + Route::get('i/{image}')->name('image')->middleware(TestMiddleware1::class)->action($action), + Route::get('f/{image}')->name('another-image')->alias('image'), + Route::get('test')->name('test'), + ); + + $routeCollection = new RouteCollection($collector); + $route1 = $routeCollection->getRoute('image'); + $route2 = $routeCollection->getRoute('another-image'); + $route3 = $routeCollection->getRoute('test'); + + $currentRoute = new CurrentRoute(); + $currentRoute->setRouteWithArguments($route1, []); + $ref = $currentRoute; + $container = new SimpleContainer([ + TestMiddleware1::class => new TestMiddleware1(), + CurrentRoute::class => &$ref, + ]); + $dispatcher = $this->getDispatcher($container) + ->withMiddlewares($route1->getData('enabledMiddlewares')); + + $response1 = $dispatcher->dispatch(new ServerRequest('GET', '/i/test'), $this->getRequestHandler()); + + $currentRoute = new CurrentRoute(); + $currentRoute->setRouteWithArguments($route2, []); + $ref = $currentRoute; + + $response2 = $dispatcher->dispatch(new ServerRequest('GET', '/f/test'), $this->getRequestHandler()); + + $this->assertNotEquals((string) $response1->getBody(), (string) $response2->getBody()); + $this->assertEquals('i/{image}imageGET', (string) $response1->getBody()); + $this->assertEquals('f/{image}another-imageGET', (string) $response2->getBody()); + } + private function getRequestHandler(): RequestHandlerInterface { return new class () implements RequestHandlerInterface { diff --git a/tests/RouteTest.php b/tests/RouteTest.php index 92b158b0..42a519c9 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 @@ -108,6 +108,15 @@ public function testHeadMethod(): void $this->assertSame([Method::HEAD], $route->getData('methods')); } + public function testAlias(): void + { + $route1 = Route::head('/')->name('home'); + $route2 = Route::head('/main')->alias('new-home')->name('new-home'); + + $this->assertSame(null, $route1->getData('alias')); + $this->assertSame('new-home', $route2->getData('alias')); + } + public function testOptionsMethod(): void { $route = Route::options('/');