diff --git a/src/Builder/GroupBuilder.php b/src/Builder/GroupBuilder.php new file mode 100644 index 0000000..9268b80 --- /dev/null +++ b/src/Builder/GroupBuilder.php @@ -0,0 +1,166 @@ + + */ + private array $middlewares = []; + + private array $disabledMiddlewares = []; + + /** + * @var string[] + */ + private array $hosts = []; + private bool $routesAdded = false; + private bool $middlewareAdded = false; + + /** + * @var array|callable|string|null Middleware definition for CORS requests. + */ + private $corsMiddleware = null; + + private function __construct( + private ?string $prefix = null, + private ?string $namePrefix = null, + ) { + } + + /** + * Create a new group instance. + * + * @param string|null $prefix URL prefix to prepend to all routes of the group. + */ + public static function create(?string $prefix = null, ?string $namePrefix = null): self + { + return new self($prefix, $namePrefix); + } + + public function routes(Group|Route|RoutableInterface ...$routes): self + { + if ($this->middlewareAdded) { + throw new RuntimeException('routes() can not be used after prependMiddleware().'); + } + + $new = clone $this; + $new->routes = $routes; + $new->routesAdded = true; + + return $new; + } + + /** + * Adds a middleware definition that handles CORS requests. + * If set, routes for {@see Method::OPTIONS} request will be added automatically. + * + * @param array|callable|string|null $middlewareDefinition Middleware definition for CORS requests. + */ + public function withCors(array|callable|string|null $middlewareDefinition): self + { + $group = clone $this; + $group->corsMiddleware = $middlewareDefinition; + + return $group; + } + + /** + * Appends a handler middleware definition that should be invoked for a matched route. + * First added handler will be executed first. + */ + public function middleware(array|callable|string ...$definition): self + { + if ($this->routesAdded) { + throw new RuntimeException('middleware() can not be used after routes().'); + } + + $new = clone $this; + array_push( + $new->middlewares, + ...array_values($definition) + ); + + return $new; + } + + /** + * Prepends a handler middleware definition that should be invoked for a matched route. + * First added handler will be executed last. + */ + public function prependMiddleware(array|callable|string ...$definition): self + { + $new = clone $this; + array_unshift( + $new->middlewares, + ...array_values($definition) + ); + + $new->middlewareAdded = true; + + return $new; + } + + public function namePrefix(string $namePrefix): self + { + $new = clone $this; + $new->namePrefix = $namePrefix; + return $new; + } + + public function host(string $host): self + { + return $this->hosts($host); + } + + public function hosts(string ...$hosts): self + { + $new = clone $this; + $new->hosts = array_values($hosts); + + return $new; + } + + /** + * 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. + */ + public function disableMiddleware(mixed ...$definition): self + { + $new = clone $this; + array_push( + $new->disabledMiddlewares, + ...array_values($definition), + ); + + return $new; + } + + public function toRoute(): Group|Route + { + return new Group( + prefix: $this->prefix, + namePrefix: $this->namePrefix, + routes: $this->routes, + middlewares: $this->middlewares, + hosts: $this->hosts, + disabledMiddlewares: $this->disabledMiddlewares, + corsMiddleware: $this->corsMiddleware + ); + } +} diff --git a/src/Builder/RouteBuilder.php b/src/Builder/RouteBuilder.php new file mode 100644 index 0000000..f00607a --- /dev/null +++ b/src/Builder/RouteBuilder.php @@ -0,0 +1,218 @@ + + */ + private array $middlewares = []; + + /** + * @var array + */ + private array $defaults = []; + + /** + * @param string[] $methods + */ + private function __construct( + private array $methods, + private string $pattern, + ) { + } + + public static function get(string $pattern): self + { + return self::methods([Method::GET], $pattern); + } + + public static function post(string $pattern): self + { + return self::methods([Method::POST], $pattern); + } + + public static function put(string $pattern): self + { + return self::methods([Method::PUT], $pattern); + } + + public static function delete(string $pattern): self + { + return self::methods([Method::DELETE], $pattern); + } + + public static function patch(string $pattern): self + { + return self::methods([Method::PATCH], $pattern); + } + + public static function head(string $pattern): self + { + return self::methods([Method::HEAD], $pattern); + } + + public static function options(string $pattern): self + { + return self::methods([Method::OPTIONS], $pattern); + } + + /** + * @param string[] $methods + */ + public static function methods(array $methods, string $pattern): self + { + return new self(methods: $methods, pattern: $pattern); + } + + public function name(string $name): self + { + $route = clone $this; + $route->name = $name; + return $route; + } + + public function pattern(string $pattern): self + { + $new = clone $this; + $new->pattern = $pattern; + return $new; + } + + public function host(string $host): self + { + return $this->hosts($host); + } + + public function hosts(string ...$hosts): self + { + $route = clone $this; + $route->hosts = array_values($hosts); + + return $route; + } + + /** + * Marks route as override. When added it will replace existing route with the same name. + */ + public function override(): self + { + $route = clone $this; + $route->override = true; + return $route; + } + + /** + * Parameter default values indexed by parameter names. + * + * @psalm-param array $defaults + */ + public function defaults(array $defaults): self + { + $route = clone $this; + $route->defaults = $defaults; + return $route; + } + + /** + * Appends a handler middleware definition that should be invoked for a matched route. + * First added handler will be executed first. + */ + public function middleware(array|callable|string ...$definition): self + { + $route = clone $this; + array_push( + $route->middlewares, + ...array_values($definition) + ); + + return $route; + } + + /** + * Prepends a handler middleware definition that should be invoked for a matched route. + * Last added handler will be executed first. + */ + public function prependMiddleware(array|callable|string ...$definition): self + { + $route = clone $this; + array_unshift( + $route->middlewares, + ...array_values($definition) + ); + + return $route; + } + + /** + * Appends action handler. It is a primary middleware definition that should be invoked last for a matched route. + */ + public function action(array|callable|string $middlewareDefinition): self + { + $route = clone $this; + $route->action = $middlewareDefinition; + return $route; + } + + /** + * 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. + */ + public function disableMiddleware(mixed ...$definition): self + { + $route = clone $this; + array_push( + $route->disabledMiddlewares, + ...array_values($definition) + ); + + return $route; + } + + public function toRoute(): Group|Route + { + return new Route( + methods: $this->methods, + pattern: $this->pattern, + name: $this->name, + action: $this->action, + middlewares: $this->middlewares, + defaults: $this->defaults, + hosts: $this->hosts, + override: $this->override, + disabledMiddlewares: $this->disabledMiddlewares + ); + } +} diff --git a/src/CurrentRoute.php b/src/CurrentRoute.php index d3eb9a2..9b9a4ff 100644 --- a/src/CurrentRoute.php +++ b/src/CurrentRoute.php @@ -36,17 +36,17 @@ final class CurrentRoute */ public function getName(): ?string { - return $this->route?->getData('name'); + return $this->route?->getName(); } /** - * Returns the current route host. + * Returns the current route hosts. * - * @return string|null The current route host. + * @return array|null The current route hosts. */ - public function getHost(): ?string + public function getHosts(): ?array { - return $this->route?->getData('host'); + return $this->route?->getHosts(); } /** @@ -56,7 +56,7 @@ public function getHost(): ?string */ public function getPattern(): ?string { - return $this->route?->getData('pattern'); + return $this->route?->getPattern(); } /** @@ -66,7 +66,7 @@ public function getPattern(): ?string */ public function getMethods(): ?array { - return $this->route?->getData('methods'); + return $this->route?->getMethods(); } /** diff --git a/src/Debug/RouterCollector.php b/src/Debug/RouterCollector.php index e9dfc59..ff7e038 100644 --- a/src/Debug/RouterCollector.php +++ b/src/Debug/RouterCollector.php @@ -55,10 +55,10 @@ public function getCollected(): array if ($currentRoute !== null && $route !== null) { $result['currentRoute'] = [ 'matchTime' => $this->matchTime, - 'name' => $route->getData('name'), - 'pattern' => $route->getData('pattern'), + 'name' => $route->getName(), + 'pattern' => $route->getPattern(), 'arguments' => $currentRoute->getArguments(), - 'host' => $route->getData('host'), + 'hosts' => implode(', ', $route->getHosts()), 'uri' => (string) $currentRoute->getUri(), 'action' => $action, 'middlewares' => $middlewares, @@ -91,10 +91,10 @@ public function getSummary(): array return [ 'router' => [ 'matchTime' => $this->matchTime, - 'name' => $route->getData('name'), - 'pattern' => $route->getData('pattern'), + 'name' => $route->getName(), + 'pattern' => $route->getPattern(), 'arguments' => $currentRoute->getArguments(), - 'host' => $route->getData('host'), + 'hosts' => implode(', ', $route->getHosts()), 'uri' => (string) $currentRoute->getUri(), 'action' => $action, 'middlewares' => $middlewares, @@ -134,15 +134,7 @@ private function getMiddlewaresAndAction(?Route $route): array if ($route === null) { return [[], null]; } - $reflection = new ReflectionObject($route); - $reflectionProperty = $reflection->getProperty('middlewareDefinitions'); - $reflectionProperty->setAccessible(true); - /** - * @var array[]|callable[]|string[] $middlewareDefinitions - */ - $middlewareDefinitions = $reflectionProperty->getValue($route); - $action = array_pop($middlewareDefinitions); - return [$middlewareDefinitions, $action]; + return [$route->getMiddlewares(), $route->getAction()]; } } diff --git a/src/Group.php b/src/Group.php index ce55426..f1b2783 100644 --- a/src/Group.php +++ b/src/Group.php @@ -4,16 +4,12 @@ namespace Yiisoft\Router; -use InvalidArgumentException; -use RuntimeException; use Yiisoft\Router\Internal\MiddlewareFilter; -use function in_array; - final class Group { /** - * @var Group[]|Route[] + * @var Group[]|Route[]|RoutableInterface[] */ private array $routes = []; @@ -27,8 +23,6 @@ final class Group * @var string[] */ private array $hosts = []; - private bool $routesAdded = false; - private bool $middlewareAdded = false; /** * @psalm-var list|null @@ -54,180 +48,109 @@ public function __construct( private array $disabledMiddlewares = [], array|callable|string|null $corsMiddleware = null ) { - $this->assertRoutes($routes); - $this->assertMiddlewares($middlewares); - $this->assertHosts($hosts); - $this->routes = $routes; - $this->middlewares = $middlewares; - $this->hosts = $hosts; + $this->setRoutes($routes); + $this->setMiddlewares($middlewares); + $this->setHosts($hosts); $this->corsMiddleware = $corsMiddleware; - if (!empty($routes)) { - $this->routesAdded = true; - } - if (!empty($middlewares)) { - $this->middlewareAdded = true; - } } /** - * Create a new group instance. - * - * @param string|null $prefix URL prefix to prepend to all routes of the group. + * @return Group[]|RoutableInterface[]|Route[] */ - public static function create(?string $prefix = null): self + public function getRoutes(): array { - return new self($prefix); + return $this->routes; } - public function routes(self|Route ...$routes): self + public function getMiddlewares(): array { - if ($this->middlewareAdded) { - throw new RuntimeException('routes() can not be used after prependMiddleware().'); - } - - $new = clone $this; - $new->routes = $routes; - $new->routesAdded = true; - - return $new; + return $this->middlewares; } - /** - * Adds a middleware definition that handles CORS requests. - * If set, routes for {@see Method::OPTIONS} request will be added automatically. - * - * @param array|callable|string|null $middlewareDefinition Middleware definition for CORS requests. - */ - public function withCors(array|callable|string|null $middlewareDefinition): self + public function getHosts(): array { - $group = clone $this; - $group->corsMiddleware = $middlewareDefinition; - - return $group; + return $this->hosts; } - /** - * Appends a handler middleware definition that should be invoked for a matched route. - * First added handler will be executed first. - */ - public function middleware(array|callable|string ...$definition): self + public function getCorsMiddleware(): callable|array|string|null { - if ($this->routesAdded) { - throw new RuntimeException('middleware() can not be used after routes().'); - } - - $new = clone $this; - array_push( - $new->middlewares, - ...array_values($definition) - ); - - $new->enabledMiddlewaresCache = null; - - return $new; + return $this->corsMiddleware; } - /** - * Prepends a handler middleware definition that should be invoked for a matched route. - * First added handler will be executed last. - */ - public function prependMiddleware(array|callable|string ...$definition): self + public function getPrefix(): ?string { - $new = clone $this; - array_unshift( - $new->middlewares, - ...array_values($definition) - ); - - $new->middlewareAdded = true; - $new->enabledMiddlewaresCache = null; + return $this->prefix; + } - return $new; + public function getNamePrefix(): ?string + { + return $this->namePrefix; } - public function namePrefix(string $namePrefix): self + public function getDisabledMiddlewares(): array { - $new = clone $this; - $new->namePrefix = $namePrefix; - return $new; + return $this->disabledMiddlewares; } - public function host(string $host): self + public function setRoutes(array $routes): self { - return $this->hosts($host); + $this->assertRoutes($routes); + $this->routes = $routes; + return $this; } - public function hosts(string ...$hosts): self + public function setMiddlewares(array $middlewares): self { - $new = clone $this; + $this->assertMiddlewares($middlewares); + $this->middlewares = $middlewares; + $this->enabledMiddlewaresCache = null; + return $this; + } + public function setHosts(array $hosts): self + { + $this->assertHosts($hosts); foreach ($hosts as $host) { $host = rtrim($host, '/'); - if ($host !== '' && !in_array($host, $new->hosts, true)) { - $new->hosts[] = $host; + if ($host !== '' && !in_array($host, $this->hosts, true)) { + $this->hosts[] = $host; } } - return $new; + return $this; } - /** - * 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. - */ - public function disableMiddleware(mixed ...$definition): self + public function setCorsMiddleware(callable|array|string|null $corsMiddleware): self { - $new = clone $this; - array_push( - $new->disabledMiddlewares, - ...array_values($definition), - ); + $this->corsMiddleware = $corsMiddleware; + return $this; + } - $new->enabledMiddlewaresCache = null; + public function setPrefix(?string $prefix): self + { + $this->prefix = $prefix; + return $this; + } - return $new; + public function setNamePrefix(?string $namePrefix): self + { + $this->namePrefix = $namePrefix; + return $this; } - /** - * @psalm-template T as string - * - * @psalm-param T $key - * - * @psalm-return ( - * T is ('prefix'|'namePrefix'|'host') ? string|null : - * (T is 'routes' ? Group[]|Route[] : - * (T is 'hosts' ? array : - * (T is ('hasCorsMiddleware') ? bool : - * (T is 'enabledMiddlewares' ? list : - * (T is 'corsMiddleware' ? array|callable|string|null : mixed) - * ) - * ) - * ) - * ) - * ) - */ - public function getData(string $key): mixed + public function setDisabledMiddlewares(array $disabledMiddlewares): self { - return match ($key) { - 'prefix' => $this->prefix, - 'namePrefix' => $this->namePrefix, - 'host' => $this->hosts[0] ?? null, - 'hosts' => $this->hosts, - 'corsMiddleware' => $this->corsMiddleware, - 'routes' => $this->routes, - 'hasCorsMiddleware' => $this->corsMiddleware !== null, - 'enabledMiddlewares' => $this->getEnabledMiddlewares(), - default => throw new InvalidArgumentException('Unknown data key: ' . $key), - }; + $this->disabledMiddlewares = $disabledMiddlewares; + $this->enabledMiddlewaresCache = null; + return $this; } /** * @return array[]|callable[]|string[] * @psalm-return list */ - private function getEnabledMiddlewares(): array + public function getEnabledMiddlewares(): array { if ($this->enabledMiddlewaresCache !== null) { return $this->enabledMiddlewaresCache; @@ -268,18 +191,18 @@ private function assertMiddlewares(array $middlewares): void } /** - * @psalm-assert array $routes + * @psalm-assert array $routes */ private function assertRoutes(array $routes): void { - /** @var mixed $route */ + /** @var Route|Group|RoutableInterface $route */ foreach ($routes as $route) { - if ($route instanceof Route || $route instanceof self) { + if ($route instanceof Route || $route instanceof self || $route instanceof RoutableInterface) { continue; } throw new \InvalidArgumentException( - 'Invalid $routes provided, array of `Route` or `Group` expected.' + 'Invalid $routes provided, array of `Route` or `Group` or `RoutableInterface` instance expected.' ); } } diff --git a/src/Middleware/Router.php b/src/Middleware/Router.php index 644bc8c..3f0c10b 100644 --- a/src/Middleware/Router.php +++ b/src/Middleware/Router.php @@ -55,7 +55,7 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface $this->currentRoute->setRouteWithArguments($result->route(), $result->arguments()); return $this->dispatcher - ->withMiddlewares($result->route()->getData('enabledMiddlewares')) + ->withMiddlewares($result->route()->getEnabledMiddlewares()) ->dispatch($request, $handler); } } diff --git a/src/RoutableInterface.php b/src/RoutableInterface.php new file mode 100644 index 0000000..8c3082f --- /dev/null +++ b/src/RoutableInterface.php @@ -0,0 +1,10 @@ + */ private array $methods = []; @@ -26,7 +23,11 @@ final class Route implements Stringable * @var string[] */ private array $hosts = []; - private bool $actionAdded = false; + + /** + * @var array|callable|string|null + */ + private $action = null; /** * @var array[]|callable[]|string[] @@ -40,14 +41,14 @@ final class Route implements Stringable private ?array $enabledMiddlewaresCache = null; /** - * @var array + * @var array */ private array $defaults = []; /** * @param array|callable|string|null $action Action handler. It is a primary middleware definition that * should be invoked last for a matched route. - * @param array $defaults Parameter default values indexed by parameter names. + * @param array $defaults Parameter default values indexed by parameter names. * @param bool $override Marks route as override. When added it will replace existing route with the same name. * @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 @@ -67,225 +68,155 @@ public function __construct( if (empty($methods)) { throw new InvalidArgumentException('$methods cannot be empty.'); } - $this->assertListOfStrings($methods, 'methods'); - $this->assertMiddlewares($middlewares); - $this->assertListOfStrings($hosts, 'hosts'); - $this->methods = $methods; - $this->middlewares = $middlewares; - $this->hosts = $hosts; - $this->defaults = array_map('\strval', $defaults); - if (!empty($action)) { - $this->middlewares[] = $action; - $this->actionAdded = true; - } + $this->setMethods($methods); + $this->action = $action; + $this->setMiddlewares($middlewares); + $this->setHosts($hosts); + $this->setDefaults($defaults); } - public static function get(string $pattern): self + /** + * @return string[] + */ + public function getMethods(): array { - return self::methods([Method::GET], $pattern); + return $this->methods; } - public static function post(string $pattern): self + public function getAction(): array|callable|string|null { - return self::methods([Method::POST], $pattern); + return $this->action; } - public static function put(string $pattern): self + public function getMiddlewares(): array { - return self::methods([Method::PUT], $pattern); + return $this->middlewares; } - public static function delete(string $pattern): self + /** + * @return string[] + */ + public function getHosts(): array { - return self::methods([Method::DELETE], $pattern); + return $this->hosts; } - public static function patch(string $pattern): self + public function getDefaults(): array { - return self::methods([Method::PATCH], $pattern); + return $this->defaults; } - public static function head(string $pattern): self + public function getPattern(): string { - return self::methods([Method::HEAD], $pattern); + return $this->pattern; } - public static function options(string $pattern): self + public function getName(): string { - return self::methods([Method::OPTIONS], $pattern); + return $this->name ??= (implode(', ', $this->methods) . ' ' . implode('|', $this->hosts) . $this->pattern); } - /** - * @param string[] $methods - */ - public static function methods(array $methods, string $pattern): self + public function isOverride(): bool { - return new self(methods: $methods, pattern: $pattern); + return $this->override; } - public function name(string $name): self + public function getDisabledMiddlewares(): array { - $route = clone $this; - $route->name = $name; - return $route; + return $this->disabledMiddlewares; } - public function pattern(string $pattern): self + /** + * @return array[]|callable[]|string[] + * @psalm-return list + */ + public function getEnabledMiddlewares(): array { - $new = clone $this; - $new->pattern = $pattern; - return $new; + if ($this->enabledMiddlewaresCache !== null) { + return $this->enabledMiddlewaresCache; + } + + $this->enabledMiddlewaresCache = MiddlewareFilter::filter($this->middlewares, $this->disabledMiddlewares); + if ($this->action !== null) { + $this->enabledMiddlewaresCache[] = $this->action; + } + + return $this->enabledMiddlewaresCache; } - public function host(string $host): self + public function setMethods(array $methods): self { - return $this->hosts($host); + $this->assertListOfStrings($methods, 'methods'); + $this->methods = $methods; + return $this; } - public function hosts(string ...$hosts): self + public function setHosts(array $hosts): self { - $route = clone $this; - $route->hosts = []; - + $this->assertListOfStrings($hosts, 'hosts'); + $this->hosts = []; foreach ($hosts as $host) { $host = rtrim($host, '/'); - if ($host !== '' && !in_array($host, $route->hosts, true)) { - $route->hosts[] = $host; + if ($host !== '' && !in_array($host, $this->hosts, true)) { + $this->hosts[] = $host; } } - return $route; + return $this; } - /** - * Marks route as override. When added it will replace existing route with the same name. - */ - public function override(): self + public function setAction(callable|array|string|null $action): self { - $route = clone $this; - $route->override = true; - return $route; + $this->action = $action; + return $this; } - /** - * Parameter default values indexed by parameter names. - * - * @psalm-param array $defaults - */ - public function defaults(array $defaults): self + public function setMiddlewares(array $middlewares): self { - $route = clone $this; - $route->defaults = array_map('\strval', $defaults); - return $route; + $this->assertMiddlewares($middlewares); + $this->middlewares = $middlewares; + $this->enabledMiddlewaresCache = null; + return $this; } - /** - * Appends a handler middleware definition that should be invoked for a matched route. - * First added handler will be executed first. - */ - public function middleware(array|callable|string ...$definition): self + public function setDefaults(array $defaults): self { - if ($this->actionAdded) { - throw new RuntimeException('middleware() can not be used after action().'); + /** @var mixed $value */ + foreach ($defaults as $key => $value) { + if (!is_scalar($value) && !($value instanceof Stringable)) { + throw new \InvalidArgumentException( + 'Invalid $defaults provided, list of scalar or `Stringable` instance expected.' + ); + } + $this->defaults[$key] = (string) $value; } - - $route = clone $this; - array_push( - $route->middlewares, - ...array_values($definition) - ); - - $route->enabledMiddlewaresCache = null; - - return $route; + return $this; } - /** - * Prepends a handler middleware definition that should be invoked for a matched route. - * Last added handler will be executed first. - */ - public function prependMiddleware(array|callable|string ...$definition): self + public function setPattern(string $pattern): self { - if (!$this->actionAdded) { - throw new RuntimeException('prependMiddleware() can not be used before action().'); - } - - $route = clone $this; - array_unshift( - $route->middlewares, - ...array_values($definition) - ); - - $route->enabledMiddlewaresCache = null; - - return $route; + $this->pattern = $pattern; + return $this; } - /** - * Appends action handler. It is a primary middleware definition that should be invoked last for a matched route. - */ - public function action(array|callable|string $middlewareDefinition): self + public function setName(?string $name): self { - $route = clone $this; - $route->middlewares[] = $middlewareDefinition; - $route->actionAdded = true; - return $route; + $this->name = $name; + return $this; } - /** - * 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. - */ - public function disableMiddleware(mixed ...$definition): self + public function setOverride(bool $override): self { - $route = clone $this; - array_push( - $route->disabledMiddlewares, - ...array_values($definition) - ); - - $route->enabledMiddlewaresCache = null; - - return $route; + $this->override = $override; + return $this; } - /** - * @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') ? bool : - * (T is 'enabledMiddlewares' ? array : mixed) - * ) - * ) - * ) - * ) - * ) - * ) - */ - public function getData(string $key): mixed + public function setDisabledMiddlewares(array $disabledMiddlewares): self { - return match ($key) { - 'name' => $this->name ?? - (implode(', ', $this->methods) . ' ' . implode('|', $this->hosts) . $this->pattern), - 'pattern' => $this->pattern, - 'host' => $this->hosts[0] ?? null, - 'hosts' => $this->hosts, - 'methods' => $this->methods, - 'defaults' => $this->defaults, - 'override' => $this->override, - 'hasMiddlewares' => $this->middlewares !== [], - 'enabledMiddlewares' => $this->getEnabledMiddlewares(), - default => throw new InvalidArgumentException('Unknown data key: ' . $key), - }; + $this->disabledMiddlewares = $disabledMiddlewares; + $this->enabledMiddlewaresCache = null; + return $this; } public function __toString(): string @@ -317,10 +248,10 @@ public function __debugInfo() 'name' => $this->name, 'methods' => $this->methods, 'pattern' => $this->pattern, + 'action' => $this->action, 'hosts' => $this->hosts, 'defaults' => $this->defaults, 'override' => $this->override, - 'actionAdded' => $this->actionAdded, 'middlewares' => $this->middlewares, 'disabledMiddlewares' => $this->disabledMiddlewares, 'enabledMiddlewares' => $this->getEnabledMiddlewares(), @@ -328,22 +259,7 @@ public function __debugInfo() } /** - * @return array[]|callable[]|string[] - * @psalm-return list - */ - private function getEnabledMiddlewares(): array - { - if ($this->enabledMiddlewaresCache !== null) { - return $this->enabledMiddlewaresCache; - } - - $this->enabledMiddlewaresCache = MiddlewareFilter::filter($this->middlewares, $this->disabledMiddlewares); - - return $this->enabledMiddlewaresCache; - } - - /** - * @psalm-assert array $items + * @psalm-assert array $items */ private function assertListOfStrings(array $items, string $argument): void { diff --git a/src/RouteCollection.php b/src/RouteCollection.php index 01be086..0b167ff 100644 --- a/src/RouteCollection.php +++ b/src/RouteCollection.php @@ -8,6 +8,8 @@ use Psr\Http\Message\ResponseFactoryInterface; use Yiisoft\Http\Method; +use Yiisoft\Router\Builder\RouteBuilder; + use function array_key_exists; use function in_array; use function is_array; @@ -65,13 +67,16 @@ private function ensureItemsInjected(): void /** * Build routes array. * - * @param Group[]|Route[] $items + * @param Group[]|Route[]|RoutableInterface[] $items */ private function injectItems(array $items): void { foreach ($items as $item) { + if ($item instanceof RoutableInterface) { + $item = $item->toRoute(); + } if (!$this->isStaticRoute($item)) { - $item = $item->prependMiddleware(...$this->collector->getMiddlewareDefinitions()); + $item->setMiddlewares(array_merge($this->collector->getMiddlewares(), $item->getMiddlewares())); } $this->injectItem($item); } @@ -87,9 +92,9 @@ private function injectItem(Group|Route $route): void return; } - $routeName = $route->getData('name'); + $routeName = $route->getName(); $this->items[] = $routeName; - if (isset($this->routes[$routeName]) && !$route->getData('override')) { + if (isset($this->routes[$routeName]) && !$route->isOverride()) { throw new InvalidArgumentException("A route with name '$routeName' already exists."); } $this->routes[$routeName] = $route; @@ -102,57 +107,61 @@ private function injectItem(Group|Route $route): void */ private function injectGroup(Group $group, array &$tree, string $prefix = '', string $namePrefix = ''): void { - $prefix .= (string) $group->getData('prefix'); - $namePrefix .= (string) $group->getData('namePrefix'); - $items = $group->getData('routes'); + $prefix .= (string) $group->getPrefix(); + $namePrefix .= (string) $group->getNamePrefix(); + $items = $group->getRoutes(); $pattern = null; $hosts = []; foreach ($items as $item) { + if ($item instanceof RoutableInterface) { + $item = $item->toRoute(); + } if (!$this->isStaticRoute($item)) { - $item = $item->prependMiddleware(...$group->getData('enabledMiddlewares')); + $item = $item->setMiddlewares(array_merge($group->getEnabledMiddlewares(), $item->getMiddlewares())); } - if (!empty($group->getData('hosts')) && empty($item->getData('hosts'))) { - $item = $item->hosts(...$group->getData('hosts')); + if (!empty($group->getHosts()) && empty($item->getHosts())) { + $item->setHosts($group->getHosts()); } if ($item instanceof Group) { - if ($group->getData('hasCorsMiddleware')) { - $item = $item->withCors($group->getData('corsMiddleware')); + if ($group->getCorsMiddleware() !== null) { + $item->setCorsMiddleware($group->getCorsMiddleware()); } - if (empty($item->getData('prefix'))) { + if (empty($item->getPrefix())) { $this->injectGroup($item, $tree, $prefix, $namePrefix); continue; } /** @psalm-suppress PossiblyNullArrayOffset Checked group prefix on not empty above. */ - if (!isset($tree[$item->getData('prefix')])) { - $tree[$item->getData('prefix')] = []; + if (!isset($tree[$item->getPrefix()])) { + $tree[$item->getPrefix()] = []; } /** * @psalm-suppress MixedArgumentTypeCoercion * @psalm-suppress MixedArgument,PossiblyNullArrayOffset * Checked group prefix on not empty above. */ - $this->injectGroup($item, $tree[$item->getData('prefix')], $prefix, $namePrefix); + $this->injectGroup($item, $tree[$item->getPrefix()], $prefix, $namePrefix); continue; } - $modifiedItem = $item->pattern($prefix . $item->getData('pattern')); + /** @var Route $item */ + $item->setPattern($prefix . $item->getPattern()); - if (!str_contains($modifiedItem->getData('name'), implode(', ', $modifiedItem->getData('methods')))) { - $modifiedItem = $modifiedItem->name($namePrefix . $modifiedItem->getData('name')); + if (!str_contains($item->getName(), implode(', ', $item->getMethods()))) { + $item->setName($namePrefix . $item->getName()); } - if ($group->getData('hasCorsMiddleware')) { - $this->processCors($group, $hosts, $pattern, $modifiedItem, $tree); + if ($group->getCorsMiddleware() !== null) { + $this->processCors($group, $hosts, $pattern, $item, $tree); } - $routeName = $modifiedItem->getData('name'); + $routeName = $item->getName(); $tree[] = $routeName; - if (isset($this->routes[$routeName]) && !$modifiedItem->getData('override')) { + if (isset($this->routes[$routeName]) && !$item->isOverride()) { throw new InvalidArgumentException("A route with name '$routeName' already exists."); } - $this->routes[$routeName] = $modifiedItem; + $this->routes[$routeName] = $item; } } @@ -163,30 +172,31 @@ private function processCors( Group $group, array &$hosts, ?string &$pattern, - Route &$modifiedItem, + Route $modifiedItem, array &$tree ): void { /** @var array|callable|string $middleware */ - $middleware = $group->getData('corsMiddleware'); - $isNotDuplicate = !in_array(Method::OPTIONS, $modifiedItem->getData('methods'), true) - && ($pattern !== $modifiedItem->getData('pattern') || $hosts !== $modifiedItem->getData('hosts')); + $middleware = $group->getCorsMiddleware(); + $isNotDuplicate = !in_array(Method::OPTIONS, $modifiedItem->getMethods(), true) + && ($pattern !== $modifiedItem->getPattern() || $hosts !== $modifiedItem->getHosts()); - $pattern = $modifiedItem->getData('pattern'); - $hosts = $modifiedItem->getData('hosts'); - $optionsRoute = Route::options($pattern); + $pattern = $modifiedItem->getPattern(); + $hosts = $modifiedItem->getHosts(); + $optionsRoute = new Route([Method::OPTIONS], $pattern); if (!empty($hosts)) { - $optionsRoute = $optionsRoute->hosts(...$hosts); + $optionsRoute->setHosts($hosts); } if ($isNotDuplicate) { - $optionsRoute = $optionsRoute->middleware($middleware); - - $routeName = $optionsRoute->getData('name'); - $tree[] = $routeName; - $this->routes[$routeName] = $optionsRoute->action( + $optionsRoute->setMiddlewares([$middleware]); + $optionsRoute->setAction( static fn (ResponseFactoryInterface $responseFactory) => $responseFactory->createResponse(204) ); + + $routeName = $optionsRoute->getName(); + $tree[] = $routeName; + $this->routes[$routeName] = $optionsRoute; } - $modifiedItem = $modifiedItem->prependMiddleware($middleware); + $modifiedItem->setMiddlewares(array_merge([$middleware], $modifiedItem->getMiddlewares())); } /** @@ -208,8 +218,8 @@ private function buildTree(array $items, bool $routeAsString): array return $tree; } - private function isStaticRoute(Group|Route $item): bool + private function isStaticRoute(Group|Route|RoutableInterface $item): bool { - return $item instanceof Route && !$item->getData('hasMiddlewares'); + return $item instanceof Route && empty($item->getMiddlewares()) && $item->getAction() === null; } } diff --git a/src/RouteCollector.php b/src/RouteCollector.php index 03fb12b..52c9d0f 100644 --- a/src/RouteCollector.php +++ b/src/RouteCollector.php @@ -7,16 +7,16 @@ final class RouteCollector implements RouteCollectorInterface { /** - * @var Group[]|Route[] + * @var Group[]|Route[]|RoutableInterface[] */ private array $items = []; /** * @var array[]|callable[]|string[] */ - private array $middlewareDefinitions = []; + private array $middlewares = []; - public function addRoute(Route|Group ...$routes): RouteCollectorInterface + public function addRoute(Route|Group|RoutableInterface ...$routes): RouteCollectorInterface { array_push( $this->items, @@ -25,20 +25,20 @@ public function addRoute(Route|Group ...$routes): RouteCollectorInterface return $this; } - public function middleware(array|callable|string ...$middlewareDefinition): RouteCollectorInterface + public function middleware(array|callable|string ...$definition): RouteCollectorInterface { array_push( - $this->middlewareDefinitions, - ...array_values($middlewareDefinition) + $this->middlewares, + ...array_values($definition) ); return $this; } - public function prependMiddleware(array|callable|string ...$middlewareDefinition): RouteCollectorInterface + public function prependMiddleware(array|callable|string ...$definition): RouteCollectorInterface { array_unshift( - $this->middlewareDefinitions, - ...array_values($middlewareDefinition) + $this->middlewares, + ...array_values($definition) ); return $this; } @@ -48,8 +48,8 @@ public function getItems(): array return $this->items; } - public function getMiddlewareDefinitions(): array + public function getMiddlewares(): array { - return $this->middlewareDefinitions; + return $this->middlewares; } } diff --git a/src/RouteCollectorInterface.php b/src/RouteCollectorInterface.php index 38c5861..8cf56aa 100644 --- a/src/RouteCollectorInterface.php +++ b/src/RouteCollectorInterface.php @@ -9,27 +9,29 @@ interface RouteCollectorInterface /** * Add a route or a group of routes. */ - public function addRoute(Route|Group ...$routes): self; + public function addRoute(Route|Group|RoutableInterface ...$routes): self; /** * Appends a handler middleware definition that should be invoked for a matched route. * First added handler will be executed first. */ - public function middleware(array|callable|string ...$middlewareDefinition): self; + public function middleware(array|callable|string ...$definition): self; /** * Prepends a handler middleware definition that should be invoked for a matched route. * First added handler will be executed last. */ - public function prependMiddleware(array|callable|string ...$middlewareDefinition): self; + public function prependMiddleware(array|callable|string ...$definition): self; /** - * @return Group[]|Route[] + * @return Group[]|Route[]|RoutableInterface[] */ public function getItems(): array; /** + * Returns middleware definitions. + * * @return array[]|callable[]|string[] */ - public function getMiddlewareDefinitions(): array; + public function getMiddlewares(): array; } diff --git a/tests/Builder/GroupBuilderTest.php b/tests/Builder/GroupBuilderTest.php new file mode 100644 index 0000000..4541ddc --- /dev/null +++ b/tests/Builder/GroupBuilderTest.php @@ -0,0 +1,439 @@ + new Response(); + $middleware2 = static fn () => new Response(); + + $group = $group + ->middleware($middleware1) + ->middleware($middleware2); + $groupRoute = $group->toRoute(); + + $this->assertCount(2, $groupRoute->getEnabledMiddlewares()); + $this->assertSame($middleware1, $groupRoute->getEnabledMiddlewares()[0]); + $this->assertSame($middleware2, $groupRoute->getEnabledMiddlewares()[1]); + } + + public function testMiddlewaresWithKeys(): void + { + $group = Group::create() + ->middleware(m3: TestMiddleware3::class) + ->prependMiddleware(m1: TestMiddleware1::class, m2: TestMiddleware2::class) + ->disableMiddleware(m1: TestMiddleware1::class); + $groupRoute = $group->toRoute(); + + $this->assertSame( + [TestMiddleware2::class, TestMiddleware3::class], + $groupRoute->getEnabledMiddlewares() + ); + } + + public function testNamedArgumentsInMiddlewareMethods(): void + { + $group = Group::create() + ->middleware(TestMiddleware3::class) + ->prependMiddleware(TestMiddleware1::class, TestMiddleware2::class) + ->disableMiddleware(TestMiddleware1::class, TestMiddleware3::class); + $groupRoute = $group->toRoute(); + + $this->assertCount(1, $groupRoute->getEnabledMiddlewares()); + $this->assertSame(TestMiddleware2::class, $groupRoute->getEnabledMiddlewares()[0]); + } + + public function testRoutesAfterMiddleware(): void + { + $group = Group::create(); + + $middleware1 = static fn () => new Response(); + + $group = $group->prependMiddleware($middleware1); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('routes() can not be used after prependMiddleware().'); + + $group->routes(Route::get('/')->toRoute()); + } + + public function testAddNestedMiddleware(): void + { + $request = new ServerRequest('GET', '/outergroup/innergroup/test1'); + + $action = static fn (ServerRequestInterface $request) => new Response( + 200, + [], + null, + '1.1', + implode('', $request->getAttributes()) + ); + + $middleware1 = static function (ServerRequestInterface $request, RequestHandlerInterface $handler) { + $request = $request->withAttribute('middleware', 'middleware1'); + return $handler->handle($request); + }; + + $middleware2 = static function (ServerRequestInterface $request, RequestHandlerInterface $handler) { + $request = $request->withAttribute('middleware', 'middleware2'); + return $handler->handle($request); + }; + + $group = Group::create('/outergroup') + ->middleware($middleware1) + ->routes( + Group::create('/innergroup') + ->middleware($middleware2) + ->routes( + Route::get('/test1') + ->action($action) + ->name('request1'), + ) + ); + + $collector = new RouteCollector(); + $collector->addRoute($group); + + $routeCollection = new RouteCollection($collector); + $route = $routeCollection->getRoute('request1'); + $response = $this->getDispatcher() + ->withMiddlewares($route->getEnabledMiddlewares()) + ->dispatch($request, $this->getRequestHandler()); + $this->assertSame(200, $response->getStatusCode()); + $this->assertSame('middleware2', $response->getReasonPhrase()); + } + + public function testGroupMiddlewareFullStackCalled(): void + { + $request = new ServerRequest('GET', '/group/test1'); + + $action = static fn (ServerRequestInterface $request) => new Response( + 200, + [], + null, + '1.1', + implode('', $request->getAttributes()) + ); + $middleware1 = function (ServerRequestInterface $request, RequestHandlerInterface $handler) { + $request = $request->withAttribute('middleware', 'middleware1'); + return $handler->handle($request); + }; + $middleware2 = function (ServerRequestInterface $request, RequestHandlerInterface $handler) { + $request = $request->withAttribute('middleware', 'middleware2'); + return $handler->handle($request); + }; + + $group = Group::create('/group') + ->middleware($middleware1) + ->middleware($middleware2) + ->routes( + Route::get('/test1') + ->action($action) + ->name('request1'), + ); + + $collector = new RouteCollector(); + $collector->addRoute($group); + + $routeCollection = new RouteCollection($collector); + $route = $routeCollection->getRoute('request1'); + + $response = $this->getDispatcher() + ->withMiddlewares($route->getEnabledMiddlewares()) + ->dispatch($request, $this->getRequestHandler()); + + $this->assertSame(200, $response->getStatusCode()); + $this->assertSame('middleware2', $response->getReasonPhrase()); + } + + public function testGroupMiddlewareStackInterrupted(): void + { + $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') + ->middleware($middleware1) + ->middleware($middleware2) + ->routes( + Route::get('/test1') + ->action($action) + ->name('request1') + ); + + $collector = new RouteCollector(); + $collector->addRoute($group); + + $routeCollection = new RouteCollection($collector); + $route = $routeCollection->getRoute('request1'); + + $response = $this->getDispatcher() + ->withMiddlewares($route->getEnabledMiddlewares()) + ->dispatch($request, $this->getRequestHandler()); + + $this->assertSame(403, $response->getStatusCode()); + } + + public function testAddGroup(): void + { + $logoutRoute = Route::post('/logout'); + $listRoute = Route::get('/'); + $viewRoute = Route::get('/{id}'); + + $middleware1 = static fn () => new Response(); + $middleware2 = static fn () => new Response(); + + $root = Group::create() + ->routes( + Group::create('/api') + ->middleware($middleware1) + ->middleware($middleware2) + ->routes( + $logoutRoute, + Group::create('/post') + ->routes( + $listRoute, + $viewRoute + ) + ), + ); + $rootGroup = $root->toRoute(); + + $this->assertCount(1, $rootGroup->getRoutes()); + + /** @var Group $api */ + $api = $rootGroup->getRoutes()[0]; + $apiRoute = $api->toRoute(); + + $this->assertSame('/api', $apiRoute->getPrefix()); + $this->assertCount(2, $apiRoute->getRoutes()); + $this->assertSame($logoutRoute, $apiRoute->getRoutes()[0]); + + /** @var Group $postGroup */ + $postGroup = $apiRoute->getRoutes()[1]; + $postGroup = $postGroup->toRoute(); + + $this->assertInstanceOf(\Yiisoft\Router\Group::class, $postGroup); + $this->assertCount(2, $apiRoute->getEnabledMiddlewares()); + $this->assertSame($middleware1, $apiRoute->getEnabledMiddlewares()[0]); + $this->assertSame($middleware2, $apiRoute->getEnabledMiddlewares()[1]); + + $this->assertSame('/post', $postGroup->getPrefix()); + $this->assertCount(2, $postGroup->getRoutes()); + $this->assertSame($listRoute, $postGroup->getRoutes()[0]); + $this->assertSame($viewRoute, $postGroup->getRoutes()[1]); + $this->assertEmpty($postGroup->getEnabledMiddlewares()); + } + + public function testHost(): void + { + $group = Group::create()->host('https://yiiframework.com/'); + + $this->assertSame('https://yiiframework.com', $group->toRoute()->getHosts()[0]); + } + + public function testHosts(): void + { + $group = Group::create()->hosts('https://yiiframework.com/', 'https://yiiframework.ru/'); + + $this->assertSame(['https://yiiframework.com', 'https://yiiframework.ru'], $group->toRoute()->getHosts()); + } + + public function testName(): void + { + $group = Group::create()->namePrefix('api'); + + $this->assertSame('api', $group->toRoute()->getNamePrefix()); + } + + + public function testWithCors(): void + { + $group = Group::create() + ->routes( + Route::get('/info')->action(static fn () => 'info'), + Route::post('/info')->action(static fn () => 'info'), + ) + ->withCors( + static fn () => new Response(204) + ); + + $collector = new RouteCollector(); + $collector->addRoute($group); + $routeCollection = new RouteCollection($collector); + + $this->assertCount(3, $routeCollection->getRoutes()); + } + + public function testWithCorsWithHostRoutes(): void + { + $group = Group::create() + ->routes( + Route::get('/info') + ->action(static fn () => 'info') + ->host('yii.dev'), + Route::get('/info') + ->action(static fn () => 'info') + ->host('yii.test'), + ) + ->withCors( + static fn () => new Response(204) + ); + + $collector = new RouteCollector(); + $collector->addRoute($group); + $routeCollection = new RouteCollection($collector); + + $this->assertCount(4, $routeCollection->getRoutes()); + } + + public function testWithCorsDoesntDuplicateRoutes(): void + { + $group = Group::create() + ->routes( + Route::get('/info') + ->action(static fn () => 'info') + ->host('yii.dev'), + Route::post('/info') + ->action(static fn () => 'info') + ->host('yii.dev'), + Route::put('/info') + ->action(static fn () => 'info') + ->host('yii.test'), + ) + ->withCors( + static fn () => new Response(204) + ); + + $collector = new RouteCollector(); + $collector->addRoute($group); + $routeCollection = new RouteCollection($collector); + + $this->assertCount(5, $routeCollection->getRoutes()); + } + + public function testWithCorsWithNestedGroups(): void + { + $group = Group::create()->routes( + Route::get('/info')->action(static fn () => 'info'), + Route::post('/info')->action(static fn () => 'info'), + Group::create('/v1') + ->routes( + Route::get('/post')->action(static fn () => 'post'), + Route::post('/post')->action(static fn () => 'post'), + Route::options('/options')->action(static fn () => 'options'), + ) + ->withCors( + static fn () => new Response(201) + ) + )->withCors( + static fn () => new Response(204) + ); + + $collector = new RouteCollector(); + $collector->addRoute($group); + + $routeCollection = new RouteCollection($collector); + $this->assertCount(7, $routeCollection->getRoutes()); + $this->assertInstanceOf(\Yiisoft\Router\Route::class, $routeCollection->getRoute('OPTIONS /v1/post')); + } + + public function testWithCorsWithNestedGroups2(): void + { + $group = Group::create()->routes( + Route::get('/info')->action(static fn () => 'info'), + Route::post('/info')->action(static fn () => 'info'), + Route::get('/v1/post')->action(static fn () => 'post'), + Group::create('/v1')->routes( + Route::post('/post')->action(static fn () => 'post'), + Route::options('/options')->action(static fn () => 'options'), + ), + Group::create('/v1')->routes( + Route::put('/post')->action(static fn () => 'post'), + ) + )->withCors( + static fn () => new Response(204) + ); + $collector = new RouteCollector(); + $collector->addRoute($group); + + $routeCollection = new RouteCollection($collector); + $this->assertCount(8, $routeCollection->getRoutes()); + $this->assertInstanceOf(\Yiisoft\Router\Route::class, $routeCollection->getRoute('OPTIONS /v1/post')); + } + + public function testMiddlewareAfterRoutes(): void + { + $group = Group::create()->routes(Route::get('/info')->action(static fn () => 'info')); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('middleware() can not be used after routes().'); + $group->middleware(static fn () => new Response()); + } + + public function testDuplicateHosts(): void + { + $route = Group::create()->hosts('a.com', 'b.com', 'a.com'); + + $this->assertSame(['a.com', 'b.com'], $route->toRoute()->getHosts()); + } + + public function testImmutability(): void + { + $group = Group::create(); + + $this->assertNotSame($group, $group->routes()); + $this->assertNotSame($group, $group->withCors(null)); + $this->assertNotSame($group, $group->middleware()); + $this->assertNotSame($group, $group->prependMiddleware()); + $this->assertNotSame($group, $group->namePrefix('')); + $this->assertNotSame($group, $group->hosts()); + $this->assertNotSame($group, $group->disableMiddleware()); + } + + private function getRequestHandler(): RequestHandlerInterface + { + return new class () implements RequestHandlerInterface { + public function handle(ServerRequestInterface $request): ResponseInterface + { + return new Response(404); + } + }; + } + + private function getDispatcher(): MiddlewareDispatcher + { + $container = new Container([]); + return new MiddlewareDispatcher( + new MiddlewareFactory($container), + $this->createMock(EventDispatcherInterface::class) + ); + } +} diff --git a/tests/Builder/RouteBuilderTest.php b/tests/Builder/RouteBuilderTest.php new file mode 100644 index 0000000..68cdca5 --- /dev/null +++ b/tests/Builder/RouteBuilderTest.php @@ -0,0 +1,392 @@ +name('test.route'); + + $this->assertSame('test.route', $route->toRoute()->getName()); + } + + public function testNameDefault(): void + { + $route = Route::get('/'); + + $this->assertSame('GET /', $route->toRoute()->getName()); + } + + public function testNameDefaultWithHosts(): void + { + $route = Route::get('/')->hosts('a.com', 'b.com'); + + $this->assertSame('GET a.com|b.com/', $route->toRoute()->getName()); + } + + public function testMethods(): void + { + $route = Route::methods([Method::POST, Method::HEAD], '/'); + + $this->assertSame([Method::POST, Method::HEAD], $route->toRoute()->getMethods()); + } + + public function testGetMethod(): void + { + $route = Route::get('/'); + + $this->assertSame([Method::GET], $route->toRoute()->getMethods()); + } + + public function testPostMethod(): void + { + $route = Route::post('/'); + + $this->assertSame([Method::POST], $route->toRoute()->getMethods()); + } + + public function testPutMethod(): void + { + $route = Route::put('/'); + + $this->assertSame([Method::PUT], $route->toRoute()->getMethods()); + } + + public function testDeleteMethod(): void + { + $route = Route::delete('/'); + + $this->assertSame([Method::DELETE], $route->toRoute()->getMethods()); + } + + public function testPatchMethod(): void + { + $route = Route::patch('/'); + + $this->assertSame([Method::PATCH], $route->toRoute()->getMethods()); + } + + public function testHeadMethod(): void + { + $route = Route::head('/'); + + $this->assertSame([Method::HEAD], $route->toRoute()->getMethods()); + } + + public function testOptionsMethod(): void + { + $route = Route::options('/'); + + $this->assertSame([Method::OPTIONS], $route->toRoute()->getMethods()); + } + + public function testPattern(): void + { + $route = Route::get('/test')->pattern('/test2'); + + $this->assertSame('/test2', $route->toRoute()->getPattern()); + } + + public function testHost(): void + { + $route = Route::get('/')->host('https://yiiframework.com/'); + + $this->assertSame('https://yiiframework.com', $route->toRoute()->getHosts()[0]); + } + + public function testHosts(): void + { + $route = Route::get('/') + ->hosts( + 'https://yiiframework.com/', + 'yf.com', + 'yii.com', + 'yf.ru' + ); + + $this->assertSame( + [ + 'https://yiiframework.com', + 'yf.com', + 'yii.com', + 'yf.ru', + ], + $route->toRoute()->getHosts() + ); + } + + public function testMultipleHosts(): void + { + $route = Route::get('/') + ->host('https://yiiframework.com/'); + $multipleRoute = Route::get('/') + ->hosts( + 'https://yiiframework.com/', + 'https://yiiframework.ru/' + ); + + $this->assertCount(1, $route->toRoute()->getHosts()); + $this->assertCount(2, $multipleRoute->toRoute()->getHosts()); + } + + public function testDefaults(): void + { + $route = Route::get('/{language}')->defaults([ + 'language' => 'en', + 'age' => 42, + ]); + + $this->assertSame([ + 'language' => 'en', + 'age' => '42', + ], $route->toRoute()->getDefaults()); + } + + public function testOverride(): void + { + $route = Route::get('/')->override(); + + $this->assertTrue($route->toRoute()->isOverride()); + } + + public function dataToString(): array + { + return [ + ['yiiframework.com/', '/'], + ['yiiframework.com/yiiframeworkXcom', '/yiiframeworkXcom'], + ]; + } + + /** + * @dataProvider dataToString + */ + public function testToString(string $expected, string $pattern): void + { + $route = Route::methods([Method::GET, Method::POST], $pattern) + ->name('test.route') + ->host('yiiframework.com'); + + $this->assertSame('[test.route] GET,POST ' . $expected, (string)$route->toRoute()); + } + + public function testToStringSimple(): void + { + $route = Route::get('/'); + + $this->assertSame('GET /', (string)$route->toRoute()); + } + + public function testDispatcherInjecting(): void + { + $request = new ServerRequest('GET', '/'); + $container = $this->getContainer( + [ + TestController::class => new TestController(), + ] + ); + + $route = Route::get('/')->action([TestController::class, 'index']); + + $response = $this + ->getDispatcher($container) + ->withMiddlewares($route->toRoute()->getEnabledMiddlewares()) + ->dispatch($request, $this->getRequestHandler()); + + $this->assertSame(200, $response->getStatusCode()); + } + + public function testDisabledMiddlewareDefinitions(): void + { + $request = new ServerRequest('GET', '/'); + + $route = Route::get('/') + ->middleware(TestMiddleware1::class, TestMiddleware2::class, TestMiddleware3::class) + ->action([TestController::class, 'index']) + ->disableMiddleware(TestMiddleware1::class, TestMiddleware3::class); + + $dispatcher = $this + ->getDispatcher( + $this->getContainer([ + TestMiddleware1::class => new TestMiddleware1(), + TestMiddleware2::class => new TestMiddleware2(), + TestMiddleware3::class => new TestMiddleware3(), + TestController::class => new TestController(), + ]) + ) + ->withMiddlewares($route->toRoute()->getEnabledMiddlewares()); + + $response = $dispatcher->dispatch($request, $this->getRequestHandler()); + $this->assertSame(200, $response->getStatusCode()); + $this->assertSame('2', (string) $response->getBody()); + } + + public function testPrependMiddlewareDefinitions(): void + { + $request = new ServerRequest('GET', '/'); + + $route = Route::get('/') + ->middleware(TestMiddleware3::class) + ->action([TestController::class, 'index']) + ->prependMiddleware(TestMiddleware1::class, TestMiddleware2::class); + + $response = $this + ->getDispatcher( + $this->getContainer([ + TestMiddleware1::class => new TestMiddleware1(), + TestMiddleware2::class => new TestMiddleware2(), + TestMiddleware3::class => new TestMiddleware3(), + TestController::class => new TestController(), + ]) + ) + ->withMiddlewares($route->toRoute()->getEnabledMiddlewares()) + ->dispatch($request, $this->getRequestHandler()); + + $this->assertSame(200, $response->getStatusCode()); + $this->assertSame('123', (string) $response->getBody()); + } + + public function testPrependMiddlewaresAfterGetEnabledMiddlewares(): void + { + $route = Route::get('/') + ->middleware(TestMiddleware3::class) + ->disableMiddleware(TestMiddleware1::class) + ->action([TestController::class, 'index']); + + $route->toRoute()->getEnabledMiddlewares(); + + $route = $route->prependMiddleware(TestMiddleware1::class, TestMiddleware2::class); + + $this->assertSame( + [TestMiddleware2::class, TestMiddleware3::class, [TestController::class, 'index']], + $route->toRoute()->getEnabledMiddlewares() + ); + } + + public function testAddMiddlewareAfterGetEnabledMiddlewares(): void + { + $route = Route::get('/') + ->middleware(TestMiddleware3::class); + + $route->toRoute()->getEnabledMiddlewares(); + + $route = $route->middleware(TestMiddleware1::class, TestMiddleware2::class); + + $this->assertSame( + [TestMiddleware3::class, TestMiddleware1::class, TestMiddleware2::class], + $route->toRoute()->getEnabledMiddlewares() + ); + } + + public function testDisableMiddlewareAfterGetEnabledMiddlewares(): void + { + $route = Route::get('/') + ->middleware(TestMiddleware1::class, TestMiddleware2::class, TestMiddleware3::class); + + $route->toRoute()->getEnabledMiddlewares(); + + $route = $route->disableMiddleware(TestMiddleware1::class, TestMiddleware2::class); + + $this->assertSame( + [TestMiddleware3::class], + $route->toRoute()->getEnabledMiddlewares() + ); + } + + public function testGetEnabledMiddlewaresTwice(): void + { + $route = Route::get('/') + ->middleware(TestMiddleware1::class, TestMiddleware2::class); + + $result1 = $route->toRoute()->getEnabledMiddlewares(); + $result2 = $route->toRoute()->getEnabledMiddlewares(); + + $this->assertSame([TestMiddleware1::class, TestMiddleware2::class], $result1); + $this->assertSame($result1, $result2); + } + + public function testMiddlewaresWithKeys(): void + { + $route = Route::get('/') + ->middleware(m3: TestMiddleware3::class) + ->action([TestController::class, 'index']) + ->prependMiddleware(m1: TestMiddleware1::class, m2: TestMiddleware2::class) + ->disableMiddleware(m1: TestMiddleware1::class); + + $this->assertSame( + [TestMiddleware2::class, TestMiddleware3::class, [TestController::class, 'index']], + $route->toRoute()->getEnabledMiddlewares() + ); + } + + public function testImmutability(): void + { + $route = Route::get('/'); + $routeWithAction = $route->action(''); + + $this->assertNotSame($route, $route->name('')); + $this->assertNotSame($route, $route->pattern('')); + $this->assertNotSame($route, $route->host('')); + $this->assertNotSame($route, $route->hosts('')); + $this->assertNotSame($route, $route->override()); + $this->assertNotSame($route, $route->defaults([])); + $this->assertNotSame($route, $route->middleware()); + $this->assertNotSame($route, $route->action('')); + $this->assertNotSame($routeWithAction, $routeWithAction->prependMiddleware()); + $this->assertNotSame($route, $route->disableMiddleware('')); + } + + private function getRequestHandler(): RequestHandlerInterface + { + return new class () implements RequestHandlerInterface { + public function handle(ServerRequestInterface $request): ResponseInterface + { + return new Response(404); + } + }; + } + + private function getDispatcher(ContainerInterface $container = null): MiddlewareDispatcher + { + if ($container === null) { + return new MiddlewareDispatcher( + new MiddlewareFactory($this->getContainer()), + $this->createMock(EventDispatcherInterface::class) + ); + } + + return new MiddlewareDispatcher( + new MiddlewareFactory($container), + $this->createMock(EventDispatcherInterface::class) + ); + } + + private function getContainer(array $instances = []): ContainerInterface + { + return new Container($instances); + } +} diff --git a/tests/ConfigTest.php b/tests/ConfigTest.php index a7a85f6..a902e09 100644 --- a/tests/ConfigTest.php +++ b/tests/ConfigTest.php @@ -10,7 +10,7 @@ use Yiisoft\Di\ContainerConfig; use Yiisoft\Di\StateResetter; use Yiisoft\Router\CurrentRoute; -use Yiisoft\Router\Route; +use Yiisoft\Router\Builder\RouteBuilder as Route; use Yiisoft\Router\RouteCollector; use Yiisoft\Router\RouteCollectorInterface; @@ -29,7 +29,7 @@ public function testCurrentRoute(): void $container = $this->createContainer(); $currentRoute = $container->get(CurrentRoute::class); - $currentRoute->setRouteWithArguments(Route::get('/main'), ['name' => 'hello']); + $currentRoute->setRouteWithArguments(Route::get('/main')->toRoute(), ['name' => 'hello']); $currentRoute->setUri(new Uri('http://example.com/')); $container diff --git a/tests/CurrentRouteTest.php b/tests/CurrentRouteTest.php index 8afd10a..4270186 100644 --- a/tests/CurrentRouteTest.php +++ b/tests/CurrentRouteTest.php @@ -7,6 +7,7 @@ use LogicException; use Nyholm\Psr7\Uri; use PHPUnit\Framework\TestCase; +use Yiisoft\Http\Method; use Yiisoft\Router\CurrentRoute; use Yiisoft\Router\Route; @@ -14,38 +15,38 @@ class CurrentRouteTest extends TestCase { public function testGetName(): void { - $route = Route::get('')->name('test'); + $route = new Route([Method::GET], '', 'test'); $currentRoute = new CurrentRoute(); $currentRoute->setRouteWithArguments($route, []); - $this->assertSame($route->getData('name'), $currentRoute->getName()); + $this->assertSame($route->getName(), $currentRoute->getName()); } public function testGetHost(): void { - $route = Route::get('')->host('test.com'); + $route = new Route([Method::GET], '', hosts: ['test.com']); $currentRoute = new CurrentRoute(); $currentRoute->setRouteWithArguments($route, []); - $this->assertSame($route->getData('host'), $currentRoute->getHost()); + $this->assertSame($route->getHosts(), $currentRoute->getHosts()); } public function testGetPattern(): void { - $route = Route::get('/home'); + $route = new Route([Method::GET], '/home'); $currentRoute = new CurrentRoute(); $currentRoute->setRouteWithArguments($route, []); - $this->assertSame($route->getData('pattern'), $currentRoute->getPattern()); + $this->assertSame($route->getPattern(), $currentRoute->getPattern()); } public function testGetMethods(): void { - $route = Route::get(''); + $route = new Route([Method::GET], ''); $currentRoute = new CurrentRoute(); $currentRoute->setRouteWithArguments($route, []); - $this->assertSame($route->getData('methods'), $currentRoute->getMethods()); + $this->assertSame($route->getMethods(), $currentRoute->getMethods()); } public function testGetCurrentUri(): void @@ -64,7 +65,7 @@ public function testGetArguments(): void 'foo' => 'bar', ]; $currentRoute = new CurrentRoute(); - $currentRoute->setRouteWithArguments(Route::get(''), $parameters); + $currentRoute->setRouteWithArguments(new Route([Method::GET], ''), $parameters); $this->assertSame($parameters, $currentRoute->getArguments()); } @@ -76,7 +77,7 @@ public function testGetArgument(): void 'foo' => 'bar', ]; $currentRoute = new CurrentRoute(); - $currentRoute->setRouteWithArguments(Route::get(''), $parameters); + $currentRoute->setRouteWithArguments(new Route([Method::GET], ''), $parameters); $this->assertSame('bar', $currentRoute->getArgument('foo')); } @@ -84,7 +85,7 @@ public function testGetArgument(): void public function testGetArgumentWithDefault(): void { $currentRoute = new CurrentRoute(); - $currentRoute->setRouteWithArguments(Route::get(''), ['test' => 1]); + $currentRoute->setRouteWithArguments(new Route([Method::GET], ''), ['test' => 1]); $this->assertSame('bar', $currentRoute->getArgument('foo', 'bar')); } @@ -92,7 +93,7 @@ public function testGetArgumentWithDefault(): void public function testGetArgumentWithNonExist(): void { $currentRoute = new CurrentRoute(); - $currentRoute->setRouteWithArguments(Route::get(''), ['test' => 1]); + $currentRoute->setRouteWithArguments(new Route([Method::GET], ''), ['test' => 1]); $this->assertNull($currentRoute->getArgument('foo')); } @@ -103,8 +104,8 @@ public function testSetRouteTwice(): void $this->expectExceptionMessage('Can not set route/arguments since it was already set.'); $currentRoute = new CurrentRoute(); - $currentRoute->setRouteWithArguments(Route::get('')->name('test'), []); - $currentRoute->setRouteWithArguments(Route::get('/home')->name('home'), []); + $currentRoute->setRouteWithArguments(new Route([Method::GET], '', 'test'), []); + $currentRoute->setRouteWithArguments(new Route([Method::GET], '/home', 'home'), []); } public function testSetUriTwice(): void @@ -123,7 +124,7 @@ public function testSetArgumentsTwice(): void $this->expectExceptionMessage('Can not set route/arguments since it was already set.'); $currentRoute = new CurrentRoute(); - $currentRoute->setRouteWithArguments(Route::get(''), ['foo' => 'bar']); - $currentRoute->setRouteWithArguments(Route::get(''), ['id' => 1]); + $currentRoute->setRouteWithArguments(new Route([Method::GET], ''), ['foo' => 'bar']); + $currentRoute->setRouteWithArguments(new Route([Method::GET], ''), ['id' => 1]); } } diff --git a/tests/Debug/RouterCollectorTest.php b/tests/Debug/RouterCollectorTest.php index e3e5731..4c375b4 100644 --- a/tests/Debug/RouterCollectorTest.php +++ b/tests/Debug/RouterCollectorTest.php @@ -7,6 +7,9 @@ use PHPUnit\Framework\MockObject\MockObject; use Yiisoft\Di\Container; use Yiisoft\Di\ContainerConfig; +use Yiisoft\Http\Method; +use Yiisoft\Router\Builder\GroupBuilder; +use Yiisoft\Router\Builder\RouteBuilder; use Yiisoft\Router\Debug\RouterCollector; use Yiisoft\Router\Group; use Yiisoft\Router\Route; @@ -76,8 +79,8 @@ protected function checkCollectedData(array $data): void private function createRoutes(): array { return [ - Route::get('/'), - Group::create('/api')->routes(Route::get('/v1')), + new Route([Method::GET], '/'), + GroupBuilder::create('/api')->routes(RouteBuilder::get('/v1')), ]; } } diff --git a/tests/GroupTest.php b/tests/GroupTest.php index ecc850d..aa82bb6 100644 --- a/tests/GroupTest.php +++ b/tests/GroupTest.php @@ -6,243 +6,63 @@ use InvalidArgumentException; use Nyholm\Psr7\Response; -use Nyholm\Psr7\ServerRequest; use PHPUnit\Framework\TestCase; -use Psr\EventDispatcher\EventDispatcherInterface; -use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\ServerRequestInterface; -use Psr\Http\Server\RequestHandlerInterface; -use RuntimeException; -use Yiisoft\Middleware\Dispatcher\MiddlewareDispatcher; -use Yiisoft\Middleware\Dispatcher\MiddlewareFactory; use Yiisoft\Router\Group; -use Yiisoft\Router\Route; -use Yiisoft\Router\RouteCollection; -use Yiisoft\Router\RouteCollector; -use Yiisoft\Router\Tests\Support\Container; use Yiisoft\Router\Tests\Support\TestMiddleware1; use Yiisoft\Router\Tests\Support\TestMiddleware2; use Yiisoft\Router\Tests\Support\TestMiddleware3; final class GroupTest extends TestCase { - public function testAddMiddleware(): void - { - $group = Group::create(); - - $middleware1 = static fn () => new Response(); - $middleware2 = static fn () => new Response(); - - $group = $group - ->middleware($middleware1) - ->middleware($middleware2); - $this->assertCount(2, $group->getData('enabledMiddlewares')); - $this->assertSame($middleware1, $group->getData('enabledMiddlewares')[0]); - $this->assertSame($middleware2, $group->getData('enabledMiddlewares')[1]); - } - public function testDisabledMiddlewareDefinitions(): void { - $group = Group::create() - ->middleware(TestMiddleware3::class) - ->prependMiddleware(TestMiddleware1::class, TestMiddleware2::class) - ->disableMiddleware(TestMiddleware1::class, TestMiddleware3::class); + $group = (new Group()) + ->setDisabledMiddlewares([TestMiddleware1::class, TestMiddleware3::class]); - $this->assertCount(1, $group->getData('enabledMiddlewares')); - $this->assertSame(TestMiddleware2::class, $group->getData('enabledMiddlewares')[0]); + $this->assertCount(2, $group->getDisabledMiddlewares()); } - public function testPrependMiddlewaresAfterGetEnabledMiddlewares(): void + public function testEnabledMiddlewares(): void { - $group = Group::create() - ->middleware(TestMiddleware3::class) - ->disableMiddleware(TestMiddleware1::class); - - $group->getData('enabledMiddlewares'); + $group = (new Group()) + ->setMiddlewares([TestMiddleware1::class, TestMiddleware2::class, TestMiddleware3::class]) + ->setDisabledMiddlewares([TestMiddleware1::class, TestMiddleware3::class]); - $group = $group->prependMiddleware(TestMiddleware1::class, TestMiddleware2::class); - - $this->assertSame( - [TestMiddleware2::class, TestMiddleware3::class], - $group->getData('enabledMiddlewares') - ); + $this->assertCount(1, $group->getEnabledMiddlewares()); + $this->assertSame(TestMiddleware2::class, $group->getEnabledMiddlewares()[0]); } - public function testAddMiddlewareAfterGetEnabledMiddlewares(): void + public function testSetMiddlewaresAfterGetEnabledMiddlewares(): void { - $group = Group::create() - ->middleware(TestMiddleware3::class); + $group = (new Group()) + ->setMiddlewares([TestMiddleware3::class]) + ->setDisabledMiddlewares([TestMiddleware1::class]); - $group->getData('enabledMiddlewares'); + $group->getEnabledMiddlewares(); - $group = $group->middleware(TestMiddleware1::class, TestMiddleware2::class); + $group->setMiddlewares([TestMiddleware1::class, TestMiddleware2::class, ...$group->getMiddlewares()]); $this->assertSame( - [TestMiddleware3::class, TestMiddleware1::class, TestMiddleware2::class], - $group->getData('enabledMiddlewares') + [TestMiddleware2::class, TestMiddleware3::class], + $group->getEnabledMiddlewares() ); } public function testDisableMiddlewareAfterGetEnabledMiddlewares(): void { - $group = Group::create() - ->middleware(TestMiddleware1::class, TestMiddleware2::class, TestMiddleware3::class); + $group = (new Group) + ->setMiddlewares([TestMiddleware1::class, TestMiddleware2::class, TestMiddleware3::class]); - $group->getData('enabledMiddlewares'); + $group->getEnabledMiddlewares(); - $group = $group->disableMiddleware(TestMiddleware1::class, TestMiddleware2::class); + $group->setDisabledMiddlewares([TestMiddleware1::class, TestMiddleware2::class]); $this->assertSame( [TestMiddleware3::class], - $group->getData('enabledMiddlewares') + $group->getEnabledMiddlewares() ); } - public function testMiddlewaresWithKeys(): void - { - $group = Group::create() - ->middleware(m3: TestMiddleware3::class) - ->prependMiddleware(m1: TestMiddleware1::class, m2: TestMiddleware2::class) - ->disableMiddleware(m1: TestMiddleware1::class); - - $this->assertSame( - [TestMiddleware2::class, TestMiddleware3::class], - $group->getData('enabledMiddlewares') - ); - } - - public function testNamedArgumentsInMiddlewareMethods(): void - { - $group = Group::create() - ->middleware(TestMiddleware3::class) - ->prependMiddleware(TestMiddleware1::class, TestMiddleware2::class) - ->disableMiddleware(TestMiddleware1::class, TestMiddleware3::class); - - $this->assertCount(1, $group->getData('enabledMiddlewares')); - $this->assertSame(TestMiddleware2::class, $group->getData('enabledMiddlewares')[0]); - } - - public function testRoutesAfterMiddleware(): void - { - $group = Group::create(); - - $middleware1 = static fn () => new Response(); - - $group = $group->prependMiddleware($middleware1); - - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('routes() can not be used after prependMiddleware().'); - - $group->routes(Route::get('/')); - } - - public function testAddNestedMiddleware(): void - { - $request = new ServerRequest('GET', '/outergroup/innergroup/test1'); - - $action = static fn (ServerRequestInterface $request) => new Response(200, [], null, '1.1', implode('', $request->getAttributes())); - - $middleware1 = static function (ServerRequestInterface $request, RequestHandlerInterface $handler) { - $request = $request->withAttribute('middleware', 'middleware1'); - return $handler->handle($request); - }; - - $middleware2 = static function (ServerRequestInterface $request, RequestHandlerInterface $handler) { - $request = $request->withAttribute('middleware', 'middleware2'); - return $handler->handle($request); - }; - - $group = Group::create('/outergroup') - ->middleware($middleware1) - ->routes( - Group::create('/innergroup') - ->middleware($middleware2) - ->routes( - Route::get('/test1') - ->action($action) - ->name('request1'), - ) - ); - - $collector = new RouteCollector(); - $collector->addRoute($group); - - $routeCollection = new RouteCollection($collector); - $route = $routeCollection->getRoute('request1'); - $response = $this->getDispatcher() - ->withMiddlewares($route->getData('enabledMiddlewares')) - ->dispatch($request, $this->getRequestHandler()); - $this->assertSame(200, $response->getStatusCode()); - $this->assertSame('middleware2', $response->getReasonPhrase()); - } - - public function testGroupMiddlewareFullStackCalled(): void - { - $request = new ServerRequest('GET', '/group/test1'); - - $action = static fn (ServerRequestInterface $request) => new Response(200, [], null, '1.1', implode('', $request->getAttributes())); - $middleware1 = function (ServerRequestInterface $request, RequestHandlerInterface $handler) { - $request = $request->withAttribute('middleware', 'middleware1'); - return $handler->handle($request); - }; - $middleware2 = function (ServerRequestInterface $request, RequestHandlerInterface $handler) { - $request = $request->withAttribute('middleware', 'middleware2'); - return $handler->handle($request); - }; - - $group = Group::create('/group') - ->middleware($middleware1) - ->middleware($middleware2) - ->routes( - Route::get('/test1') - ->action($action) - ->name('request1'), - ); - - $collector = new RouteCollector(); - $collector->addRoute($group); - - $routeCollection = new RouteCollection($collector); - $route = $routeCollection->getRoute('request1'); - - $response = $this->getDispatcher() - ->withMiddlewares($route->getData('enabledMiddlewares')) - ->dispatch($request, $this->getRequestHandler()); - - $this->assertSame(200, $response->getStatusCode()); - $this->assertSame('middleware2', $response->getReasonPhrase()); - } - - public function testGroupMiddlewareStackInterrupted(): void - { - $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') - ->middleware($middleware1) - ->middleware($middleware2) - ->routes( - Route::get('/test1') - ->action($action) - ->name('request1') - ); - - $collector = new RouteCollector(); - $collector->addRoute($group); - - $routeCollection = new RouteCollection($collector); - $route = $routeCollection->getRoute('request1'); - - $response = $this->getDispatcher() - ->withMiddlewares($route->getData('enabledMiddlewares')) - ->dispatch($request, $this->getRequestHandler()); - - $this->assertSame(403, $response->getStatusCode()); - } - public function testInvalidMiddlewares(): void { $this->expectException(InvalidArgumentException::class); @@ -252,65 +72,11 @@ public function testInvalidMiddlewares(): void $group = new Group('/api', middlewares: [$middleware, new \stdClass()]); } - public function testAddGroup(): void - { - $logoutRoute = Route::post('/logout'); - $listRoute = Route::get('/'); - $viewRoute = Route::get('/{id}'); - - $middleware1 = static fn () => new Response(); - $middleware2 = static fn () => new Response(); - - $root = Group::create() - ->routes( - Group::create('/api') - ->middleware($middleware1) - ->middleware($middleware2) - ->routes( - $logoutRoute, - Group::create('/post') - ->routes( - $listRoute, - $viewRoute - ) - ), - ); - - $this->assertCount(1, $root->getData('routes')); - - /** @var Group $api */ - $api = $root->getData('routes')[0]; - - $this->assertSame('/api', $api->getData('prefix')); - $this->assertCount(2, $api->getData('routes')); - $this->assertSame($logoutRoute, $api->getData('routes')[0]); - - /** @var Group $postGroup */ - $postGroup = $api->getData('routes')[1]; - $this->assertInstanceOf(Group::class, $postGroup); - $this->assertCount(2, $api->getData('enabledMiddlewares')); - $this->assertSame($middleware1, $api->getData('enabledMiddlewares')[0]); - $this->assertSame($middleware2, $api->getData('enabledMiddlewares')[1]); - - $this->assertSame('/post', $postGroup->getData('prefix')); - $this->assertCount(2, $postGroup->getData('routes')); - $this->assertSame($listRoute, $postGroup->getData('routes')[0]); - $this->assertSame($viewRoute, $postGroup->getData('routes')[1]); - $this->assertEmpty($postGroup->getData('enabledMiddlewares')); - } - - public function testHost(): void - { - $group = Group::create()->host('https://yiiframework.com/'); - - $this->assertSame('https://yiiframework.com', $group->getData('host')); - } - public function testHosts(): void { - $group = Group::create()->hosts('https://yiiframework.com/', 'https://yiiframework.ru/'); + $group = (new Group())->setHosts(['https://yiiframework.com/']); - $this->assertSame(['https://yiiframework.com', 'https://yiiframework.ru'], $group->getData('hosts')); + $this->assertSame(['https://yiiframework.com'], $group->getHosts()); } public function testInvalidHosts(): void @@ -321,183 +87,24 @@ public function testInvalidHosts(): void $group = new Group(hosts: ['https://yiiframework.com/', 123]); } - public function testName(): void + public function testPrefix(): void { - $group = Group::create()->namePrefix('api'); - - $this->assertSame('api', $group->getData('namePrefix')); - } - - public function testGetDataWithWrongKey(): void - { - $group = Group::create(); - - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Unknown data key: wrong'); - - $group->getData('wrong'); - } - - public function testWithCors(): void - { - $group = Group::create() - ->routes( - Route::get('/info')->action(static fn () => 'info'), - Route::post('/info')->action(static fn () => 'info'), - ) - ->withCors( - static fn () => new Response(204) - ); - - $collector = new RouteCollector(); - $collector->addRoute($group); - $routeCollection = new RouteCollection($collector); - - $this->assertCount(3, $routeCollection->getRoutes()); - } - - public function testWithCorsWithHostRoutes(): void - { - $group = Group::create() - ->routes( - Route::get('/info') - ->action(static fn () => 'info') - ->host('yii.dev'), - Route::get('/info') - ->action(static fn () => 'info') - ->host('yii.test'), - ) - ->withCors( - static fn () => new Response(204) - ); - - $collector = new RouteCollector(); - $collector->addRoute($group); - $routeCollection = new RouteCollection($collector); + $group = (new Group())->setPrefix('/api'); - $this->assertCount(4, $routeCollection->getRoutes()); + $this->assertSame('/api', $group->getPrefix()); } - public function testWithCorsDoesntDuplicateRoutes(): void - { - $group = Group::create() - ->routes( - Route::get('/info') - ->action(static fn () => 'info') - ->host('yii.dev'), - Route::post('/info') - ->action(static fn () => 'info') - ->host('yii.dev'), - Route::put('/info') - ->action(static fn () => 'info') - ->host('yii.test'), - ) - ->withCors( - static fn () => new Response(204) - ); - - $collector = new RouteCollector(); - $collector->addRoute($group); - $routeCollection = new RouteCollection($collector); - - $this->assertCount(5, $routeCollection->getRoutes()); - } - - public function testWithCorsWithNestedGroups(): void - { - $group = Group::create()->routes( - Route::get('/info')->action(static fn () => 'info'), - Route::post('/info')->action(static fn () => 'info'), - Group::create('/v1') - ->routes( - Route::get('/post')->action(static fn () => 'post'), - Route::post('/post')->action(static fn () => 'post'), - Route::options('/options')->action(static fn () => 'options'), - ) - ->withCors( - static fn () => new Response(201) - ) - )->withCors( - static fn () => new Response(204) - ); - - $collector = new RouteCollector(); - $collector->addRoute($group); - - $routeCollection = new RouteCollection($collector); - $this->assertCount(7, $routeCollection->getRoutes()); - $this->assertInstanceOf(Route::class, $routeCollection->getRoute('OPTIONS /v1/post')); - } - - public function testWithCorsWithNestedGroups2(): void - { - $group = Group::create()->routes( - Route::get('/info')->action(static fn () => 'info'), - Route::post('/info')->action(static fn () => 'info'), - Route::get('/v1/post')->action(static fn () => 'post'), - Group::create('/v1')->routes( - Route::post('/post')->action(static fn () => 'post'), - Route::options('/options')->action(static fn () => 'options'), - ), - Group::create('/v1')->routes( - Route::put('/post')->action(static fn () => 'post'), - ) - )->withCors( - static fn () => new Response(204) - ); - $collector = new RouteCollector(); - $collector->addRoute($group); - - $routeCollection = new RouteCollection($collector); - $this->assertCount(8, $routeCollection->getRoutes()); - $this->assertInstanceOf(Route::class, $routeCollection->getRoute('OPTIONS /v1/post')); - } - - public function testMiddlewareAfterRoutes(): void - { - $group = Group::create()->routes(Route::get('/info')->action(static fn () => 'info')); - - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('middleware() can not be used after routes().'); - $group->middleware(static fn () => new Response()); - } - - public function testDuplicateHosts(): void - { - $route = Group::create()->hosts('a.com', 'b.com', 'a.com'); - - $this->assertSame(['a.com', 'b.com'], $route->getData('hosts')); - } - - public function testImmutability(): void + public function testName(): void { - $group = Group::create(); + $group = (new Group())->setNamePrefix('api'); - $this->assertNotSame($group, $group->routes()); - $this->assertNotSame($group, $group->withCors(null)); - $this->assertNotSame($group, $group->middleware()); - $this->assertNotSame($group, $group->prependMiddleware()); - $this->assertNotSame($group, $group->namePrefix('')); - $this->assertNotSame($group, $group->hosts()); - $this->assertNotSame($group, $group->disableMiddleware()); + $this->assertSame('api', $group->getNamePrefix()); } - private function getRequestHandler(): RequestHandlerInterface + public function testCors(): void { - return new class () implements RequestHandlerInterface { - public function handle(ServerRequestInterface $request): ResponseInterface - { - return new Response(404); - } - }; - } + $group = (new Group())->setCorsMiddleware($cors = static fn () => new Response()); - private function getDispatcher(): MiddlewareDispatcher - { - $container = new Container([]); - return new MiddlewareDispatcher( - new MiddlewareFactory($container), - $this->createMock(EventDispatcherInterface::class) - ); + $this->assertSame($cors, $group->getCorsMiddleware()); } } diff --git a/tests/MatchingResultTest.php b/tests/MatchingResultTest.php index 10d67b8..e3cb650 100644 --- a/tests/MatchingResultTest.php +++ b/tests/MatchingResultTest.php @@ -14,7 +14,7 @@ final class MatchingResultTest extends TestCase { public function testFromSuccess(): void { - $route = Route::get('/{name}'); + $route = new Route([Method::GET], '/{name}'); $result = MatchingResult::fromSuccess($route, ['name' => 'Mehdi']); $this->assertTrue($result->isSuccess()); diff --git a/tests/Middleware/RouterTest.php b/tests/Middleware/RouterTest.php index 72c5ead..8b3545c 100644 --- a/tests/Middleware/RouterTest.php +++ b/tests/Middleware/RouterTest.php @@ -15,10 +15,10 @@ use Yiisoft\Http\Method; use Yiisoft\Middleware\Dispatcher\MiddlewareFactory; use Yiisoft\Router\CurrentRoute; -use Yiisoft\Router\Group; +use Yiisoft\Router\Builder\GroupBuilder as Group; use Yiisoft\Router\MatchingResult; use Yiisoft\Router\Middleware\Router; -use Yiisoft\Router\Route; +use Yiisoft\Router\Builder\RouteBuilder as Route; use Yiisoft\Router\RouteCollection; use Yiisoft\Router\RouteCollectionInterface; use Yiisoft\Router\RouteCollector; @@ -232,7 +232,7 @@ public function match(ServerRequestInterface $request): MatchingResult ->getUri() ->getPath() === '/options') { $route = Route::options('/options')->middleware($this->middleware); - return MatchingResult::fromSuccess($route, ['method' => 'options']); + return MatchingResult::fromSuccess($route->toRoute(), ['method' => 'options']); } if ($request @@ -243,7 +243,7 @@ public function match(ServerRequestInterface $request): MatchingResult if ($request->getMethod() === Method::GET) { $route = Route::get('/')->middleware($this->middleware); - return MatchingResult::fromSuccess($route, ['parameter' => 'value']); + return MatchingResult::fromSuccess($route->toRoute(), ['parameter' => 'value']); } return MatchingResult::fromFailure([Method::GET, Method::HEAD]); diff --git a/tests/RouteCollectionTest.php b/tests/RouteCollectionTest.php index 72d71b9..ab47f9e 100644 --- a/tests/RouteCollectionTest.php +++ b/tests/RouteCollectionTest.php @@ -15,8 +15,8 @@ use RuntimeException; use Yiisoft\Middleware\Dispatcher\MiddlewareDispatcher; use Yiisoft\Middleware\Dispatcher\MiddlewareFactory; -use Yiisoft\Router\Group; -use Yiisoft\Router\Route; +use Yiisoft\Router\Builder\GroupBuilder as Group; +use Yiisoft\Router\Builder\RouteBuilder as Route; use Yiisoft\Router\RouteCollection; use Yiisoft\Router\RouteCollector; use Yiisoft\Router\RouteNotFoundException; @@ -89,7 +89,7 @@ public function testRouteOverride(): void $routeCollection = new RouteCollection($collector); $route = $routeCollection->getRoute('my-route'); - $this->assertSame('/{id}', $route->getData('pattern')); + $this->assertSame('/{id}', $route->getPattern()); } public function testRouteWithoutAction(): void @@ -108,7 +108,7 @@ public function testRouteWithoutAction(): void $routeCollection = new RouteCollection($collector); $route = $routeCollection->getRoute('image'); - $this->assertFalse($route->getData('hasMiddlewares')); + $this->assertEmpty($route->getAction()); } public function testGetRouterTree(): void @@ -207,10 +207,10 @@ public function testGroupHost(): void $route1 = $routeCollection->getRoute('image'); $route2 = $routeCollection->getRoute('project'); $route3 = $routeCollection->getRoute('user'); - $this->assertSame('https://yiiframework.com', $route1->getData('host')); - $this->assertCount(2, $route2->getData('hosts')); - $this->assertSame(['https://yiipowered.com', 'https://yiiframework.ru'], $route2->getData('hosts')); - $this->assertSame('https://yiiframework.com', $route3->getData('host')); + $this->assertSame('https://yiiframework.com', $route1->getHosts()[0]); + $this->assertCount(2, $route2->getHosts()); + $this->assertSame(['https://yiipowered.com', 'https://yiiframework.ru'], $route2->getHosts()); + $this->assertSame('https://yiiframework.com', $route3->getHosts()[0]); } public function testGroupName(): void @@ -239,10 +239,10 @@ public function testGroupName(): void $route2 = $routeCollection->getRoute('api/v1/package/downloads'); $route3 = $routeCollection->getRoute('api/index'); $route4 = $routeCollection->getRoute('GET api/user/{username}'); - $this->assertInstanceOf(Route::class, $route1); - $this->assertInstanceOf(Route::class, $route2); - $this->assertInstanceOf(Route::class, $route3); - $this->assertInstanceOf(Route::class, $route4); + $this->assertInstanceOf(\Yiisoft\Router\Route::class, $route1); + $this->assertInstanceOf(\Yiisoft\Router\Route::class, $route2); + $this->assertInstanceOf(\Yiisoft\Router\Route::class, $route3); + $this->assertInstanceOf(\Yiisoft\Router\Route::class, $route4); } public function testCollectorMiddlewareFullstackCalled(): void @@ -277,10 +277,10 @@ public function testCollectorMiddlewareFullstackCalled(): void $route2 = $routeCollection->getRoute('view'); $request = new ServerRequest('GET', '/'); $response1 = $this->getDispatcher() - ->withMiddlewares($route1->getData('enabledMiddlewares')) + ->withMiddlewares($route1->getEnabledMiddlewares()) ->dispatch($request, $this->getRequestHandler()); $response2 = $this->getDispatcher() - ->withMiddlewares($route2->getData('enabledMiddlewares')) + ->withMiddlewares($route2->getEnabledMiddlewares()) ->dispatch($request, $this->getRequestHandler()); $this->assertEquals('middleware1', $response1->getReasonPhrase()); @@ -328,7 +328,7 @@ public function testMiddlewaresOrder(bool $groupWrapped): void TestController::class => new TestController(), ]) ) - ->withMiddlewares($route->getData('enabledMiddlewares')); + ->withMiddlewares($route->getEnabledMiddlewares()); $response = $dispatcher->dispatch($request, $this->getRequestHandler()); $this->assertSame(200, $response->getStatusCode()); @@ -354,7 +354,7 @@ public function testStaticRouteWithCollectorMiddlewares(): void TestMiddleware1::class => new TestMiddleware1(), ]) ) - ->withMiddlewares($route->getData('enabledMiddlewares')); + ->withMiddlewares($route->getEnabledMiddlewares()); $this->expectException(RuntimeException::class); $this->expectExceptionMessage('Stack is empty.'); diff --git a/tests/RouteCollectorTest.php b/tests/RouteCollectorTest.php index 5fa1ea1..f30a9d4 100644 --- a/tests/RouteCollectorTest.php +++ b/tests/RouteCollectorTest.php @@ -6,8 +6,8 @@ use Nyholm\Psr7\Response; use PHPUnit\Framework\TestCase; -use Yiisoft\Router\Group; -use Yiisoft\Router\Route; +use Yiisoft\Router\Builder\GroupBuilder as Group; +use Yiisoft\Router\Builder\RouteBuilder as Route; use Yiisoft\Router\RouteCollector; final class RouteCollectorTest extends TestCase @@ -73,12 +73,12 @@ public function testAddMiddleware(): void ->middleware($middleware3, $middleware4) ->middleware($middleware5) ->prependMiddleware($middleware1, $middleware2); - $this->assertCount(5, $collector->getMiddlewareDefinitions()); - $this->assertSame($middleware1, $collector->getMiddlewareDefinitions()[0]); - $this->assertSame($middleware2, $collector->getMiddlewareDefinitions()[1]); - $this->assertSame($middleware3, $collector->getMiddlewareDefinitions()[2]); - $this->assertSame($middleware4, $collector->getMiddlewareDefinitions()[3]); - $this->assertSame($middleware5, $collector->getMiddlewareDefinitions()[4]); + $this->assertCount(5, $collector->getMiddlewares()); + $this->assertSame($middleware1, $collector->getMiddlewares()[0]); + $this->assertSame($middleware2, $collector->getMiddlewares()[1]); + $this->assertSame($middleware3, $collector->getMiddlewares()[2]); + $this->assertSame($middleware4, $collector->getMiddlewares()[3]); + $this->assertSame($middleware5, $collector->getMiddlewares()[4]); } public function testNamedArgumentsInMiddlewareMethods(): void @@ -91,7 +91,7 @@ public function testNamedArgumentsInMiddlewareMethods(): void $collector ->middleware(a: $middleware2) ->prependMiddleware(b: $middleware1); - $this->assertSame($middleware1, $collector->getMiddlewareDefinitions()[0]); - $this->assertSame($middleware2, $collector->getMiddlewareDefinitions()[1]); + $this->assertSame($middleware1, $collector->getMiddlewares()[0]); + $this->assertSame($middleware2, $collector->getMiddlewares()[1]); } } diff --git a/tests/RouteTest.php b/tests/RouteTest.php index cf701bc..6a808e3 100644 --- a/tests/RouteTest.php +++ b/tests/RouteTest.php @@ -5,23 +5,13 @@ namespace Yiisoft\Router\Tests; use Nyholm\Psr7\Response; -use Nyholm\Psr7\ServerRequest; use PHPUnit\Framework\TestCase; -use Psr\Container\ContainerInterface; -use Psr\EventDispatcher\EventDispatcherInterface; -use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\ServerRequestInterface; -use Psr\Http\Server\RequestHandlerInterface; -use RuntimeException; use Yiisoft\Http\Method; -use Yiisoft\Middleware\Dispatcher\MiddlewareDispatcher; -use Yiisoft\Middleware\Dispatcher\MiddlewareFactory; 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 @@ -39,8 +29,23 @@ public function testSimpleInstance(): void ); $this->assertInstanceOf(Route::class, $route); - $this->assertCount(2, $route->getData('enabledMiddlewares')); - $this->assertTrue($route->getData('override')); + $this->assertCount(2, $route->getEnabledMiddlewares()); + $this->assertTrue($route->isOverride()); + } + + public function testDisabledMiddlewares(): void + { + $route = new Route( + methods: [Method::GET], + pattern: '/', + action: [TestController::class, 'index'], + middlewares: [TestMiddleware1::class], + override: true, + ); + $route->setDisabledMiddlewares([TestMiddleware2::class]); + + $this->assertCount(1, $route->getDisabledMiddlewares()); + $this->assertSame(TestMiddleware2::class, $route->getDisabledMiddlewares()[0]); } public function testEmptyMethods(): void @@ -53,114 +58,48 @@ public function testEmptyMethods(): void public function testName(): void { - $route = Route::get('/')->name('test.route'); + $route = (new Route([Method::GET], '/'))->setName('test.route'); - $this->assertSame('test.route', $route->getData('name')); + $this->assertSame('test.route', $route->getName()); } public function testNameDefault(): void { - $route = Route::get('/'); + $route = new Route([Method::GET], '/'); - $this->assertSame('GET /', $route->getData('name')); + $this->assertSame('GET /', $route->getName()); } public function testNameDefaultWithHosts(): void { - $route = Route::get('/')->hosts('a.com', 'b.com'); + $route = (new Route([Method::GET], '/'))->setHosts(['a.com', 'b.com']); - $this->assertSame('GET a.com|b.com/', $route->getData('name')); + $this->assertSame('GET a.com|b.com/', $route->getName()); } public function testMethods(): void { - $route = Route::methods([Method::POST, Method::HEAD], '/'); - - $this->assertSame([Method::POST, Method::HEAD], $route->getData('methods')); - } - - public function testGetDataWithWrongKey(): void - { - $route = Route::get(''); - - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Unknown data key: wrong'); - - $route->getData('wrong'); - } - - public function testGetMethod(): void - { - $route = Route::get('/'); - - $this->assertSame([Method::GET], $route->getData('methods')); - } - - public function testPostMethod(): void - { - $route = Route::post('/'); - - $this->assertSame([Method::POST], $route->getData('methods')); - } - - public function testPutMethod(): void - { - $route = Route::put('/'); - - $this->assertSame([Method::PUT], $route->getData('methods')); - } - - public function testDeleteMethod(): void - { - $route = Route::delete('/'); + $route = new Route([Method::POST, Method::HEAD], '/'); - $this->assertSame([Method::DELETE], $route->getData('methods')); - } - - public function testPatchMethod(): void - { - $route = Route::patch('/'); - - $this->assertSame([Method::PATCH], $route->getData('methods')); - } - - public function testHeadMethod(): void - { - $route = Route::head('/'); - - $this->assertSame([Method::HEAD], $route->getData('methods')); - } - - public function testOptionsMethod(): void - { - $route = Route::options('/'); - - $this->assertSame([Method::OPTIONS], $route->getData('methods')); + $this->assertSame([Method::POST, Method::HEAD], $route->getMethods()); } public function testPattern(): void { - $route = Route::get('/test')->pattern('/test2'); + $route = (new Route([Method::GET], '/test'))->setPattern('/test2'); - $this->assertSame('/test2', $route->getData('pattern')); - } - - public function testHost(): void - { - $route = Route::get('/')->host('https://yiiframework.com/'); - - $this->assertSame('https://yiiframework.com', $route->getData('host')); + $this->assertSame('/test2', $route->getPattern()); } public function testHosts(): void { - $route = Route::get('/') - ->hosts( + $route = (new Route([Method::GET], '/')) + ->setHosts([ 'https://yiiframework.com/', 'yf.com', 'yii.com', - 'yf.ru' - ); + 'yf.ru', + ]); $this->assertSame( [ @@ -169,27 +108,13 @@ public function testHosts(): void 'yii.com', 'yf.ru', ], - $route->getData('hosts') + $route->getHosts() ); } - public function testMultipleHosts(): void - { - $route = Route::get('/') - ->host('https://yiiframework.com/'); - $multipleRoute = Route::get('/') - ->hosts( - 'https://yiiframework.com/', - 'https://yiiframework.ru/' - ); - - $this->assertCount(1, $route->getData('hosts')); - $this->assertCount(2, $multipleRoute->getData('hosts')); - } - public function testDefaults(): void { - $route = Route::get('/{language}')->defaults([ + $route = (new Route([Method::GET], '/{language}'))->setDefaults([ 'language' => 'en', 'age' => 42, ]); @@ -197,14 +122,14 @@ public function testDefaults(): void $this->assertSame([ 'language' => 'en', 'age' => '42', - ], $route->getData('defaults')); + ], $route->getDefaults()); } public function testOverride(): void { - $route = Route::get('/')->override(); + $route = (new Route([Method::GET], '/'))->setOverride(true); - $this->assertTrue($route->getData('override')); + $this->assertTrue($route->isOverride()); } public function dataToString(): array @@ -220,178 +145,18 @@ public function dataToString(): array */ public function testToString(string $expected, string $pattern): void { - $route = Route::methods([Method::GET, Method::POST], $pattern) - ->name('test.route') - ->host('yiiframework.com'); + $route = (new Route([Method::GET, Method::POST], $pattern)) + ->setName('test.route') + ->setHosts(['yiiframework.com']); - $this->assertSame('[test.route] GET,POST ' . $expected, (string)$route); + $this->assertSame('[test.route] GET,POST ' . $expected, (string) $route); } public function testToStringSimple(): void { - $route = Route::get('/'); - - $this->assertSame('GET /', (string)$route); - } - - public function testDispatcherInjecting(): void - { - $request = new ServerRequest('GET', '/'); - $container = $this->getContainer( - [ - TestController::class => new TestController(), - ] - ); - - $route = Route::get('/')->action([TestController::class, 'index']); - - $response = $this - ->getDispatcher($container) - ->withMiddlewares($route->getData('enabledMiddlewares')) - ->dispatch($request, $this->getRequestHandler()); - - $this->assertSame(200, $response->getStatusCode()); - } - - public function testMiddlewareAfterAction(): void - { - $route = Route::get('/')->action([TestController::class, 'index']); - - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('middleware() can not be used after action().'); - $route->middleware(static fn () => new Response()); - } - - public function testPrependMiddlewareBeforeAction(): void - { - $route = Route::get('/'); - - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('prependMiddleware() can not be used before action().'); - $route->prependMiddleware(static fn () => new Response()); - } - - public function testDisabledMiddlewareDefinitions(): void - { - $request = new ServerRequest('GET', '/'); - - $route = Route::get('/') - ->middleware(TestMiddleware1::class, TestMiddleware2::class, TestMiddleware3::class) - ->action([TestController::class, 'index']) - ->disableMiddleware(TestMiddleware1::class, TestMiddleware3::class); - - $dispatcher = $this - ->getDispatcher( - $this->getContainer([ - TestMiddleware1::class => new TestMiddleware1(), - TestMiddleware2::class => new TestMiddleware2(), - TestMiddleware3::class => new TestMiddleware3(), - TestController::class => new TestController(), - ]) - ) - ->withMiddlewares($route->getData('enabledMiddlewares')); - - $response = $dispatcher->dispatch($request, $this->getRequestHandler()); - $this->assertSame(200, $response->getStatusCode()); - $this->assertSame('2', (string) $response->getBody()); - } - - public function testPrependMiddlewareDefinitions(): void - { - $request = new ServerRequest('GET', '/'); - - $route = Route::get('/') - ->middleware(TestMiddleware3::class) - ->action([TestController::class, 'index']) - ->prependMiddleware(TestMiddleware1::class, TestMiddleware2::class); - - $response = $this - ->getDispatcher( - $this->getContainer([ - TestMiddleware1::class => new TestMiddleware1(), - TestMiddleware2::class => new TestMiddleware2(), - TestMiddleware3::class => new TestMiddleware3(), - TestController::class => new TestController(), - ]) - ) - ->withMiddlewares($route->getData('enabledMiddlewares')) - ->dispatch($request, $this->getRequestHandler()); - - $this->assertSame(200, $response->getStatusCode()); - $this->assertSame('123', (string) $response->getBody()); - } - - public function testPrependMiddlewaresAfterGetEnabledMiddlewares(): void - { - $route = Route::get('/') - ->middleware(TestMiddleware3::class) - ->disableMiddleware(TestMiddleware1::class) - ->action([TestController::class, 'index']); - - $route->getData('enabledMiddlewares'); - - $route = $route->prependMiddleware(TestMiddleware1::class, TestMiddleware2::class); - - $this->assertSame( - [TestMiddleware2::class, TestMiddleware3::class, [TestController::class, 'index']], - $route->getData('enabledMiddlewares') - ); - } - - public function testAddMiddlewareAfterGetEnabledMiddlewares(): void - { - $route = Route::get('/') - ->middleware(TestMiddleware3::class); - - $route->getData('enabledMiddlewares'); - - $route = $route->middleware(TestMiddleware1::class, TestMiddleware2::class); - - $this->assertSame( - [TestMiddleware3::class, TestMiddleware1::class, TestMiddleware2::class], - $route->getData('enabledMiddlewares') - ); - } - - public function testDisableMiddlewareAfterGetEnabledMiddlewares(): void - { - $route = Route::get('/') - ->middleware(TestMiddleware1::class, TestMiddleware2::class, TestMiddleware3::class); + $route = new Route([Method::GET], '/'); - $route->getData('enabledMiddlewares'); - - $route = $route->disableMiddleware(TestMiddleware1::class, TestMiddleware2::class); - - $this->assertSame( - [TestMiddleware3::class], - $route->getData('enabledMiddlewares') - ); - } - - public function testGetEnabledMiddlewaresTwice(): void - { - $route = Route::get('/') - ->middleware(TestMiddleware1::class, TestMiddleware2::class); - - $result1 = $route->getData('enabledMiddlewares'); - $result2 = $route->getData('enabledMiddlewares'); - - $this->assertSame([TestMiddleware1::class, TestMiddleware2::class], $result1); - $this->assertSame($result1, $result2); - } - - public function testMiddlewaresWithKeys(): void - { - $route = Route::get('/') - ->middleware(m3: TestMiddleware3::class) - ->action([TestController::class, 'index']) - ->prependMiddleware(m1: TestMiddleware1::class, m2: TestMiddleware2::class) - ->disableMiddleware(m1: TestMiddleware1::class); - - $this->assertSame( - [TestMiddleware2::class, TestMiddleware3::class, [TestController::class, 'index']], - $route->getData('enabledMiddlewares') - ); + $this->assertSame('GET /', (string) $route); } public function testInvalidMiddlewares(): void @@ -404,15 +169,17 @@ public function testInvalidMiddlewares(): void public function testDebugInfo(): void { - $route = Route::get('/') - ->name('test') - ->host('example.com') - ->defaults(['age' => 42]) - ->override() - ->middleware(TestMiddleware1::class, TestMiddleware2::class) - ->disableMiddleware(TestMiddleware2::class) - ->action('go') - ->prependMiddleware(TestMiddleware3::class); + $route = new Route( + methods: [Method::GET], + pattern: '/', + name: 'test', + action: 'go', + middlewares: [TestMiddleware3::class, TestMiddleware1::class, TestMiddleware2::class], + defaults: ['age' => 42], + hosts: ['example.com'], + override: true, + disabledMiddlewares: [TestMiddleware2::class] + ); $expected = << / + [action] => go [hosts] => Array ( [0] => example.com @@ -435,13 +203,11 @@ public function testDebugInfo(): void ) [override] => 1 - [actionAdded] => 1 [middlewares] => Array ( [0] => Yiisoft\Router\Tests\Support\TestMiddleware3 [1] => Yiisoft\Router\Tests\Support\TestMiddleware1 [2] => Yiisoft\Router\Tests\Support\TestMiddleware2 - [3] => go ) [disabledMiddlewares] => Array @@ -464,9 +230,9 @@ public function testDebugInfo(): void public function testDuplicateHosts(): void { - $route = Route::get('/')->hosts('a.com', 'b.com', 'a.com'); + $route = (new Route([Method::GET], '/'))->setHosts(['a.com', 'b.com', 'a.com']); - $this->assertSame(['a.com', 'b.com'], $route->getData('hosts')); + $this->assertSame(['a.com', 'b.com'], $route->getHosts()); } public function testInvalidHosts(): void @@ -476,51 +242,4 @@ public function testInvalidHosts(): void $route = new Route([Method::GET], '/', hosts: ['b.com', 123]); } - - public function testImmutability(): void - { - $route = Route::get('/'); - $routeWithAction = $route->action(''); - - $this->assertNotSame($route, $route->name('')); - $this->assertNotSame($route, $route->pattern('')); - $this->assertNotSame($route, $route->host('')); - $this->assertNotSame($route, $route->hosts('')); - $this->assertNotSame($route, $route->override()); - $this->assertNotSame($route, $route->defaults([])); - $this->assertNotSame($route, $route->middleware()); - $this->assertNotSame($route, $route->action('')); - $this->assertNotSame($routeWithAction, $routeWithAction->prependMiddleware()); - $this->assertNotSame($route, $route->disableMiddleware('')); - } - - private function getRequestHandler(): RequestHandlerInterface - { - return new class () implements RequestHandlerInterface { - public function handle(ServerRequestInterface $request): ResponseInterface - { - return new Response(404); - } - }; - } - - private function getDispatcher(ContainerInterface $container = null): MiddlewareDispatcher - { - if ($container === null) { - return new MiddlewareDispatcher( - new MiddlewareFactory($this->getContainer()), - $this->createMock(EventDispatcherInterface::class) - ); - } - - return new MiddlewareDispatcher( - new MiddlewareFactory($container), - $this->createMock(EventDispatcherInterface::class) - ); - } - - private function getContainer(array $instances = []): ContainerInterface - { - return new Container($instances); - } }