diff --git a/.phpstorm.meta.php/Group.php b/.phpstorm.meta.php/Group.php deleted file mode 100644 index c2f0bb06..00000000 --- a/.phpstorm.meta.php/Group.php +++ /dev/null @@ -1,18 +0,0 @@ -name('post-delete') @@ -176,6 +178,7 @@ Besides action, additional middleware to execute before the action itself could ```php use Yiisoft\Router\Route; +use Yiisoft\Http\Method; Route::methods([Method::GET, Method::POST], '/page/add') ->middleware(Authentication::class) @@ -200,14 +203,34 @@ Route::get('/special') ->override(); ``` +Every fluent configuration method returns a new builder. The collector converts route builders to `Route` objects while +building the collection. Most applications do not need to import or convert builders directly. + +For configuration generated dynamically, a mutable `Route` data object may be constructed directly: + +```php +use Yiisoft\Http\Method; +use Yiisoft\Router\Route; + +$route = new Route( + methods: [Method::GET, Method::POST], + pattern: '/page/add', + name: 'page-add', + action: [PageController::class, 'actionAdd'], +); + +$route->setHosts(['https://example.com']); +``` + ### Route groups -Routes could be grouped. That is useful for API endpoints and similar cases: +Create route groups with the static `Group::create()` method. It returns an immutable builder and is useful for API +endpoints and similar cases: ```php -use \Yiisoft\Router\Route; -use \Yiisoft\Router\Group; -use \Yiisoft\Router\RouteCollectorInterface; +use Yiisoft\Router\Group; +use Yiisoft\Router\Route; +use Yiisoft\Router\RouteCollectorInterface; // for obtaining router see adapter package of choice readme $collector = $container->get(RouteCollectorInterface::class); @@ -233,6 +256,49 @@ and `disableMiddleware()`. These middleware are executed prior to matched route' If host is specified, all routes in the group would match only if the host match. +Every fluent group configuration method returns a new builder, which the collector accepts directly. Most applications +do not need to import or convert group builders. A mutable `Group` data object can also be constructed directly: + +```php +use Yiisoft\Router\Group; + +$group = new Group( + prefix: '/api', + namePrefix: 'api/', + routes: [$route], + middlewares: [ApiAuthentication::class], +); +``` + +### Custom route definitions + +`RouteCollectorInterface::addRoute()` accepts `Route`, `Group`, and `RoutableInterface` instances. Implement +`RoutableInterface` when an application or package needs its own route-definition abstraction: + +```php +use Yiisoft\Http\Method; +use Yiisoft\Router\RoutableInterface; +use Yiisoft\Router\Route; + +final class HealthCheckRoute implements RoutableInterface +{ + public function toRoute(): Route + { + return new Route( + methods: [Method::GET], + pattern: '/health', + name: 'health', + action: HealthCheckAction::class, + ); + } +} + +$collector->addRoute(new HealthCheckRoute()); +``` + +The route collection clones the `Route` or `Group` returned by `toRoute()` before applying collection and group +configuration, so implementations may safely return a retained object. + ### Automatic OPTIONS response and CORS By default, router responds automatically to OPTIONS requests based on the routes defined: @@ -246,8 +312,8 @@ Generally that is fine unless you need [CORS headers](https://developer.mozilla. case, you can add a middleware for handling it such as [tuupola/cors-middleware](https://github.com/tuupola/cors-middleware): ```php +use Tuupola\Middleware\CorsMiddleware; use Yiisoft\Router\Group; -use \Tuupola\Middleware\CorsMiddleware; return [ Group::create('/api') @@ -344,7 +410,7 @@ modifying URLs for filtering and/or sorting. For such a route: ```php -use \Yiisoft\Router\Route; +use Yiisoft\Router\Route; $routes = [ Route::post('/post/{id:\d+}') @@ -358,8 +424,6 @@ The information could be obtained as follows: use Psr\Http\Message\ResponseInterface use Psr\Http\Message\UriInterface; use Yiisoft\Router\CurrentRoute; -use Yiisoft\Router\Route; - final class PostController { public function actionEdit(CurrentRoute $currentRoute): ResponseInterface @@ -379,7 +443,7 @@ In addition to commonly used `getArgument()` method, the following methods are a - `getArguments()` - To obtain all arguments at once. - `getName()` - To get route name. -- `getHost()` - To get route host. +- `getHosts()` - To get route hosts. - `getPattern()` - To get route pattern. - `getMethods()` - To get route methods. - `getUri()` - To get current URI. diff --git a/UPGRADE.md b/UPGRADE.md index bc3c5f19..95e98297 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -3,6 +3,134 @@ This file contains the upgrade notes for the Yii Router. These notes highlight changes that could break your application when you upgrade it from one major version to another. +## 5.0.0 + +### Route and group builders + +The immutable fluent APIs were moved from `Yiisoft\Router\Route` and `Yiisoft\Router\Group` to dedicated builder classes: + +- `Yiisoft\Router\Builder\RouteBuilder` +- `Yiisoft\Router\Builder\GroupBuilder` + +The static factory methods on `Route` and `Group` are retained as facades. They now return the corresponding builder +instead of a `Route` or `Group` data object. + +- If you only use fluent route declarations such as `Route::get('/')->name('home')` and pass their results to + `RouteCollectorInterface::addRoute()`, then no changes are required. +- If you type a result of `Route::get()`, `Route::post()`, `Route::methods()`, or another route factory as `Route`, then + change the type to `RouteBuilder` or call `toRoute()` to obtain a `Route` data object. +- If you type a result of `Group::create()` as `Group`, then change the type to `GroupBuilder` or call `toRoute()` to + obtain a `Group` data object. +- If you check a fluent factory result with `instanceof Route` or `instanceof Group`, then check for the corresponding + builder instead, or call `toRoute()` before the check. +- If you call `getData()` on a fluent factory result, then call `toRoute()` and use an explicit getter on the resulting + data object. +- If you want imports to reflect the actual types returned by the factories, then import the builders directly. Aliases + allow route declarations to keep the familiar short names: + +```php +use Yiisoft\Router\Builder\GroupBuilder as Group; +use Yiisoft\Router\Builder\RouteBuilder as Route; +``` + +Builders are immutable and can be passed directly to `RouteCollectorInterface::addRoute()`. + +### `Route` changes + +`Yiisoft\Router\Route` is now a mutable route data object with a public constructor: + +```php +use Yiisoft\Http\Method; +use Yiisoft\Router\Route; + +$route = new Route( + methods: [Method::GET], + pattern: '/', + name: 'home', + action: HomeAction::class, +); +``` + +The fluent configuration methods moved to `RouteBuilder`. The static construction methods remain available on `Route` +as facades that return a `RouteBuilder`. + +`Route::getData()` was removed. Replace it with the corresponding explicit method: + +| Before | After | +|---|---| +| `getData('name')` | `getName()` | +| `getData('pattern')` | `getPattern()` | +| `getData('host')` | `getHosts()[0] ?? null` | +| `getData('hosts')` | `getHosts()` | +| `getData('methods')` | `getMethods()` | +| `getData('defaults')` | `getDefaults()` | +| `getData('override')` | `isOverride()` | +| `getData('hasMiddlewares')` | `getMiddlewares() !== [] || getAction() !== null` | +| `getData('enabledMiddlewares')` | `getEnabledMiddlewaresAndAction()` | + +The action is stored separately from route middleware. Use `getAction()` for the action, +`getEnabledMiddlewares()` for filtered middleware only, or `getEnabledMiddlewaresAndAction()` for the dispatch pipeline. +Mutable configuration is available through `setMethods()`, `setPattern()`, `setName()`, `setAction()`, +`setMiddlewares()`, `setDefaults()`, `setHosts()`, `setOverride()`, and `setDisabledMiddlewares()`. + +### `Group` changes + +`Yiisoft\Router\Group` is now a mutable group data object with a public constructor. The fluent configuration methods +moved to `GroupBuilder`. The static `create()` method remains available on `Group` as a facade that returns a +`GroupBuilder`. + +`Group::getData()` was removed. Replace it with the corresponding explicit method: + +| Before | After | +|---|---| +| `getData('prefix')` | `getPrefix()` | +| `getData('namePrefix')` | `getNamePrefix()` | +| `getData('host')` | `getHosts()[0] ?? null` | +| `getData('hosts')` | `getHosts()` | +| `getData('routes')` | `getRoutes()` | +| `getData('hasCorsMiddleware')` | `getCorsMiddleware() !== null` | +| `getData('corsMiddleware')` | `getCorsMiddleware()` | +| `getData('enabledMiddlewares')` | `getEnabledMiddlewares()` | + +Mutable configuration is available through `setPrefix()`, `setNamePrefix()`, `setRoutes()`, `setMiddlewares()`, +`setHosts()`, `setCorsMiddleware()`, and `setDisabledMiddlewares()`. + +### `RoutableInterface` + +`RoutableInterface` was added for custom route definitions. Its `toRoute()` method must return a `Route` or `Group`. +The route collector and groups accept routable instances in addition to route and group data objects. The route +collection clones the returned object before applying collection middleware or group transformations. + +### `RouteCollectorInterface` changes + +`RouteCollectorInterface::addRoute()` now also accepts `RoutableInterface` instances. + +If you call or implement `RouteCollectorInterface::getMiddlewareDefinitions()`, then rename the method to +`getMiddlewares()`: + +```php +// Before +$collector->getMiddlewareDefinitions(); + +// After +$collector->getMiddlewares(); +``` + +### `CurrentRoute` changes + +`CurrentRoute::getHost()` is deprecated but remains functional and returns the first route host. + +- If you only need the first route host, then no changes are required. +- If you need all route hosts, then use `CurrentRoute::getHosts()`: + +```php +// Before +$host = $currentRoute->getHost(); + +// After +$hosts = $currentRoute->getHosts(); +``` + ## 4.0.0 ### `Route`, `Group` and `MatchingResult` changes diff --git a/src/Builder/GroupBuilder.php b/src/Builder/GroupBuilder.php new file mode 100644 index 00000000..42619188 --- /dev/null +++ b/src/Builder/GroupBuilder.php @@ -0,0 +1,154 @@ + + */ + private array $middlewares = []; + + private array $disabledMiddlewares = []; + + /** + * @var string[] + */ + private array $hosts = []; + + /** + * @var array|callable|string|null Middleware definition for CORS requests. + */ + private $corsMiddleware = null; + + private function __construct( + private readonly ?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 + { + $new = clone $this; + $new->routes = $routes; + + 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 + { + $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), + ); + + 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; + array_push($new->hosts, ...$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 00000000..c4a89af7 --- /dev/null +++ b/src/Builder/RouteBuilder.php @@ -0,0 +1,215 @@ + + */ + private array $middlewares = []; + + /** + * @var array + */ + private array $defaults = []; + + /** + * @param string[] $methods + */ + private function __construct( + private readonly 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; + array_push($route->hosts, ...$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 d3eb9a21..938c7318 100644 --- a/src/CurrentRoute.php +++ b/src/CurrentRoute.php @@ -36,17 +36,29 @@ final class CurrentRoute */ public function getName(): ?string { - return $this->route?->getData('name'); + return $this->route?->getName(); } /** * Returns the current route host. * + * @deprecated Use {@see getHosts()} instead. + * * @return string|null The current route host. */ public function getHost(): ?string { - return $this->route?->getData('host'); + return $this->route?->getHosts()[0] ?? null; + } + + /** + * Returns the current route hosts. + * + * @return string[]|null The current route hosts. + */ + public function getHosts(): ?array + { + return $this->route?->getHosts(); } /** @@ -56,7 +68,7 @@ public function getHost(): ?string */ public function getPattern(): ?string { - return $this->route?->getData('pattern'); + return $this->route?->getPattern(); } /** @@ -66,7 +78,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 24714da2..9711ad54 100644 --- a/src/Debug/RouterCollector.php +++ b/src/Debug/RouterCollector.php @@ -53,10 +53,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, @@ -86,10 +86,10 @@ public function getSummary(): array return [ '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, @@ -128,9 +128,6 @@ private function getMiddlewaresAndAction(?Route $route): array return [[], null]; } - $middlewares = $route->getData('enabledMiddlewares'); - $action = array_pop($middlewares); - - return [$middlewares, $action]; + return [$route->getEnabledMiddlewares(), $route->getAction()]; } } diff --git a/src/Group.php b/src/Group.php index e23092b5..3f2335f3 100644 --- a/src/Group.php +++ b/src/Group.php @@ -5,14 +5,23 @@ namespace Yiisoft\Router; use InvalidArgumentException; +use Yiisoft\Router\Builder\GroupBuilder; use Yiisoft\Router\Internal\MiddlewareFilter; use function in_array; - +use function is_array; +use function is_callable; +use function is_string; + +/** + * Mutable data object that groups routes and applies common configuration to them. + * + * For immutable fluent group definitions, use {@see GroupBuilder}. + */ final class Group { /** - * @var Group[]|Route[] + * @var Group[]|RoutableInterface[]|Route[] */ private array $routes = []; @@ -26,8 +35,6 @@ final class Group * @var string[] */ private array $hosts = []; - private ?string $namePrefix = null; - private array $disabledMiddlewares = []; /** * @psalm-var list|null @@ -39,167 +46,176 @@ final class Group */ private $corsMiddleware = null; - private function __construct( + /** + * @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 + * a certain route. + */ + public function __construct( private ?string $prefix = null, - ) {} + private ?string $namePrefix = null, + array $routes = [], + array $middlewares = [], + array $hosts = [], + private array $disabledMiddlewares = [], + array|callable|string|null $corsMiddleware = null, + ) { + $this->setRoutes($routes); + $this->setMiddlewares($middlewares); + $this->setHosts($hosts); + $this->corsMiddleware = $corsMiddleware; + } /** * 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): self - { - return new self($prefix); - } - - public function routes(self|Route ...$routes): self + public static function create(?string $prefix = null, ?string $namePrefix = null): GroupBuilder { - $new = clone $this; - $new->routes = $routes; - - return $new; + return GroupBuilder::create($prefix, $namePrefix); } /** - * 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. + * @return Group[]|RoutableInterface[]|Route[] */ - public function withCors(array|callable|string|null $middlewareDefinition): self + public function getRoutes(): array { - $group = clone $this; - $group->corsMiddleware = $middlewareDefinition; - - return $group; + return $this->routes; } - /** - * 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 getMiddlewares(): array { - $new = clone $this; - array_push( - $new->middlewares, - ...array_values($definition), - ); - - $new->enabledMiddlewaresCache = null; + return $this->middlewares; + } - return $new; + public function getHosts(): array + { + return $this->hosts; } - /** - * 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 getCorsMiddleware(): callable|array|string|null { - $new = clone $this; - array_unshift( - $new->middlewares, - ...array_values($definition), - ); + return $this->corsMiddleware; + } - $new->enabledMiddlewaresCache = null; + public function getPrefix(): ?string + { + 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->hosts = []; foreach ($hosts as $host) { + if (!is_string($host)) { + throw new InvalidArgumentException('Invalid $hosts provided, list of string expected.'); + } $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) { /** @infection-ignore-all */ return $this->enabledMiddlewaresCache; } - $this->enabledMiddlewaresCache = MiddlewareFilter::filter($this->middlewares, $this->disabledMiddlewares); + return $this->enabledMiddlewaresCache = MiddlewareFilter::filter($this->middlewares, $this->disabledMiddlewares); + } + + /** + * @psalm-assert list $middlewares + */ + private function assertMiddlewares(array $middlewares): void + { + /** @var mixed $middleware */ + foreach ($middlewares as $middleware) { + if (is_string($middleware) || is_callable($middleware) || is_array($middleware)) { + continue; + } + + throw new InvalidArgumentException( + 'Invalid $middlewares provided, list of string or array or callable expected.', + ); + } + } + + /** + * @psalm-assert array $routes + */ + private function assertRoutes(array $routes): void + { + /** @var Group|RoutableInterface|Route $route */ + foreach ($routes as $route) { + if ($route instanceof Route || $route instanceof self || $route instanceof RoutableInterface) { + continue; + } - return $this->enabledMiddlewaresCache; + throw new InvalidArgumentException( + '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 e613d9c3..4a546ff9 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()->getEnabledMiddlewaresAndAction()) ->dispatch($request, $handler); } } diff --git a/src/RoutableInterface.php b/src/RoutableInterface.php new file mode 100644 index 00000000..2a7cfcb3 --- /dev/null +++ b/src/RoutableInterface.php @@ -0,0 +1,16 @@ + + */ + private array $methods = []; /** * @var string[] */ private array $hosts = []; - private bool $override = false; - private bool $actionAdded = false; + + /** + * @var array|callable|string|null + */ + private $action = null; /** * @var array[]|callable[]|string[] @@ -34,25 +44,42 @@ final class Route implements Stringable */ private array $middlewares = []; - private array $disabledMiddlewares = []; - /** * @psalm-var list|null */ private ?array $enabledMiddlewaresCache = null; /** - * @var array + * @var array */ private array $defaults = []; /** - * @param string[] $methods + * @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 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 + * a certain route. */ - private function __construct( - private array $methods, + public function __construct( + array $methods, private string $pattern, - ) {} + private ?string $name = null, + array|callable|string|null $action = null, + array $middlewares = [], + array $defaults = [], + array $hosts = [], + private bool $override = false, + private array $disabledMiddlewares = [], + ) { + $this->setMethods($methods); + $this->action = $action; + $this->setMiddlewares($middlewares); + $this->setHosts($hosts); + $this->setDefaults($defaults); + } public function __toString(): string { @@ -64,7 +91,7 @@ public function __toString(): string $result .= implode(',', $this->methods) . ' '; } - if ($this->hosts) { + if (!empty($this->hosts)) { $quoted = array_map(static fn($host) => preg_quote($host, '/'), $this->hosts); if (!preg_match('/' . implode('|', $quoted) . '/', $this->pattern)) { @@ -83,252 +110,247 @@ 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(), + 'enabledMiddlewaresAndAction' => $this->getEnabledMiddlewaresAndAction(), ]; } - public static function get(string $pattern): self + public static function get(string $pattern): RouteBuilder { - return self::methods([Method::GET], $pattern); + return RouteBuilder::get($pattern); } - public static function post(string $pattern): self + public static function post(string $pattern): RouteBuilder { - return self::methods([Method::POST], $pattern); + return RouteBuilder::post($pattern); } - public static function put(string $pattern): self + public static function put(string $pattern): RouteBuilder { - return self::methods([Method::PUT], $pattern); + return RouteBuilder::put($pattern); } - public static function delete(string $pattern): self + public static function delete(string $pattern): RouteBuilder { - return self::methods([Method::DELETE], $pattern); + return RouteBuilder::delete($pattern); } - public static function patch(string $pattern): self + public static function patch(string $pattern): RouteBuilder { - return self::methods([Method::PATCH], $pattern); + return RouteBuilder::patch($pattern); } - public static function head(string $pattern): self + public static function head(string $pattern): RouteBuilder { - return self::methods([Method::HEAD], $pattern); + return RouteBuilder::head($pattern); } - public static function options(string $pattern): self + public static function options(string $pattern): RouteBuilder { - return self::methods([Method::OPTIONS], $pattern); + return RouteBuilder::options($pattern); } /** * @param string[] $methods */ - public static function methods(array $methods, string $pattern): self + public static function methods(array $methods, string $pattern): RouteBuilder { - return new self($methods, $pattern); + return RouteBuilder::methods($methods, $pattern); } - public function name(string $name): self + /** + * @return string[] + */ + public function getMethods(): array { - $route = clone $this; - $route->name = $name; - return $route; + return $this->methods; } - public function pattern(string $pattern): self + public function getAction(): array|callable|string|null { - $new = clone $this; - $new->pattern = $pattern; - return $new; + return $this->action; } - public function host(string $host): self + public function getMiddlewares(): array { - return $this->hosts($host); + return $this->middlewares; } - public function hosts(string ...$hosts): self + /** + * @return string[] + */ + public function getHosts(): array { - $route = clone $this; - $route->hosts = []; + return $this->hosts; + } - foreach ($hosts as $host) { - $host = rtrim($host, '/'); + public function getDefaults(): array + { + return $this->defaults; + } - if ($host !== '' && !in_array($host, $route->hosts, true)) { - $route->hosts[] = $host; - } - } + public function getPattern(): string + { + return $this->pattern; + } - return $route; + public function getName(): string + { + return $this->name ?? (implode(', ', $this->methods) . ' ' . implode('|', $this->hosts) . $this->pattern); } - /** - * Marks route as override. When added it will replace existing route with the same name. - */ - public function override(): self + public function isOverride(): bool { - $route = clone $this; - $route->override = true; - return $route; + return $this->override; + } + + public function getDisabledMiddlewares(): array + { + return $this->disabledMiddlewares; } /** - * Parameter default values indexed by parameter names. + * Returns the dispatch pipeline: enabled middlewares with the action appended as the final handler. * - * @psalm-param array $defaults + * @return array[]|callable[]|string[] + * @psalm-return list */ - public function defaults(array $defaults): self + public function getEnabledMiddlewaresAndAction(): array { - $route = clone $this; - $route->defaults = array_map(strval(...), $defaults); - return $route; + $stack = $this->getEnabledMiddlewares(); + if ($this->action !== null) { + $stack[] = $this->action; + } + return $stack; } - /** - * Appends a handler middleware definition that should be invoked for a matched route. - * First added handler will be executed first. - * If no actions have been added, the middleware is added to the end of the list. Otherwise, it is added before the action. - */ - public function middleware(array|callable|string ...$definition): self + public function setMethods(array $methods): self { - $route = clone $this; - if ($this->actionAdded) { - /** - * @psalm-suppress PropertyTypeCoercion Keys in the replacement array are not preserved. - * @infection-ignore-all - */ - array_splice( - $route->middlewares, - offset: count($route->middlewares) - 1, - length: 0, - replacement: $definition, - ); - } else { - array_push( - $route->middlewares, - ...array_values($definition), - ); + if (empty($methods)) { + throw new InvalidArgumentException('$methods cannot be empty.'); } + $this->methods = []; + foreach ($methods as $method) { + if (!is_string($method)) { + throw new InvalidArgumentException('Invalid $methods provided, list of string expected.'); + } + $this->methods[] = $method; + } + return $this; + } - $route->enabledMiddlewaresCache = null; + public function setHosts(array $hosts): self + { + $this->hosts = []; + foreach ($hosts as $host) { + if (!is_string($host)) { + throw new InvalidArgumentException('Invalid $hosts provided, list of string expected.'); + } + $host = rtrim($host, '/'); - return $route; + if ($host !== '' && !in_array($host, $this->hosts, true)) { + $this->hosts[] = $host; + } + } + + return $this; } - /** - * Prepends a handler middleware definition that should be invoked for a matched route. Last added handlers will be - * executed first. - * - * Passed definitions will be added to beginning. For example: - * - * ```php - * // Resulting middleware stack order: Middleware1, Middleware2, Middleware3 - * Route::get('/') - * ->middleware(Middleware3::class) - * ->prependMiddleware(Middleware1::class, Middleware2::class) - * ``` - */ - public function prependMiddleware(array|callable|string ...$definition): self + public function setAction(callable|array|string|null $action): self { - $route = clone $this; - array_unshift( - $route->middlewares, - ...array_values($definition), - ); - - $route->enabledMiddlewaresCache = null; + $this->action = $action; + return $this; + } - return $route; + public function setMiddlewares(array $middlewares): self + { + $this->assertMiddlewares($middlewares); + $this->middlewares = $middlewares; + $this->enabledMiddlewaresCache = null; + 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 setDefaults(array $defaults): self { - $route = clone $this; - $route->middlewares[] = $middlewareDefinition; - $route->actionAdded = true; - return $route; + $this->defaults = []; + /** @var mixed $value */ + foreach ($defaults as $key => $value) { + if (!is_scalar($value) && !($value instanceof Stringable) && null !== $value) { + throw new InvalidArgumentException( + 'Invalid $defaults provided, array of scalar, `Stringable`, or null values expected.', + ); + } + $this->defaults[$key] = (string) $value; + } + 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 setPattern(string $pattern): self { - $route = clone $this; - array_push( - $route->disabledMiddlewares, - ...array_values($definition), - ); + $this->pattern = $pattern; + return $this; + } - $route->enabledMiddlewaresCache = null; + public function setName(?string $name): self + { + $this->name = $name; + return $this; + } - return $route; + public function setOverride(bool $override): self + { + $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; } /** * @return array[]|callable[]|string[] * @psalm-return list */ - private function getEnabledMiddlewares(): array + public function getEnabledMiddlewares(): array { if ($this->enabledMiddlewaresCache !== null) { - /** @infection-ignore-all */ + /** @infection-ignore-all Cached and freshly filtered values are indistinguishable by behavior. */ return $this->enabledMiddlewaresCache; } - $this->enabledMiddlewaresCache = MiddlewareFilter::filter($this->middlewares, $this->disabledMiddlewares); + return $this->enabledMiddlewaresCache = MiddlewareFilter::filter( + $this->middlewares, + $this->disabledMiddlewares, + ); + } - return $this->enabledMiddlewaresCache; + /** + * @psalm-assert list $middlewares + */ + private function assertMiddlewares(array $middlewares): void + { + /** @var mixed $middleware */ + foreach ($middlewares as $middleware) { + if (is_string($middleware)) { + continue; + } + + if (is_callable($middleware) || is_array($middleware)) { + continue; + } + + throw new InvalidArgumentException( + 'Invalid $middlewares provided, list of string or array or callable expected.', + ); + } } } diff --git a/src/RouteCollection.php b/src/RouteCollection.php index ac456b0b..fb07f2af 100644 --- a/src/RouteCollection.php +++ b/src/RouteCollection.php @@ -63,13 +63,14 @@ private function ensureItemsInjected(): void /** * Build routes array. * - * @param Group[]|Route[] $items + * @param Group[]|RoutableInterface[]|Route[] $items */ private function injectItems(array $items): void { foreach ($items as $item) { + $item = clone ($item instanceof RoutableInterface ? $item->toRoute() : $item); if (!$this->isStaticRoute($item)) { - $item = $item->prependMiddleware(...$this->collector->getMiddlewareDefinitions()); + $item->setMiddlewares(array_merge($this->collector->getMiddlewares(), $item->getMiddlewares())); } $this->injectItem($item); } @@ -85,9 +86,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; @@ -100,57 +101,59 @@ 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) { + $item = clone ($item instanceof RoutableInterface ? $item->toRoute() : $item); if (!$this->isStaticRoute($item)) { - $item = $item->prependMiddleware(...$group->getData('enabledMiddlewares')); + $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; } } @@ -161,30 +164,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())); } /** @@ -206,8 +210,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 4111b599..a6f41197 100644 --- a/src/RouteCollector.php +++ b/src/RouteCollector.php @@ -7,16 +7,16 @@ final class RouteCollector implements RouteCollectorInterface { /** - * @var Group[]|Route[] + * @var Group[]|RoutableInterface[]|Route[] */ 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 38c5861c..711c83d6 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[]|RoutableInterface[]|Route[] */ 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 00000000..5d89d354 --- /dev/null +++ b/tests/Builder/GroupBuilderTest.php @@ -0,0 +1,440 @@ + 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 + { + $middleware1 = static fn() => new Response(); + + $group = Group::create() + ->prependMiddleware($middleware1) + ->routes(Route::get('/')); + + $this->assertSame([$middleware1], $group->toRoute()->getEnabledMiddlewares()); + } + + 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->getEnabledMiddlewaresAndAction()) + ->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->getEnabledMiddlewaresAndAction()) + ->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->getEnabledMiddlewaresAndAction()) + ->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(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 + { + $corsMiddleware = static fn() => new Response(204); + $group = Group::create() + ->routes( + Route::get('/info') + ->middleware(TestMiddleware1::class) + ->action(static fn() => 'info'), + Route::post('/info')->action(static fn() => 'info'), + ) + ->withCors($corsMiddleware); + + $collector = new RouteCollector(); + $collector->addRoute($group); + $routeCollection = new RouteCollection($collector); + + $this->assertCount(3, $routeCollection->getRoutes()); + $this->assertSame( + [$corsMiddleware, TestMiddleware1::class], + $routeCollection->getRoute('GET /info')->getEnabledMiddlewares(), + ); + } + + 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 + { + $corsMiddleware = static fn() => new Response(204); + $nestedCorsMiddleware = static fn() => new Response(201); + $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($nestedCorsMiddleware), + )->withCors($corsMiddleware); + + $collector = new RouteCollector(); + $collector->addRoute($group); + + $routeCollection = new RouteCollection($collector); + $this->assertCount(7, $routeCollection->getRoutes()); + $optionsRoute = $routeCollection->getRoute('OPTIONS /v1/post'); + $this->assertInstanceOf(Route::class, $optionsRoute); + $this->assertSame([$corsMiddleware], $optionsRoute->getEnabledMiddlewares()); + } + + 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 + { + $middleware = static fn() => new Response(); + $group = Group::create() + ->routes(Route::get('/info')->action(static fn() => 'info')) + ->middleware($middleware); + + $this->assertSame([$middleware], $group->toRoute()->getEnabledMiddlewares()); + } + + public function testDuplicateHosts(): void + { + $route = Group::create()->host('a.com')->hosts('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 00000000..a4ef3bf4 --- /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('/') + ->host('https://yiiframework.com/') + ->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 static 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()->getEnabledMiddlewaresAndAction()) + ->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()->getEnabledMiddlewaresAndAction()); + + $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()->getEnabledMiddlewaresAndAction()) + ->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()->getEnabledMiddlewaresAndAction(); + + $route = $route->prependMiddleware(TestMiddleware1::class, TestMiddleware2::class); + + $this->assertSame( + [TestMiddleware2::class, TestMiddleware3::class, [TestController::class, 'index']], + $route->toRoute()->getEnabledMiddlewaresAndAction(), + ); + } + + 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()->getEnabledMiddlewaresAndAction(), + ); + } + + 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 c25c0f16..1946d7f4 100644 --- a/tests/ConfigTest.php +++ b/tests/ConfigTest.php @@ -34,7 +34,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 73ba9875..3f3d33fb 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; @@ -18,6 +19,7 @@ public function testGettersReturnDefaultValuesWhenRouteIsNotSet(): void $this->assertNull($currentRoute->getName()); $this->assertNull($currentRoute->getHost()); + $this->assertNull($currentRoute->getHosts()); $this->assertNull($currentRoute->getPattern()); $this->assertNull($currentRoute->getMethods()); $this->assertNull($currentRoute->getUri()); @@ -28,38 +30,39 @@ public function testGettersReturnDefaultValuesWhenRouteIsNotSet(): void 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()[0], $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 @@ -78,7 +81,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()); } @@ -90,7 +93,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')); } @@ -98,7 +101,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')); } @@ -106,7 +109,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')); } @@ -117,8 +120,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 @@ -137,7 +140,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 751f61fe..4eaac3a3 100644 --- a/tests/Debug/RouterCollectorTest.php +++ b/tests/Debug/RouterCollectorTest.php @@ -7,6 +7,7 @@ use PHPUnit\Framework\MockObject\MockObject; use Yiisoft\Di\Container; use Yiisoft\Di\ContainerConfig; +use Yiisoft\Http\Method; use Yiisoft\Router\CurrentRoute; use Yiisoft\Router\Debug\RouterCollector; use Yiisoft\Router\Group; @@ -44,7 +45,7 @@ public function testWithoutCurrentRoute(): void public function testWithoutRouteCollection(): void { - $route = Route::get('/'); + $route = new Route([Method::GET], '/'); $arguments = ['a' => 19]; $result = MatchingResult::fromSuccess($route, $arguments); @@ -61,7 +62,7 @@ public function testWithoutRouteCollection(): void $this->assertSame(['currentRoute'], array_keys($collected)); $this->assertSame( - ['matchTime', 'name', 'pattern', 'arguments', 'host', 'uri', 'action', 'middlewares'], + ['matchTime', 'name', 'pattern', 'arguments', 'hosts', 'uri', 'action', 'middlewares'], array_keys($collected['currentRoute']), ); } @@ -131,7 +132,7 @@ protected function checkCollectedData(array $data): void private function createRoutes(): array { return [ - Route::get('/'), + new Route([Method::GET], '/'), Group::create('/api')->routes(Route::get('/v1')), ]; } diff --git a/tests/Debug/UrlMatcherInterfaceProxyTest.php b/tests/Debug/UrlMatcherInterfaceProxyTest.php index 280a092f..72373d53 100644 --- a/tests/Debug/UrlMatcherInterfaceProxyTest.php +++ b/tests/Debug/UrlMatcherInterfaceProxyTest.php @@ -6,6 +6,7 @@ use Nyholm\Psr7\ServerRequest; use PHPUnit\Framework\TestCase; +use Yiisoft\Http\Method; use ReflectionException; use ReflectionProperty; use Yiisoft\Router\CurrentRoute; @@ -42,7 +43,7 @@ public function testConstructor(): void public function testBase(): void { $request = new ServerRequest('GET', '/'); - $route = Route::get('/'); + $route = new Route([Method::GET], '/'); $arguments = ['a' => '19']; $result = MatchingResult::fromSuccess($route, $arguments); diff --git a/tests/GroupTest.php b/tests/GroupTest.php index 016b02ab..277f34ac 100644 --- a/tests/GroupTest.php +++ b/tests/GroupTest.php @@ -6,489 +6,123 @@ 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 Yiisoft\Middleware\Dispatcher\MiddlewareDispatcher; -use Yiisoft\Middleware\Dispatcher\MiddlewareFactory; +use Yiisoft\Http\Method; 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; -use Yiisoft\Router\Tests\Support\TestController; +use stdClass; 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 = $group->prependMiddleware(TestMiddleware1::class, TestMiddleware2::class); + $group = (new Group()) + ->setMiddlewares([TestMiddleware1::class, TestMiddleware2::class, TestMiddleware3::class]) + ->setDisabledMiddlewares([TestMiddleware1::class, TestMiddleware3::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'), - ); - } - - 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'), + $group->getEnabledMiddlewares(), ); } - 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() - ->prependMiddleware(TestMiddleware1::class) - ->routes(Route::get('/')); - - $this->assertSame([TestMiddleware1::class], $group->getData('enabledMiddlewares')); - } - - 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 testAddGroup(): void + public function testInvalidMiddlewares(): 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->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid $middlewares provided, list of string or array or callable expected.'); - $this->assertSame('https://yiiframework.com', $group->getData('host')); + $middleware = static fn() => new Response(); + $group = new Group('/api', middlewares: [$middleware, new stdClass()]); } 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 testName(): void + public function testInvalidHosts(): 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'); + $this->expectExceptionMessage('Invalid $hosts provided, list of string expected.'); - $group->getData('wrong'); + $group = new Group(hosts: ['https://yiiframework.com/', 123]); } - public function testWithCors(): void + public function testPrefix(): 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); + $group = (new Group())->setPrefix('/api'); - $this->assertCount(3, $routeCollection->getRoutes()); + $this->assertSame('/api', $group->getPrefix()); } - 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(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 + public function testName(): void { - $group = Group::create() - ->routes(Route::get('/info') - ->middleware(TestMiddleware3::class) - ->action([TestController::class, 'index'])) - ->middleware(TestMiddleware1::class, TestMiddleware2::class); + $group = (new Group())->setNamePrefix('api'); - $collector = new RouteCollector(); - $collector->addRoute($group); - $routeCollection = new RouteCollection($collector); - - $this->assertSame( - [TestMiddleware1::class, TestMiddleware2::class], - $group->getData('enabledMiddlewares'), - ); - $this->assertSame( - [TestMiddleware1::class, TestMiddleware2::class, TestMiddleware3::class, [TestController::class, 'index']], - $routeCollection->getRoute('GET /info')->getData('enabledMiddlewares'), - ); + $this->assertSame('api', $group->getNamePrefix()); } - public function testDuplicateHosts(): void + public function testCors(): void { - $route = Group::create()->hosts('a.com', 'b.com', 'a.com'); + $group = (new Group())->setCorsMiddleware($cors = static fn() => new Response()); - $this->assertSame(['a.com', 'b.com'], $route->getData('hosts')); + $this->assertSame($cors, $group->getCorsMiddleware()); } - public function testImmutability(): void + public function testRoutes(): void { - $group = Group::create(); + $group = (new Group())->setRoutes($routes = [new Route([Method::GET], '')]); - $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($routes, $group->getRoutes()); } - private function getRequestHandler(): RequestHandlerInterface + public function testInvalidRoutes(): void { - return new class implements RequestHandlerInterface { - public function handle(ServerRequestInterface $request): ResponseInterface - { - return new Response(404); - } - }; - } + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid $routes provided, array of `Route` or `Group` or `RoutableInterface` instance expected.'); - private function getDispatcher(): MiddlewareDispatcher - { - $container = new Container([]); - return new MiddlewareDispatcher( - new MiddlewareFactory($container), - $this->createMock(EventDispatcherInterface::class), - ); + $group = (new Group())->setRoutes([new Route([Method::GET], ''), new stdClass()]); } } diff --git a/tests/HydratorAttribute/RouteArgumentTest.php b/tests/HydratorAttribute/RouteArgumentTest.php index a1b0078a..656050bb 100644 --- a/tests/HydratorAttribute/RouteArgumentTest.php +++ b/tests/HydratorAttribute/RouteArgumentTest.php @@ -80,7 +80,7 @@ public function testUnexpectedAttributeException(): void private function createHydrator(array $arguments): Hydrator { $currentRoute = new CurrentRoute(); - $currentRoute->setRouteWithArguments(RouterRoute::get('/'), $arguments); + $currentRoute->setRouteWithArguments(RouterRoute::get('/')->toRoute(), $arguments); return new Hydrator( attributeResolverFactory: new ContainerAttributeResolverFactory( diff --git a/tests/MatchingResultTest.php b/tests/MatchingResultTest.php index 10d67b87..e3cb650e 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 efa5a16e..390c7d42 100644 --- a/tests/Middleware/RouterTest.php +++ b/tests/Middleware/RouterTest.php @@ -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 1715c217..1109ca35 100644 --- a/tests/RouteCollectionTest.php +++ b/tests/RouteCollectionTest.php @@ -16,11 +16,13 @@ use RuntimeException; use Yiisoft\Middleware\Dispatcher\MiddlewareDispatcher; use Yiisoft\Middleware\Dispatcher\MiddlewareFactory; +use Yiisoft\Http\Method; use Yiisoft\Router\Group; use Yiisoft\Router\Route; use Yiisoft\Router\RouteCollection; use Yiisoft\Router\RouteCollector; use Yiisoft\Router\RouteNotFoundException; +use Yiisoft\Router\RoutableInterface; use Yiisoft\Router\Tests\Support\TestController; use Yiisoft\Router\Tests\Support\TestMiddleware1; use Yiisoft\Router\Tests\Support\TestMiddleware2; @@ -90,7 +92,85 @@ 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 testCollectorCanBeReusedWithRawRouteAndGroupInstances(): void + { + $route = new Route([Method::GET], '/users', 'users'); + $group = new Group( + prefix: '/api', + namePrefix: 'api/', + routes: [new Route([Method::GET], '/posts', 'posts')], + ); + + $collector = new RouteCollector(); + $collector->middleware(static fn() => new Response()); + $collector->addRoute($group, $route); + + $first = new RouteCollection($collector); + $second = new RouteCollection($collector); + + $this->assertSame('/api/posts', $first->getRoute('api/posts')->getPattern()); + $this->assertSame('/users', $first->getRoute('users')->getPattern()); + $this->assertSame('/api/posts', $second->getRoute('api/posts')->getPattern()); + $this->assertSame('/users', $second->getRoute('users')->getPattern()); + + $this->assertSame('/posts', $group->getRoutes()[0]->getPattern()); + $this->assertSame('/users', $route->getPattern()); + } + + public function testCollectorCanBeReusedWithRetainedRoutesFromRoutables(): void + { + $route = new Route([Method::GET], '/users', 'users'); + $routeRoutable = new class ($route) implements RoutableInterface { + public function __construct(private readonly Route $route) {} + + public function toRoute(): Route + { + return $this->route; + } + }; + $nestedRoute = new Route([Method::GET], '/posts', 'posts'); + $nestedRouteRoutable = new class ($nestedRoute) implements RoutableInterface { + public function __construct(private readonly Route $route) {} + + public function toRoute(): Route + { + return $this->route; + } + }; + $group = new Group( + prefix: '/api', + namePrefix: 'api/', + routes: [$nestedRouteRoutable], + ); + $groupRoutable = new class ($group) implements RoutableInterface { + public function __construct(private readonly Group $group) {} + + public function toRoute(): Group + { + return $this->group; + } + }; + + $collector = new RouteCollector(); + $collector->middleware(static fn() => new Response()); + $collector->addRoute($groupRoutable, $routeRoutable); + + $first = new RouteCollection($collector); + $second = new RouteCollection($collector); + + $this->assertSame('/api/posts', $first->getRoute('api/posts')->getPattern()); + $this->assertSame('/users', $first->getRoute('users')->getPattern()); + $this->assertSame('/api/posts', $second->getRoute('api/posts')->getPattern()); + $this->assertSame('/users', $second->getRoute('users')->getPattern()); + + $this->assertSame('/posts', $nestedRoute->getPattern()); + $this->assertSame([], $nestedRoute->getMiddlewares()); + $this->assertSame('/users', $route->getPattern()); + $this->assertSame([], $route->getMiddlewares()); + $this->assertSame([], $group->getMiddlewares()); } public function testRouteWithoutAction(): void @@ -109,7 +189,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 @@ -180,9 +260,9 @@ public function testGetRouteTreeReturnsRouteInstances(): void $routeTree = (new RouteCollection($collector))->getRouteTree(false); $this->assertInstanceOf(Route::class, $routeTree[0]); - $this->assertSame('/api/posts', $routeTree[0]->getData('name')); + $this->assertSame('/api/posts', $routeTree[0]->getName()); $this->assertInstanceOf(Route::class, $routeTree['/v1'][0]); - $this->assertSame('/api/comments', $routeTree['/v1'][0]->getData('name')); + $this->assertSame('/api/comments', $routeTree['/v1'][0]->getName()); } public function testGetRoutes(): void @@ -229,10 +309,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 @@ -299,10 +379,10 @@ public function testCollectorMiddlewareFullstackCalled(): void $route2 = $routeCollection->getRoute('view'); $request = new ServerRequest('GET', '/'); $response1 = $this->getDispatcher() - ->withMiddlewares($route1->getData('enabledMiddlewares')) + ->withMiddlewares($route1->getEnabledMiddlewaresAndAction()) ->dispatch($request, $this->getRequestHandler()); $response2 = $this->getDispatcher() - ->withMiddlewares($route2->getData('enabledMiddlewares')) + ->withMiddlewares($route2->getEnabledMiddlewaresAndAction()) ->dispatch($request, $this->getRequestHandler()); $this->assertEquals('middleware1', $response1->getReasonPhrase()); @@ -341,7 +421,7 @@ public function testMiddlewaresOrder(bool $groupWrapped): void TestController::class => new TestController(), ]), ) - ->withMiddlewares($route->getData('enabledMiddlewares')); + ->withMiddlewares($route->getEnabledMiddlewaresAndAction()); $response = $dispatcher->dispatch($request, $this->getRequestHandler()); $this->assertSame(200, $response->getStatusCode()); @@ -367,7 +447,7 @@ public function testStaticRouteWithCollectorMiddlewares(): void TestMiddleware1::class => new TestMiddleware1(), ]), ) - ->withMiddlewares($route->getData('enabledMiddlewares')); + ->withMiddlewares($route->getEnabledMiddlewaresAndAction()); $this->expectException(RuntimeException::class); $this->expectExceptionMessage('Stack is empty.'); diff --git a/tests/RouteCollectorTest.php b/tests/RouteCollectorTest.php index e51dc34d..275f30dd 100644 --- a/tests/RouteCollectorTest.php +++ b/tests/RouteCollectorTest.php @@ -9,6 +9,7 @@ use Yiisoft\Router\Group; use Yiisoft\Router\Route; use Yiisoft\Router\RouteCollector; +use Yiisoft\Router\RoutableInterface; final class RouteCollectorTest extends TestCase { @@ -56,7 +57,7 @@ public function testAddGroup(): void $collector->addRoute($rootGroup, $postGroup, test: $testGroup); $this->assertCount(3, $collector->getItems()); - $this->assertContainsOnlyInstancesOf(Group::class, $collector->getItems()); + $this->assertContainsOnlyInstancesOf(RoutableInterface::class, $collector->getItems()); } public function testAddMiddleware(): void @@ -73,12 +74,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 +92,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 44d1d744..c5c4f909 100644 --- a/tests/RouteTest.php +++ b/tests/RouteTest.php @@ -4,24 +4,14 @@ namespace Yiisoft\Router\Tests; -use Nyholm\Psr7\Response; -use Nyholm\Psr7\ServerRequest; use PHPUnit\Framework\Attributes\DataProvider; 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 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; use InvalidArgumentException; @@ -29,116 +19,104 @@ final class RouteTest extends TestCase { use AssertTrait; - public function testName(): void - { - $route = Route::get('/')->name('test.route'); - - $this->assertSame('test.route', $route->getData('name')); - } - - public function testNameDefault(): void + public function testSimpleInstance(): void { - $route = Route::get('/'); + $route = new Route( + methods: [Method::GET], + pattern: '/', + action: [TestController::class, 'index'], + middlewares: [TestMiddleware1::class], + override: true, + ); - $this->assertSame('GET /', $route->getData('name')); + $this->assertInstanceOf(Route::class, $route); + $this->assertCount(1, $route->getEnabledMiddlewares()); + $this->assertCount(2, $route->getEnabledMiddlewaresAndAction()); + $this->assertTrue($route->isOverride()); } - public function testNameDefaultWithHosts(): void + public function testDisabledMiddlewares(): void { - $route = Route::get('/')->hosts('a.com', 'b.com'); + $route = new Route( + methods: [Method::GET], + pattern: '/', + action: [TestController::class, 'index'], + middlewares: [TestMiddleware1::class], + override: true, + ); + $route->setDisabledMiddlewares([TestMiddleware2::class]); - $this->assertSame('GET a.com|b.com/', $route->getData('name')); + $this->assertCount(1, $route->getDisabledMiddlewares()); + $this->assertSame(TestMiddleware2::class, $route->getDisabledMiddlewares()[0]); } - public function testMethods(): void + public function testEnabledMiddlewares(): void { - $route = Route::methods([Method::POST, Method::HEAD], '/'); + $route = new Route( + methods: [Method::GET], + pattern: '/', + middlewares: [TestMiddleware1::class, TestMiddleware2::class], + override: true, + ); + $route->setDisabledMiddlewares([TestMiddleware2::class]); - $this->assertSame([Method::POST, Method::HEAD], $route->getData('methods')); + $this->assertCount(1, $route->getEnabledMiddlewares()); + $this->assertSame(TestMiddleware1::class, $route->getEnabledMiddlewares()[0]); } - public function testGetDataWithWrongKey(): void + public function testEmptyMethods(): void { - $route = Route::get(''); - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Unknown data key: wrong'); + $this->expectExceptionMessage('$methods cannot be empty.'); - $route->getData('wrong'); + new Route([], ''); } - 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 + public function testName(): void { - $route = Route::delete('/'); + $route = (new Route([Method::GET], '/'))->setName('test.route'); - $this->assertSame([Method::DELETE], $route->getData('methods')); + $this->assertSame('test.route', $route->getName()); } - public function testPatchMethod(): void + public function testNameDefault(): void { - $route = Route::patch('/'); + $route = new Route([Method::GET], '/'); - $this->assertSame([Method::PATCH], $route->getData('methods')); + $this->assertSame('GET /', $route->getName()); } - public function testHeadMethod(): void + public function testNameDefaultWithHosts(): void { - $route = Route::head('/'); + $route = (new Route([Method::GET], '/'))->setHosts(['a.com', 'b.com']); - $this->assertSame([Method::HEAD], $route->getData('methods')); + $this->assertSame('GET a.com|b.com/', $route->getName()); } - public function testOptionsMethod(): void + public function testMethods(): void { - $route = Route::options('/'); + $route = new Route([Method::GET], '/'); + $route->setMethods([Method::POST, Method::HEAD]); - $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'); - - $this->assertSame('/test2', $route->getData('pattern')); - } - - public function testHost(): void - { - $route = Route::get('/')->host('https://yiiframework.com/'); + $route = (new Route([Method::GET], '/test'))->setPattern('/test2'); - $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', - ); + ]); $this->assertSame( [ @@ -147,27 +125,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, ]); @@ -175,14 +139,17 @@ 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], '/'); + + $this->assertFalse($route->isOverride()); - $this->assertTrue($route->getData('override')); + $route->setOverride(true); + $this->assertTrue($route->isOverride()); } public static function dataToString(): array @@ -196,199 +163,62 @@ public static function dataToString(): array #[DataProvider('dataToString')] 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); } public function testToStringSimple(): void { - $route = Route::get('/'); + $route = new Route([Method::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 testPrependMiddlewareBeforeAction(): void - { - $route = Route::get('/') - ->prependMiddleware(TestMiddleware1::class) - ->action([TestController::class, 'index']); - - $this->assertSame( - [TestMiddleware1::class, [TestController::class, 'index']], - $route->getData('enabledMiddlewares'), - ); - } - - public function testMiddlewareAfterAction(): void - { - $route = Route::get('/'); - $route = $route->middleware(TestMiddleware1::class) - ->action([TestController::class, 'index']) - ->middleware(TestMiddleware2::class) - ->middleware(TestMiddleware3::class); - - $this->assertSame( - [TestMiddleware1::class, TestMiddleware2::class, TestMiddleware3::class, [TestController::class, 'index']], - $route->getData('enabledMiddlewares'), - ); - } - - 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 + public static function invalidMiddlewaresProvider(): array { - $route = Route::get('/') - ->middleware(TestMiddleware3::class) - ->disableMiddleware(TestMiddleware1::class) - ->action([TestController::class, 'index']); + $invalidMiddleware = (object) ['test' => 1]; - $route->getData('enabledMiddlewares'); - - $route = $route->prependMiddleware(TestMiddleware1::class, TestMiddleware2::class); - - $this->assertSame( - [TestMiddleware2::class, TestMiddleware3::class, [TestController::class, 'index']], - $route->getData('enabledMiddlewares'), - ); + return [ + 'after string' => [[TestMiddleware1::class, $invalidMiddleware]], + 'after callable' => [[static fn() => null, $invalidMiddleware]], + ]; } - public function testAddMiddlewareAfterGetEnabledMiddlewares(): void + #[DataProvider('invalidMiddlewaresProvider')] + public function testInvalidMiddlewares(array $middlewares): void { - $route = Route::get('/') - ->middleware(TestMiddleware3::class); - - $route->getData('enabledMiddlewares'); - - $route = $route->middleware(TestMiddleware1::class, TestMiddleware2::class); + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid $middlewares provided, list of string or array or callable expected.'); - $this->assertSame( - [TestMiddleware3::class, TestMiddleware1::class, TestMiddleware2::class], - $route->getData('enabledMiddlewares'), - ); + new Route([Method::GET], '/', middlewares: $middlewares); } - public function testDisableMiddlewareAfterGetEnabledMiddlewares(): void + public function testInvalidDefaults(): void { - $route = Route::get('/') - ->middleware(TestMiddleware1::class, TestMiddleware2::class, TestMiddleware3::class); - - $route->getData('enabledMiddlewares'); - - $route = $route->disableMiddleware(TestMiddleware1::class, TestMiddleware2::class); - - $this->assertSame( - [TestMiddleware3::class], - $route->getData('enabledMiddlewares'), + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage( + 'Invalid $defaults provided, array of scalar, `Stringable`, or null values expected.', ); - } - - 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'), - ); + new Route([Method::GET], '/', defaults: ['test' => 1, 'foo' => ['bar']]); } 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 @@ -411,13 +242,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 @@ -426,6 +255,12 @@ public function testDebugInfo(): void ) [enabledMiddlewares] => Array + ( + [0] => Yiisoft\Router\Tests\Support\TestMiddleware3 + [1] => Yiisoft\Router\Tests\Support\TestMiddleware1 + ) + + [enabledMiddlewaresAndAction] => Array ( [0] => Yiisoft\Router\Tests\Support\TestMiddleware3 [1] => Yiisoft\Router\Tests\Support\TestMiddleware1 @@ -440,55 +275,24 @@ 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 testImmutability(): void + public function testInvalidHosts(): 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('')); - } + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid $hosts provided, list of string expected.'); - private function getRequestHandler(): RequestHandlerInterface - { - return new class implements RequestHandlerInterface { - public function handle(ServerRequestInterface $request): ResponseInterface - { - return new Response(404); - } - }; + $route = new Route([Method::GET], '/', hosts: ['b.com', 123]); } - private function getDispatcher(?ContainerInterface $container = null): MiddlewareDispatcher + public function testInvalidMethods(): void { - if ($container === null) { - return new MiddlewareDispatcher( - new MiddlewareFactory($this->getContainer()), - $this->createMock(EventDispatcherInterface::class), - ); - } - - return new MiddlewareDispatcher( - new MiddlewareFactory($container), - $this->createMock(EventDispatcherInterface::class), - ); - } + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid $methods provided, list of string expected.'); - private function getContainer(array $instances = []): ContainerInterface - { - return new Container($instances); + $route = new Route([1], '/'); } }