From 02971c6683242afdf22f4363c85b64b0c7ec8d1b Mon Sep 17 00:00:00 2001 From: Rustam Date: Mon, 6 Nov 2023 09:27:26 +0500 Subject: [PATCH] Allow to create route instance directly --- src/Group.php | 50 +++++++++++++++++++++++++--- src/Route.php | 80 +++++++++++++++++++++++++++++++++++++++------ tests/GroupTest.php | 17 ++++++++++ tests/RouteTest.php | 39 ++++++++++++++++++++++ 4 files changed, 172 insertions(+), 14 deletions(-) diff --git a/src/Group.php b/src/Group.php index 0099cb1..e3c414d 100644 --- a/src/Group.php +++ b/src/Group.php @@ -27,10 +27,8 @@ final class Group * @var string[] */ private array $hosts = []; - private ?string $namePrefix = null; private bool $routesAdded = false; private bool $middlewareAdded = false; - private array $disabledMiddlewares = []; /** * @psalm-var list|null @@ -42,9 +40,24 @@ final class Group */ private $corsMiddleware = null; - private function __construct( - private ?string $prefix = null + /** + * @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, + array $middlewares = [], + array $hosts = [], + private ?string $namePrefix = null, + private array $disabledMiddlewares = [], + array|callable|string|null $corsMiddleware = null ) { + $this->assertMiddlewares($middlewares); + $this->assertHosts($hosts); + $this->middlewares = $middlewares; + $this->hosts = $hosts; + $this->corsMiddleware = $corsMiddleware; } /** @@ -215,4 +228,33 @@ private function getEnabledMiddlewares(): array return $this->enabledMiddlewaresCache; } + + /** + * @psalm-assert array $hosts + */ + private function assertHosts(array $hosts): void + { + foreach ($hosts as $host) { + if (!is_string($host)) { + throw new \InvalidArgumentException('Invalid $hosts provided, list of string expected.'); + } + } + } + + /** + * @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.' + ); + } + } } diff --git a/src/Route.php b/src/Route.php index d594070..27fc042 100644 --- a/src/Route.php +++ b/src/Route.php @@ -17,13 +17,15 @@ */ final class Route implements Stringable { - private ?string $name = null; + /** + * @var string[] + */ + private array $methods = []; /** * @var string[] */ private array $hosts = []; - private bool $override = false; private bool $actionAdded = false; /** @@ -32,25 +34,50 @@ 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 $action = null, + array $middlewares = [], + array $defaults = [], + array $hosts = [], + private bool $override = false, + private array $disabledMiddlewares = [], ) { + if (empty($methods)) { + throw new InvalidArgumentException('$methods cannot be empty.'); + } + $this->assertListOfStrings($methods, 'methods'); + $this->assertMiddlewares($middlewares); + $this->assertListOfStrings($hosts, 'hosts'); + $this->methods = $methods; + $this->middlewares = $middlewares; + $this->hosts = $hosts; + $this->defaults = array_map('\strval', $defaults); + if (!empty($action)) { + $this->middlewares[] = $action; + $this->actionAdded = true; + } } public static function get(string $pattern): self @@ -93,7 +120,7 @@ public static function options(string $pattern): self */ public static function methods(array $methods, string $pattern): self { - return new self($methods, $pattern); + return new self(methods: $methods, pattern: $pattern); } public function name(string $name): self @@ -271,7 +298,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)) { @@ -314,4 +341,37 @@ private function getEnabledMiddlewares(): array return $this->enabledMiddlewaresCache; } + + /** + * @psalm-assert array $items + */ + private function assertListOfStrings(array $items, string $argument): void + { + foreach ($items as $item) { + if (!is_string($item)) { + throw new \InvalidArgumentException('Invalid $' . $argument . ' provided, list of string expected.'); + } + } + } + + /** + * @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/tests/GroupTest.php b/tests/GroupTest.php index c74eaf9..c95b7cf 100644 --- a/tests/GroupTest.php +++ b/tests/GroupTest.php @@ -243,6 +243,15 @@ public function testGroupMiddlewareStackInterrupted(): void $this->assertSame(403, $response->getStatusCode()); } + public function testInvalidMiddlewares(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid $middlewares provided, list of string or array or callable expected.'); + + $middleware = static fn () => new Response(); + $group = new Group('/api', [$middleware, new \stdClass()]); + } + public function testAddGroup(): void { $logoutRoute = Route::post('/logout'); @@ -304,6 +313,14 @@ public function testHosts(): void $this->assertSame(['https://yiiframework.com', 'https://yiiframework.ru'], $group->getData('hosts')); } + public function testInvalidHosts(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid $hosts provided, list of string expected.'); + + $group = new Group(hosts: ['https://yiiframework.com/', 123]); + } + public function testName(): void { $group = Group::create()->namePrefix('api'); diff --git a/tests/RouteTest.php b/tests/RouteTest.php index 92b158b..cf701bc 100644 --- a/tests/RouteTest.php +++ b/tests/RouteTest.php @@ -28,6 +28,29 @@ final class RouteTest extends TestCase { use AssertTrait; + public function testSimpleInstance(): void + { + $route = new Route( + methods: [Method::GET], + pattern: '/', + action: [TestController::class, 'index'], + middlewares: [TestMiddleware1::class], + override: true, + ); + + $this->assertInstanceOf(Route::class, $route); + $this->assertCount(2, $route->getData('enabledMiddlewares')); + $this->assertTrue($route->getData('override')); + } + + public function testEmptyMethods(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('$methods cannot be empty.'); + + new Route([], ''); + } + public function testName(): void { $route = Route::get('/')->name('test.route'); @@ -371,6 +394,14 @@ public function testMiddlewaresWithKeys(): void ); } + public function testInvalidMiddlewares(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid $middlewares provided, list of string or array or callable expected.'); + + $route = new Route([Method::GET], '/', middlewares: [static fn () => new Response(), (object) ['test' => 1]]); + } + public function testDebugInfo(): void { $route = Route::get('/') @@ -438,6 +469,14 @@ public function testDuplicateHosts(): void $this->assertSame(['a.com', 'b.com'], $route->getData('hosts')); } + public function testInvalidHosts(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid $hosts provided, list of string expected.'); + + $route = new Route([Method::GET], '/', hosts: ['b.com', 123]); + } + public function testImmutability(): void { $route = Route::get('/');