diff --git a/src/Http/Adapter.php b/src/Http/Adapter.php index 0b0e478..1dd94bb 100755 --- a/src/Http/Adapter.php +++ b/src/Http/Adapter.php @@ -11,5 +11,11 @@ abstract class Adapter abstract public function onStart(callable $callback): void; abstract public function onRequest(callable $callback): void; abstract public function start(): void; - abstract public function getContainer(): Container; + + /** + * Container for the current execution context: the per-request + * container inside a request (coroutine-local under Swoole, with + * parent-chain fallback to global), the global container otherwise. + */ + abstract public function getContext(): Container; } diff --git a/src/Http/Adapter/FPM/Server.php b/src/Http/Adapter/FPM/Server.php index 2b56568..c8fa593 100755 --- a/src/Http/Adapter/FPM/Server.php +++ b/src/Http/Adapter/FPM/Server.php @@ -25,7 +25,7 @@ public function onStart(callable $callback): void \call_user_func($callback, $this); } - public function getContainer(): Container + public function getContext(): Container { return $this->container; } diff --git a/src/Http/Adapter/Swoole/Server.php b/src/Http/Adapter/Swoole/Server.php index b8cf4f1..232302c 100755 --- a/src/Http/Adapter/Swoole/Server.php +++ b/src/Http/Adapter/Swoole/Server.php @@ -12,7 +12,7 @@ class Server extends Adapter { protected SwooleServer $server; - protected const string REQUEST_CONTAINER_CONTEXT_KEY = '__utopia_http_request_container'; + protected const string CONTEXT_KEY = '__utopia_http_context'; protected Container $container; /** @@ -28,20 +28,20 @@ public function __construct(string $host, ?string $port = null, array $settings public function onRequest(callable $callback): void { $this->server->on('request', function (SwooleRequest $request, SwooleResponse $response) use ($callback) { - $requestContainer = new Container($this->container); - $requestContainer->set('swooleRequest', fn() => $request); - $requestContainer->set('swooleResponse', fn() => $response); + $context = new Container($this->container); + $context->set('swooleRequest', fn() => $request); + $context->set('swooleResponse', fn() => $response); - Coroutine::getContext()[self::REQUEST_CONTAINER_CONTEXT_KEY] = $requestContainer; + Coroutine::getContext()[self::CONTEXT_KEY] = $context; \call_user_func($callback, new Request($request), new Response($response)); }); } - public function getContainer(): Container + public function getContext(): Container { if (Coroutine::getCid() !== -1) { - return Coroutine::getContext()[self::REQUEST_CONTAINER_CONTEXT_KEY] ?? $this->container; + return Coroutine::getContext()[self::CONTEXT_KEY] ?? $this->container; } return $this->container; diff --git a/src/Http/Adapter/SwooleCoroutine/Server.php b/src/Http/Adapter/SwooleCoroutine/Server.php index 2b21ab1..d29c964 100644 --- a/src/Http/Adapter/SwooleCoroutine/Server.php +++ b/src/Http/Adapter/SwooleCoroutine/Server.php @@ -11,7 +11,7 @@ class Server extends Adapter { - protected const string REQUEST_CONTAINER_CONTEXT_KEY = '__utopia_http_request_container'; + protected const string CONTEXT_KEY = '__utopia_http_context'; protected SwooleServer $server; protected Container $container; @@ -36,23 +36,23 @@ public function __construct( public function onRequest(callable $callback): void { $this->server->handle('/', function (SwooleRequest $request, SwooleResponse $response) use ($callback) { - $requestContainer = new Container($this->container); - $requestContainer->set('swooleRequest', fn() => $request); - $requestContainer->set('swooleResponse', fn() => $response); + $context = new Container($this->container); + $context->set('swooleRequest', fn() => $request); + $context->set('swooleResponse', fn() => $response); - Coroutine::getContext()[self::REQUEST_CONTAINER_CONTEXT_KEY] = $requestContainer; + Coroutine::getContext()[self::CONTEXT_KEY] = $context; try { \call_user_func($callback, new Request($request), new Response($response)); } finally { - unset(Coroutine::getContext()[self::REQUEST_CONTAINER_CONTEXT_KEY]); + unset(Coroutine::getContext()[self::CONTEXT_KEY]); } }); } - public function getContainer(): Container + public function getContext(): Container { - return Coroutine::getContext()[self::REQUEST_CONTAINER_CONTEXT_KEY] ?? $this->container; + return Coroutine::getContext()[self::CONTEXT_KEY] ?? $this->container; } public function getServer(): SwooleServer diff --git a/src/Http/Http.php b/src/Http/Http.php index 74965fc..9853ba3 100755 --- a/src/Http/Http.php +++ b/src/Http/Http.php @@ -48,8 +48,6 @@ class Http protected Container $container; - protected ?Container $requestContainer = null; - /** * Current running mode */ @@ -105,13 +103,6 @@ class Http */ protected static array $requestHooks = []; - /** - * Route - * - * Memory cached result for chosen route - */ - protected ?Route $route = null; - /** * Wildcard route * If set, this get's executed if no other route is matched @@ -145,7 +136,8 @@ public function __construct(Adapter $server, string $timezone) date_default_timezone_set($timezone); $this->files = new Files(); $this->server = $server; - $this->container = $server->getContainer(); + // Captures the global container; assumes Http is constructed at boot, not inside a request. + $this->container = $server->getContext(); $this->setTelemetry(new NoTelemetry()); } @@ -372,7 +364,7 @@ public static function setAllowOverride(bool $value): void public function getResource(string $name): mixed { try { - return $this->server->getContainer()->get($name); + return $this->server->getContext()->get($name); } catch (ContainerExceptionInterface|NotFoundExceptionInterface $e) { // Normalize DI container errors to the Http layer's "resource" terminology. $message = str_replace('dependency', 'resource', $e->getMessage()); @@ -415,13 +407,14 @@ public function setResource(string $name, callable $callback, array $injections } /** - * Set a request-scoped resource on the current request's container. + * Register a per-request value on the context container. + * Counterpart to setResource() for global singletons. * * @param list $injections */ - protected function setRequestResource(string $name, callable $callback, array $injections = []): void + public function setContext(string $name, callable $callback, array $injections = []): void { - $this->server->getContainer()->set($name, $callback, $injections); + $this->server->getContext()->set($name, $callback, $injections); } /** @@ -460,24 +453,6 @@ public static function getRoutes(): array return Router::getRoutes(); } - /** - * Get the current route - */ - public function getRoute(): ?Route - { - return $this->route ?? null; - } - - /** - * Set the current route - */ - public function setRoute(Route $route): self - { - $this->route = $route; - - return $this; - } - /** * Add Route * @@ -590,8 +565,13 @@ public function start(): void */ public function match(Request $request, bool $fresh = true): ?Route { - if (null !== $this->route && !$fresh) { - return $this->route; + $context = $this->server->getContext(); + + if (!$fresh && $context->has('match')) { + $cached = $context->get('match'); + if ($cached instanceof RouteMatch) { + return $cached->route; + } } $url = parse_url($request->getURI(), PHP_URL_PATH); @@ -599,9 +579,10 @@ public function match(Request $request, bool $fresh = true): ?Route $method = $request->getMethod(); $method = (self::REQUEST_METHOD_HEAD === $method) ? self::REQUEST_METHOD_GET : $method; - $this->route = Router::match($method, $url); + $match = Router::match($method, $url); + $context->set('match', fn() => $match); - return $this->route; + return $match?->route; } /** @@ -612,7 +593,14 @@ public function execute(Route $route, Request $request, Response $response): sta $arguments = []; $groups = $route->getGroups(); - $preparedPath = Router::preparePath($route->getMatchedPath()); + $context = $this->server->getContext(); + $match = $context->has('match') ? $context->get('match') : null; + if (!$match instanceof RouteMatch || $match->route !== $route) { + // execute() called directly without a prior match(). + $match = new RouteMatch($route, ''); + $context->set('match', fn() => $match); + } + $preparedPath = Router::preparePath($match->path); $pathValues = $route->getPathValues($request, $preparedPath[0]); try { @@ -635,7 +623,7 @@ public function execute(Route $route, Request $request, Response $response): sta } if (!$response->isSent()) { - $arguments = $this->getArguments($route, $pathValues, $request->getParams()); + $arguments = $this->getArguments($route, $pathValues, $request->getParams(), $match->arguments); \call_user_func_array($route->getAction(), $arguments); } @@ -657,7 +645,7 @@ public function execute(Route $route, Request $request, Response $response): sta } } } catch (\Throwable $e) { - $this->setRequestResource('error', fn() => $e, []); + $this->setContext('error', fn() => $e, []); foreach ($groups as $group) { foreach (self::$errors as $error) { // Group error hooks @@ -688,17 +676,17 @@ public function execute(Route $route, Request $request, Response $response): sta } /** - * Get Arguments - * * @param array $values * @param array $requestParams + * @param array $resolved + * @param-out array $resolved * @return array * @throws Exception */ - protected function getArguments(Hook $hook, array $values, array $requestParams): array + protected function getArguments(Hook $hook, array $values, array $requestParams, array &$resolved = []): array { $arguments = []; - foreach ($hook->getParams() as $key => $param) { // Get value from route or request object + foreach ($hook->getParams() as $key => $param) { $existsInRequest = \array_key_exists($key, $requestParams); $existsInValues = \array_key_exists($key, $values); $paramExists = $existsInRequest || $existsInValues; @@ -709,6 +697,8 @@ protected function getArguments(Hook $hook, array $values, array $requestParams) } $value = $existsInValues ? $values[$key] : $arg; + $resolved[(string) $key] = $value; + if (!$param['skipValidation']) { if (!$paramExists && !$param['optional']) { throw new Exception('Param "' . $key . '" is not optional.', 400); @@ -719,7 +709,6 @@ protected function getArguments(Hook $hook, array $values, array $requestParams) } } - $hook->setParamValue($key, $value); $arguments[$param['order']] = $value; } @@ -744,10 +733,12 @@ public function run(Request $request, Response $response): static $result = $this->runInternal($request, $response); $requestDuration = microtime(true) - $start; + $context = $this->server->getContext(); + $match = $context->has('match') ? $context->get('match') : null; $attributes = [ 'url.scheme' => $request->getProtocol(), 'http.request.method' => $request->getMethod(), - 'http.route' => $this->route?->getPath(), + 'http.route' => $match instanceof RouteMatch ? $match->route->getPath() : null, 'http.response.status_code' => $response->getStatusCode(), ]; $this->requestDuration->record($requestDuration, $attributes); @@ -777,8 +768,9 @@ private function runInternal(Request $request, Response $response): static $response->setCompressionSupported($this->compressionSupported); } - $this->setRequestResource('request', fn() => $request); - $this->setRequestResource('response', fn() => $response); + $this->setContext('request', fn() => $request); + $this->setContext('response', fn() => $response); + $this->setContext('match', fn() => null); try { foreach (self::$requestHooks as $hook) { @@ -786,7 +778,7 @@ private function runInternal(Request $request, Response $response): static \call_user_func_array($hook->getAction(), $arguments); } } catch (\Exception $e) { - $this->setRequestResource('error', fn() => $e, []); + $this->setContext('error', fn() => $e, []); foreach (self::$errors as $error) { // Global error hooks if (\in_array('*', $error->getGroups())) { @@ -816,8 +808,6 @@ private function runInternal(Request $request, Response $response): static $route = $this->match($request); $groups = ($route instanceof Route) ? $route->getGroups() : []; - $this->setRequestResource('route', fn() => $route, []); - if (self::REQUEST_METHOD_HEAD === $method) { $method = self::REQUEST_METHOD_GET; $response->disablePayload(); @@ -844,7 +834,7 @@ private function runInternal(Request $request, Response $response): static foreach (self::$errors as $error) { // Global error hooks /** @var Hook $error */ if (\in_array('*', $error->getGroups())) { - $this->setRequestResource('error', fn() => $e, []); + $this->setContext('error', fn() => $e, []); \call_user_func_array($error->getAction(), $this->getArguments($error, [], $request->getParams())); } } @@ -854,13 +844,14 @@ private function runInternal(Request $request, Response $response): static } if (null === $route && null !== self::$wildcardRoute) { - $route = self::$wildcardRoute; - $this->route = $route; + // Clone before stamping $path so concurrent coroutines don't fight over the singleton. + $route = clone self::$wildcardRoute; $path = parse_url($request->getURI(), PHP_URL_PATH); $path = \is_string($path) ? ($path === '' ? '/' : $path) : '/'; $route->path($path); - $this->setRequestResource('route', fn() => $route, []); + $match = new RouteMatch($route, ''); + $this->setContext('match', fn() => $match); } if (null !== $route) { return $this->execute($route, $request, $response); @@ -884,7 +875,7 @@ private function runInternal(Request $request, Response $response): static } catch (\Throwable $e) { foreach (self::$errors as $error) { // Global error hooks if (\in_array('*', $error->getGroups())) { - $this->setRequestResource('error', fn() => $e, []); + $this->setContext('error', fn() => $e, []); \call_user_func_array($error->getAction(), $this->getArguments($error, [], $request->getParams())); } } @@ -892,7 +883,7 @@ private function runInternal(Request $request, Response $response): static } else { foreach (self::$errors as $error) { // Global error hooks if (\in_array('*', $error->getGroups())) { - $this->setRequestResource('error', fn() => new Exception('Not Found', 404), []); + $this->setContext('error', fn() => new Exception('Not Found', 404), []); \call_user_func_array($error->getAction(), $this->getArguments($error, [], $request->getParams())); } } diff --git a/src/Http/Route.php b/src/Http/Route.php index b72886d..adb8ca3 100755 --- a/src/Http/Route.php +++ b/src/Http/Route.php @@ -38,8 +38,6 @@ class Route extends Hook */ protected int $order; - protected string $matchedPath = ''; - public function __construct(string $method, string $path) { parent::__construct(); @@ -48,17 +46,6 @@ public function __construct(string $method, string $path) $this->order = ++self::$counter; } - public function setMatchedPath(string $path): self - { - $this->matchedPath = $path; - return $this; - } - - public function getMatchedPath(): string - { - return $this->matchedPath; - } - /** * Get Route Order ID */ diff --git a/src/Http/RouteMatch.php b/src/Http/RouteMatch.php new file mode 100644 index 0000000..53a8011 --- /dev/null +++ b/src/Http/RouteMatch.php @@ -0,0 +1,31 @@ +inject('match')`. + */ +final class RouteMatch +{ + /** @var array */ + public array $arguments; + + /** + * @param array $arguments + */ + public function __construct( + public readonly Route $route, + public readonly string $path, + array $arguments = [], + ) { + $this->arguments = $arguments; + } +} diff --git a/src/Http/Router.php b/src/Http/Router.php index 8118ffe..f37189a 100644 --- a/src/Http/Router.php +++ b/src/Http/Router.php @@ -106,9 +106,14 @@ public static function addRouteAlias(string $path, Route $route): void } /** - * Match route against the method and path. + * Match a request against the registered routes. + * + * Returns a RouteMatch holding the matched Route and the prepared-path + * key it was found under (placeholders replaced with `:::`), so callers + * can resolve path params without mutating the shared Route singleton. + * Returns null when no route matches. */ - public static function match(string $method, string $path): ?Route + public static function match(string $method, string $path): ?RouteMatch { if (!\array_key_exists($method, self::$routes)) { return null; @@ -129,9 +134,7 @@ public static function match(string $method, string $path): ?Route ); if (\array_key_exists($match, self::$routes[$method])) { - $route = self::$routes[$method][$match]; - $route->setMatchedPath($match); - return $route; + return new RouteMatch(self::$routes[$method][$match], $match); } } @@ -140,9 +143,7 @@ public static function match(string $method, string $path): ?Route */ $match = self::WILDCARD_TOKEN; if (\array_key_exists($match, self::$routes[$method])) { - $route = self::$routes[$method][$match]; - $route->setMatchedPath($match); - return $route; + return new RouteMatch(self::$routes[$method][$match], $match); } /** @@ -152,9 +153,7 @@ public static function match(string $method, string $path): ?Route $current = ($current ?? '') . "{$part}/"; $match = $current . self::WILDCARD_TOKEN; if (\array_key_exists($match, self::$routes[$method])) { - $route = self::$routes[$method][$match]; - $route->setMatchedPath($match); - return $route; + return new RouteMatch(self::$routes[$method][$match], $match); } } diff --git a/tests/HttpTest.php b/tests/HttpTest.php index 6535df0..498bf04 100755 --- a/tests/HttpTest.php +++ b/tests/HttpTest.php @@ -366,13 +366,94 @@ public function testCanHookThrowExceptions(): void $this->assertSame('(init)-y-def-x-def-(shutdown)', $result); } - public function testCanSetRoute(): void + public function testShutdownHookCanInjectResolvedArguments(): void { - $route = new Route('GET', '/path'); + $route = new Route('GET', '/files/:fileId'); + $route + ->param('fileId', '', new Text(64), 'file id', false) + ->param('width', 0, new Text(8), 'width', true) + ->action(function ($fileId, $width) { + echo 'action:' . $fileId . ',' . $width; + }); + + $this->http + ->shutdown() + ->inject('match') + ->action(function (RouteMatch $match) { + echo '|shutdown:fileId=' . $match->arguments['fileId'] . ',width=' . $match->arguments['width']; + }); + + $request = new UtopiaFPMRequestTest(); + $request::_setParams(['fileId' => 'abc123', 'width' => '200']); + $_SERVER['REQUEST_URI'] = '/files/abc123'; + + ob_start(); + $this->http->execute($route, $request, new Response()); + $result = ob_get_contents(); + ob_end_clean(); + + $this->assertSame('action:abc123,200|shutdown:fileId=abc123,width=200', $result); + } + + public function testErrorHookSeesPartialMatchArgumentsWhenValidationFails(): void + { + $route = new Route('GET', '/files/:fileId/things/:thingId'); + $route + ->param('fileId', '', new Text(64), 'file id', false) + ->param('thingId', '', new Text(1, min: 0), 'thing id', false) + ->action(function ($fileId, $thingId) { + echo 'never'; + }); + + $seen = new \stdClass(); + + $this->http + ->error() + ->inject('match') + ->action(function (?RouteMatch $match) use ($seen) { + $seen->arguments = $match?->arguments; + }); + + $request = new UtopiaFPMRequestTest(); + $request::_setParams(['fileId' => 'abc123', 'thingId' => 'too-long-for-the-validator']); + + ob_start(); + $this->http->execute($route, $request, new Response()); + ob_end_clean(); - $this->assertNull($this->http->getRoute()); - $this->http->setRoute($route); - $this->assertSame($route, $this->http->getRoute()); + // fileId resolved fine; thingId failed validation. + // Error hook should see fileId (and the candidate thingId value). + $this->assertSame('abc123', $seen->arguments['fileId'] ?? null); + $this->assertSame('too-long-for-the-validator', $seen->arguments['thingId'] ?? null); + } + + public function testMatchIsNullDuringEarlyErrorBeforeRouting(): void + { + $_SERVER['REQUEST_METHOD'] = 'GET'; + $_SERVER['REQUEST_URI'] = '/whatever'; + + Http::onRequest() + ->action(function (): void { + throw new \RuntimeException('boom'); + }); + + $seen = new \stdClass(); + $seen->matchWasSet = false; + $seen->matchValue = 'sentinel'; + + Http::error() + ->inject('match') + ->action(function (?RouteMatch $match) use ($seen): void { + $seen->matchWasSet = true; + $seen->matchValue = $match; + }); + + ob_start(); + @$this->http->run(new Request(), new Response()); + ob_end_clean(); + + $this->assertTrue($seen->matchWasSet, 'global error hook should have run'); + $this->assertNull($seen->matchValue, "'match' should be null on the context before routing"); } /** @@ -422,7 +503,7 @@ public function testCanMatchRoute(string $method, string $path, ?string $url = n $_SERVER['REQUEST_URI'] = $url; $this->assertSame($expected, $this->http->match(new Request())); - $this->assertSame($expected, $this->http->getRoute()); + $this->assertSame($expected, $this->http->getResource('match')?->route); } public function testNoMismatchRoute(): void @@ -451,7 +532,7 @@ public function testNoMismatchRoute(): void $route = $this->http->match(new Request(), fresh: true); $this->assertNull($route); - $this->assertNull($this->http->getRoute()); + $this->assertNull($this->http->getResource('match')?->route); } } @@ -466,7 +547,7 @@ public function testCanMatchFreshRoute(): void $_SERVER['REQUEST_URI'] = '/path1'; $matched = $this->http->match(new Request()); $this->assertSame($route1, $matched); - $this->assertSame($route1, $this->http->getRoute()); + $this->assertSame($route1, $this->http->getResource('match')?->route); // Second request match returns cached route $_SERVER['REQUEST_METHOD'] = 'HEAD'; @@ -474,12 +555,12 @@ public function testCanMatchFreshRoute(): void $request2 = new Request(); $matched = $this->http->match($request2, fresh: false); $this->assertSame($route1, $matched); - $this->assertSame($route1, $this->http->getRoute()); + $this->assertSame($route1, $this->http->getResource('match')?->route); // Fresh match returns new route $matched = $this->http->match($request2, fresh: true); $this->assertSame($route2, $matched); - $this->assertSame($route2, $this->http->getRoute()); + $this->assertSame($route2, $this->http->getResource('match')?->route); } catch (\Exception $e) { $this->fail($e->getMessage()); } @@ -493,7 +574,7 @@ public function testCanMatchRootRouteWhenUriHasNoPath(): void $_SERVER['REQUEST_URI'] = 'https://example.com?x=1'; $this->assertSame($route, $this->http->match(new Request())); - $this->assertSame($route, $this->http->getRoute()); + $this->assertSame($route, $this->http->getResource('match')?->route); } public function testCanRunRequest(): void @@ -532,9 +613,9 @@ public function testWildcardRoute(): void $_SERVER['REQUEST_URI'] = '/unknown_path'; Http::init() - ->action(function () { - $route = $this->http->getRoute(); - $this->container->set('myRoute', fn() => $route); + ->inject('match') + ->action(function (RouteMatch $match) { + $this->container->set('myRoute', fn() => $match->route); }); diff --git a/tests/RouterTest.php b/tests/RouterTest.php index 4ca0134..d6ec216 100644 --- a/tests/RouterTest.php +++ b/tests/RouterTest.php @@ -23,9 +23,9 @@ public function testCanMatchUrl(): void Router::addRoute($routeAbout); Router::addRoute($routeAboutMe); - $this->assertEquals($routeIndex, Router::match(Http::REQUEST_METHOD_GET, '/')); - $this->assertEquals($routeAbout, Router::match(Http::REQUEST_METHOD_GET, '/about')); - $this->assertEquals($routeAboutMe, Router::match(Http::REQUEST_METHOD_GET, '/about/me')); + $this->assertSame($routeIndex, Router::match(Http::REQUEST_METHOD_GET, '/')->route); + $this->assertSame($routeAbout, Router::match(Http::REQUEST_METHOD_GET, '/about')->route); + $this->assertSame($routeAboutMe, Router::match(Http::REQUEST_METHOD_GET, '/about/me')->route); } public function testCanMatchUrlWithPlaceholder(): void @@ -44,12 +44,12 @@ public function testCanMatchUrlWithPlaceholder(): void Router::addRoute($routeBlogPostComments); Router::addRoute($routeBlogPostCommentsSingle); - $this->assertEquals($routeBlog, Router::match(Http::REQUEST_METHOD_GET, '/blog')); - $this->assertEquals($routeBlogAuthors, Router::match(Http::REQUEST_METHOD_GET, '/blog/authors')); - $this->assertEquals($routeBlogAuthorsComments, Router::match(Http::REQUEST_METHOD_GET, '/blog/authors/comments')); - $this->assertEquals($routeBlogPost, Router::match(Http::REQUEST_METHOD_GET, '/blog/test')); - $this->assertEquals($routeBlogPostComments, Router::match(Http::REQUEST_METHOD_GET, '/blog/test/comments')); - $this->assertEquals($routeBlogPostCommentsSingle, Router::match(Http::REQUEST_METHOD_GET, '/blog/test/comments/:comment')); + $this->assertSame($routeBlog, Router::match(Http::REQUEST_METHOD_GET, '/blog')->route); + $this->assertSame($routeBlogAuthors, Router::match(Http::REQUEST_METHOD_GET, '/blog/authors')->route); + $this->assertSame($routeBlogAuthorsComments, Router::match(Http::REQUEST_METHOD_GET, '/blog/authors/comments')->route); + $this->assertSame($routeBlogPost, Router::match(Http::REQUEST_METHOD_GET, '/blog/test')->route); + $this->assertSame($routeBlogPostComments, Router::match(Http::REQUEST_METHOD_GET, '/blog/test/comments')->route); + $this->assertSame($routeBlogPostCommentsSingle, Router::match(Http::REQUEST_METHOD_GET, '/blog/test/comments/:comment')->route); } public function testCanMatchUrlWithWildcard(): void @@ -62,11 +62,11 @@ public function testCanMatchUrlWithWildcard(): void Router::addRoute($routeAbout); Router::addRoute($routeAboutWildcard); - $this->assertEquals($routeIndex, Router::match('GET', '/')); - $this->assertEquals($routeAbout, Router::match('GET', '/about')); - $this->assertEquals($routeAboutWildcard, Router::match('GET', '/about/me')); - $this->assertEquals($routeAboutWildcard, Router::match('GET', '/about/you')); - $this->assertEquals($routeAboutWildcard, Router::match('GET', '/about/me/myself/i')); + $this->assertSame($routeIndex, Router::match('GET', '/')->route); + $this->assertSame($routeAbout, Router::match('GET', '/about')->route); + $this->assertSame($routeAboutWildcard, Router::match('GET', '/about/me')->route); + $this->assertSame($routeAboutWildcard, Router::match('GET', '/about/you')->route); + $this->assertSame($routeAboutWildcard, Router::match('GET', '/about/me/myself/i')->route); } public function testCanMatchHttpMethod(): void @@ -77,11 +77,11 @@ public function testCanMatchHttpMethod(): void Router::addRoute($routeGET); Router::addRoute($routePOST); - $this->assertEquals($routeGET, Router::match(Http::REQUEST_METHOD_GET, '/')); - $this->assertEquals($routePOST, Router::match(Http::REQUEST_METHOD_POST, '/')); + $this->assertSame($routeGET, Router::match(Http::REQUEST_METHOD_GET, '/')->route); + $this->assertSame($routePOST, Router::match(Http::REQUEST_METHOD_POST, '/')->route); - $this->assertNotEquals($routeGET, Router::match(Http::REQUEST_METHOD_POST, '/')); - $this->assertNotEquals($routePOST, Router::match(Http::REQUEST_METHOD_GET, '/')); + $this->assertNotSame($routeGET, Router::match(Http::REQUEST_METHOD_POST, '/')?->route); + $this->assertNotSame($routePOST, Router::match(Http::REQUEST_METHOD_GET, '/')?->route); } public function testCanMatchAlias(): void @@ -93,9 +93,9 @@ public function testCanMatchAlias(): void Router::addRoute($routeGET); - $this->assertEquals($routeGET, Router::match(Http::REQUEST_METHOD_GET, '/target')); - $this->assertEquals($routeGET, Router::match(Http::REQUEST_METHOD_GET, '/alias')); - $this->assertEquals($routeGET, Router::match(Http::REQUEST_METHOD_GET, '/alias2')); + $this->assertSame($routeGET, Router::match(Http::REQUEST_METHOD_GET, '/target')->route); + $this->assertSame($routeGET, Router::match(Http::REQUEST_METHOD_GET, '/alias')->route); + $this->assertSame($routeGET, Router::match(Http::REQUEST_METHOD_GET, '/alias2')->route); } public function testCanMatchMultipleAliases(): void @@ -108,10 +108,10 @@ public function testCanMatchMultipleAliases(): void Router::addRoute($routeGET); - $this->assertEquals($routeGET, Router::match(Http::REQUEST_METHOD_GET, '/target')); - $this->assertEquals($routeGET, Router::match(Http::REQUEST_METHOD_GET, '/alias1')); - $this->assertEquals($routeGET, Router::match(Http::REQUEST_METHOD_GET, '/alias2')); - $this->assertEquals($routeGET, Router::match(Http::REQUEST_METHOD_GET, '/alias3')); + $this->assertSame($routeGET, Router::match(Http::REQUEST_METHOD_GET, '/target')->route); + $this->assertSame($routeGET, Router::match(Http::REQUEST_METHOD_GET, '/alias1')->route); + $this->assertSame($routeGET, Router::match(Http::REQUEST_METHOD_GET, '/alias2')->route); + $this->assertSame($routeGET, Router::match(Http::REQUEST_METHOD_GET, '/alias3')->route); } public function testCanMatchMix(): void @@ -127,14 +127,14 @@ public function testCanMatchMix(): void Router::addRoute($routeGET); - $this->assertEquals($routeGET, Router::match(Http::REQUEST_METHOD_GET, '/')); - $this->assertEquals($routeGET, Router::match(Http::REQUEST_METHOD_GET, '/console')); - $this->assertEquals($routeGET, Router::match(Http::REQUEST_METHOD_GET, '/invite')); - $this->assertEquals($routeGET, Router::match(Http::REQUEST_METHOD_GET, '/login')); - $this->assertEquals($routeGET, Router::match(Http::REQUEST_METHOD_GET, '/recover')); - $this->assertEquals($routeGET, Router::match(Http::REQUEST_METHOD_GET, '/console/lorem/ipsum/dolor')); - $this->assertEquals($routeGET, Router::match(Http::REQUEST_METHOD_GET, '/auth/lorem/ipsum')); - $this->assertEquals($routeGET, Router::match(Http::REQUEST_METHOD_GET, '/register/lorem/ipsum')); + $this->assertSame($routeGET, Router::match(Http::REQUEST_METHOD_GET, '/')->route); + $this->assertSame($routeGET, Router::match(Http::REQUEST_METHOD_GET, '/console')->route); + $this->assertSame($routeGET, Router::match(Http::REQUEST_METHOD_GET, '/invite')->route); + $this->assertSame($routeGET, Router::match(Http::REQUEST_METHOD_GET, '/login')->route); + $this->assertSame($routeGET, Router::match(Http::REQUEST_METHOD_GET, '/recover')->route); + $this->assertSame($routeGET, Router::match(Http::REQUEST_METHOD_GET, '/console/lorem/ipsum/dolor')->route); + $this->assertSame($routeGET, Router::match(Http::REQUEST_METHOD_GET, '/auth/lorem/ipsum')->route); + $this->assertSame($routeGET, Router::match(Http::REQUEST_METHOD_GET, '/register/lorem/ipsum')->route); } public function testCanMatchFilename(): void @@ -142,7 +142,7 @@ public function testCanMatchFilename(): void $routeGET = new Route(Http::REQUEST_METHOD_GET, '/robots.txt'); Router::addRoute($routeGET); - $this->assertEquals($routeGET, Router::match(Http::REQUEST_METHOD_GET, '/robots.txt')); + $this->assertSame($routeGET, Router::match(Http::REQUEST_METHOD_GET, '/robots.txt')->route); } public function testCannotFindUnknownRouteByPath(): void @@ -156,7 +156,7 @@ public function testCannotFindUnknownRouteByMethod(): void Router::addRoute($route); - $this->assertEquals($route, Router::match(Http::REQUEST_METHOD_GET, '/404')); + $this->assertSame($route, Router::match(Http::REQUEST_METHOD_GET, '/404')->route); $this->assertNull(Router::match(Http::REQUEST_METHOD_POST, '/404')); }