Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Alias route #246

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions src/Route.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
final class Route implements Stringable
{
private ?string $name = null;
private ?string $alias = null;

/**
* @var string[]
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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,
Expand Down
62 changes: 51 additions & 11 deletions src/RouteCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
*/
final class RouteCollection implements RouteCollectionInterface
{
/**
* @var Route[]
*/
private array $aliases = [];
/**
* @psalm-var Items
*/
Expand Down Expand Up @@ -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);
}

/**
Expand Down Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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;
}
}
74 changes: 74 additions & 0 deletions tests/RouteCollectionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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 {
Expand Down
11 changes: 10 additions & 1 deletion tests/RouteTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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('/');
Expand Down