Skip to content

Commit 6dc51c2

Browse files
innocenzibrendt
andauthored
fix(router): use route registry to generate uris (#1724)
Co-authored-by: Brent Roose <[email protected]>
1 parent 55a9034 commit 6dc51c2

File tree

12 files changed

+113
-32
lines changed

12 files changed

+113
-32
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tempest\Router\Exceptions;
6+
7+
use Exception;
8+
9+
final class ControllerActionDoesNotExist extends Exception implements RouterException
10+
{
11+
public static function controllerNotFound(string $controller): self
12+
{
13+
return new self("The controller class `{$controller}` does not exist.");
14+
}
15+
16+
public static function actionNotFound(string $controller, string $method): self
17+
{
18+
return new self("The method `{$method}()` does not exist in controller class `{$controller}`.");
19+
}
20+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tempest\Router\Exceptions;
6+
7+
use Exception;
8+
9+
final class ControllerMethodHadNoRoute extends Exception implements RouterException
10+
{
11+
public function __construct(string $controllerClass, string $controllerMethod)
12+
{
13+
parent::__construct("No route found for `{$controllerClass}::{$controllerMethod}()`. Did you forget to add a `Route` attribute?");
14+
}
15+
}

packages/router/src/Exceptions/ControllerMethodHadNoRouteAttribute.php

Lines changed: 0 additions & 15 deletions
This file was deleted.

packages/router/src/RouteConfig.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ public function __construct(
1818
/** @var array<string,\Tempest\Router\Routing\Matching\MatchingRegex> */
1919
public array $matchingRegexes = [],
2020

21+
/** @var array<string,string[]> */
22+
public array $handlerIndex = [],
23+
2124
/** @var class-string<\Tempest\Router\ResponseProcessor>[] */
2225
public array $responseProcessors = [],
2326

@@ -37,6 +40,7 @@ public function apply(RouteConfig $newConfig): void
3740
$this->staticRoutes = $newConfig->staticRoutes;
3841
$this->dynamicRoutes = $newConfig->dynamicRoutes;
3942
$this->matchingRegexes = $newConfig->matchingRegexes;
43+
$this->handlerIndex = $newConfig->handlerIndex;
4044
}
4145

4246
public function addResponseProcessor(string $responseProcessor): void

packages/router/src/RouteDiscovery.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ public function discover(DiscoveryLocation $location, ClassReflector $class): vo
2727

2828
foreach ($routeAttributes as $routeAttribute) {
2929
$decorators = [
30-
...$method->getDeclaringClass()->getAttributes(RouteDecorator::class),
3130
...$method->getAttributes(RouteDecorator::class),
31+
...$method->getDeclaringClass()->getAttributes(RouteDecorator::class),
3232
];
3333

3434
$route = DiscoveredRoute::fromRoute($routeAttribute, $decorators, $method);

packages/router/src/Routing/Construction/RouteConfigurator.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ final class RouteConfigurator
2222
/** @var array<string,array<string,\Tempest\Router\Routing\Construction\DiscoveredRoute>> */
2323
private array $dynamicRoutes = [];
2424

25+
/** @var array<string,string[]> */
26+
private array $handlerIndex = [];
27+
2528
private bool $isDirty = false;
2629

2730
private RoutingTree $routingTree;
@@ -35,6 +38,10 @@ public function addRoute(DiscoveredRoute $route): void
3538
{
3639
$this->isDirty = true;
3740

41+
$handler = $route->handler->getDeclaringClass()->getName() . '::' . $route->handler->getName();
42+
$this->handlerIndex[$handler] ??= [];
43+
$this->handlerIndex[$handler][] = $route->uri;
44+
3845
if ($route->isDynamic) {
3946
$this->addDynamicRoute($route);
4047
} else {
@@ -76,6 +83,7 @@ public function toRouteConfig(): RouteConfig
7683
$this->staticRoutes,
7784
$this->dynamicRoutes,
7885
$this->routingTree->toMatchingRegexes(),
86+
$this->handlerIndex,
7987
);
8088
}
8189

packages/router/src/UriGenerator.php

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
use Tempest\Http\Request;
1414
use Tempest\Reflection\ClassReflector;
1515
use Tempest\Reflection\MethodReflector;
16-
use Tempest\Router\Exceptions\ControllerMethodHadNoRouteAttribute;
16+
use Tempest\Router\Exceptions\ControllerActionDoesNotExist;
17+
use Tempest\Router\Exceptions\ControllerMethodHadNoRoute;
1718
use Tempest\Router\Routing\Construction\DiscoveredRoute;
1819
use Tempest\Support\Arr;
1920
use Tempest\Support\Regex;
@@ -25,6 +26,7 @@ final class UriGenerator
2526
{
2627
public function __construct(
2728
private AppConfig $appConfig,
29+
private RouteConfig $routeConfig,
2830
private Signer $signer,
2931
private Container $container,
3032
) {}
@@ -178,7 +180,7 @@ public function isCurrentUri(array|string|MethodReflector $action, mixed ...$par
178180

179181
$matchedRoute = $this->container->get(MatchedRoute::class);
180182
$candidateUri = $this->createUri($action, ...[...$matchedRoute->params, ...$params]);
181-
$currentUri = $this->createUri([$matchedRoute->route->handler->getDeclaringClass(), $matchedRoute->route->handler->getName()]);
183+
$currentUri = $this->createUri([$matchedRoute->route->handler->getDeclaringClass()->getName(), $matchedRoute->route->handler->getName()]);
182184

183185
foreach ($matchedRoute->params as $key => $value) {
184186
if ($value instanceof BackedEnum) {
@@ -206,14 +208,20 @@ private function normalizeActionToUri(array|string|MethodReflector $action): str
206208

207209
[$controllerClass, $controllerMethod] = is_array($action) ? $action : [$action, '__invoke'];
208210

209-
$routeAttribute = new ClassReflector($controllerClass)
210-
->getMethod($controllerMethod)
211-
->getAttribute(Route::class);
211+
$routes = array_unique($this->routeConfig->handlerIndex[$controllerClass . '::' . $controllerMethod] ?? []);
212212

213-
if ($routeAttribute === null) {
214-
throw new ControllerMethodHadNoRouteAttribute($controllerClass, $controllerMethod);
213+
if ($routes === []) {
214+
if (! class_exists($controllerClass)) {
215+
throw ControllerActionDoesNotExist::controllerNotFound($controllerClass, $controllerMethod);
216+
}
217+
218+
if (! method_exists($controllerClass, $controllerMethod)) {
219+
throw ControllerActionDoesNotExist::actionNotFound($controllerClass, $controllerMethod);
220+
}
221+
222+
throw new ControllerMethodHadNoRoute($controllerClass, $controllerMethod);
215223
}
216224

217-
return Str\ensure_starts_with($routeAttribute->uri, '/');
225+
return Str\ensure_starts_with($routes[0], '/');
218226
}
219227
}

rector.php

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,11 @@
55
use Rector\Arguments\Rector\ClassMethod\ArgumentAdderRector;
66
use Rector\Caching\ValueObject\Storage\FileCacheStorage;
77
use Rector\CodingStyle\Rector\Encapsed\EncapsedStringsToSprintfRector;
8-
use Rector\CodingStyle\Rector\Stmt\NewlineAfterStatementRector;
98
use Rector\Config\RectorConfig;
109
use Rector\DeadCode\Rector\PropertyProperty\RemoveNullPropertyInitializationRector;
1110
use Rector\Php70\Rector\StaticCall\StaticCallOnNonStaticToInstanceCallRector;
1211
use Rector\Php74\Rector\Closure\ClosureToArrowFunctionRector;
1312
use Rector\Php74\Rector\Property\RestoreDefaultNullToNullableTypePropertyRector;
14-
use Rector\Php74\Rector\Ternary\ParenthesizeNestedTernaryRector;
1513
use Rector\Php81\Rector\Array_\FirstClassCallableRector;
1614
use Rector\Php81\Rector\FuncCall\NullToStrictStringFuncCallArgRector;
1715
use Rector\Php81\Rector\Property\ReadOnlyPropertyRector;
@@ -21,8 +19,8 @@
2119
use Rector\Php84\Rector\Param\ExplicitNullableParamTypeRector;
2220
use Rector\Privatization\Rector\ClassMethod\PrivatizeFinalClassMethodRector;
2321
use Rector\TypeDeclaration\Rector\ArrowFunction\AddArrowFunctionReturnTypeRector;
22+
use Rector\TypeDeclaration\Rector\ClassMethod\NarrowObjectReturnTypeRector;
2423
use Rector\TypeDeclaration\Rector\ClassMethod\ReturnNeverTypeRector;
25-
use Rector\TypeDeclaration\Rector\Closure\ClosureReturnTypeRector;
2624
use Rector\TypeDeclaration\Rector\Empty_\EmptyOnNullableObjectToInstanceOfRector;
2725

2826
return RectorConfig::configure()
@@ -54,10 +52,10 @@
5452
RestoreDefaultNullToNullableTypePropertyRector::class,
5553
ReturnNeverTypeRector::class,
5654
StaticCallOnNonStaticToInstanceCallRector::class,
57-
ClosureReturnTypeRector::class,
5855
EncapsedStringsToSprintfRector::class,
5956
AddArrowFunctionReturnTypeRector::class,
6057
PrivatizeFinalClassMethodRector::class,
58+
NarrowObjectReturnTypeRector::class,
6159
])
6260
->withParallel(300, 10, 10)
6361
->withPreparedSets(

tests/Fixtures/Controllers/ControllerWithRepeatedRoutes.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#[Get('/repeated/d')]
1818
#[Post('/repeated/e')]
1919
#[Post('/repeated/f')]
20+
#[Get('/repeated/f')]
2021
public function __invoke(): Response
2122
{
2223
return new Ok();

tests/Fixtures/Controllers/PrefixController.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
#[Prefix('/prefix')]
1010
final class PrefixController
1111
{
12-
#[Get('/endpoint')]
12+
#[Prefix('/method'), Get('/endpoint')]
1313
public function __invoke(): Ok
1414
{
1515
return new Ok();

0 commit comments

Comments
 (0)