Skip to content

Commit

Permalink
Allow to create route instance directly
Browse files Browse the repository at this point in the history
  • Loading branch information
rustamwin committed Nov 6, 2023
1 parent 2a4f200 commit 02971c6
Show file tree
Hide file tree
Showing 4 changed files with 172 additions and 14 deletions.
50 changes: 46 additions & 4 deletions src/Group.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<array|callable|string>|null
Expand All @@ -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;
}

/**
Expand Down Expand Up @@ -215,4 +228,33 @@ private function getEnabledMiddlewares(): array

return $this->enabledMiddlewaresCache;
}

/**
* @psalm-assert array<string> $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<array|callable|string> $middlewares
*/
private function assertMiddlewares(array $middlewares): void
{
/** @var mixed $middleware */
foreach ($middlewares as $middleware) {
if (is_string($middleware) || is_callable($middleware) || is_array($middleware)) {

Check warning on line 251 in src/Group.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.1-ubuntu-latest

Escaped Mutant for Mutator "LogicalOr": --- Original +++ New @@ @@ { /** @var mixed $middleware */ foreach ($middlewares as $middleware) { - if (is_string($middleware) || is_callable($middleware) || is_array($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.');

Check warning on line 251 in src/Group.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.1-ubuntu-latest

Escaped Mutant for Mutator "LogicalOr": --- Original +++ New @@ @@ { /** @var mixed $middleware */ foreach ($middlewares as $middleware) { - if (is_string($middleware) || is_callable($middleware) || is_array($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.');

Check warning on line 251 in src/Group.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.1-ubuntu-latest

Escaped Mutant for Mutator "LogicalOrNegation": --- Original +++ New @@ @@ { /** @var mixed $middleware */ foreach ($middlewares as $middleware) { - if (is_string($middleware) || is_callable($middleware) || is_array($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.');

Check warning on line 251 in src/Group.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.1-ubuntu-latest

Escaped Mutant for Mutator "LogicalOrSingleSubExprNegation": --- Original +++ New @@ @@ { /** @var mixed $middleware */ foreach ($middlewares as $middleware) { - if (is_string($middleware) || is_callable($middleware) || is_array($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.');
continue;
}

throw new \InvalidArgumentException(
'Invalid $middlewares provided, list of string or array or callable expected.'
);
}
}
}
80 changes: 70 additions & 10 deletions src/Route.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -32,25 +34,50 @@ final class Route implements Stringable
*/
private array $middlewares = [];

private array $disabledMiddlewares = [];

/**
* @psalm-var list<array|callable|string>|null
*/
private ?array $enabledMiddlewaresCache = null;

/**
* @var array<string,string>
* @var array<string,scalar|Stringable|null>
*/
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<string,scalar|Stringable|null> $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);

Check warning on line 76 in src/Route.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.1-ubuntu-latest

Escaped Mutant for Mutator "UnwrapArrayMap": --- Original +++ New @@ @@ $this->methods = $methods; $this->middlewares = $middlewares; $this->hosts = $hosts; - $this->defaults = array_map('\\strval', $defaults); + $this->defaults = $defaults; if (!empty($action)) { $this->middlewares[] = $action; $this->actionAdded = true;
if (!empty($action)) {
$this->middlewares[] = $action;
$this->actionAdded = true;

Check warning on line 79 in src/Route.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.1-ubuntu-latest

Escaped Mutant for Mutator "TrueValue": --- Original +++ New @@ @@ $this->defaults = array_map('\\strval', $defaults); if (!empty($action)) { $this->middlewares[] = $action; - $this->actionAdded = true; + $this->actionAdded = false; } } public static function get(string $pattern) : self
}
}

public static function get(string $pattern): self
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -314,4 +341,37 @@ private function getEnabledMiddlewares(): array

return $this->enabledMiddlewaresCache;
}

/**
* @psalm-assert array<string> $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<array|callable|string> $middlewares
*/
private function assertMiddlewares(array $middlewares): void
{
/** @var mixed $middleware */
foreach ($middlewares as $middleware) {
if (is_string($middleware)) {
continue;

Check warning on line 365 in src/Route.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.1-ubuntu-latest

Escaped Mutant for Mutator "Continue_": --- Original +++ New @@ @@ /** @var mixed $middleware */ foreach ($middlewares as $middleware) { if (is_string($middleware)) { - continue; + break; } if (is_callable($middleware) || is_array($middleware)) { continue;
}

if (is_callable($middleware) || is_array($middleware)) {

Check warning on line 368 in src/Route.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.1-ubuntu-latest

Escaped Mutant for Mutator "LogicalOr": --- Original +++ New @@ @@ if (is_string($middleware)) { continue; } - if (is_callable($middleware) || is_array($middleware)) { + if (is_callable($middleware) && is_array($middleware)) { continue; } throw new \InvalidArgumentException('Invalid $middlewares provided, list of string or array or callable expected.');

Check warning on line 368 in src/Route.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.1-ubuntu-latest

Escaped Mutant for Mutator "LogicalOrNegation": --- Original +++ New @@ @@ if (is_string($middleware)) { continue; } - if (is_callable($middleware) || is_array($middleware)) { + if (!(is_callable($middleware) || is_array($middleware))) { continue; } throw new \InvalidArgumentException('Invalid $middlewares provided, list of string or array or callable expected.');

Check warning on line 368 in src/Route.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.1-ubuntu-latest

Escaped Mutant for Mutator "LogicalOrSingleSubExprNegation": --- Original +++ New @@ @@ if (is_string($middleware)) { continue; } - if (is_callable($middleware) || is_array($middleware)) { + if (!is_callable($middleware) || is_array($middleware)) { continue; } throw new \InvalidArgumentException('Invalid $middlewares provided, list of string or array or callable expected.');
continue;
}

throw new \InvalidArgumentException(
'Invalid $middlewares provided, list of string or array or callable expected.'
);
}
}
}
17 changes: 17 additions & 0 deletions tests/GroupTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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');
Expand Down
39 changes: 39 additions & 0 deletions tests/RouteTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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('/')
Expand Down Expand Up @@ -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('/');
Expand Down

0 comments on commit 02971c6

Please sign in to comment.