Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Router alt nested route #20

Merged
merged 10 commits into from
Feb 2, 2020
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 57 additions & 103 deletions src/Route.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,105 +3,89 @@
namespace Yiisoft\Router;

use InvalidArgumentException;
use LogicException;
use Yiisoft\Http\Method;
use Yiisoft\Router\Middleware\Callback;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;

/**
* Route defines a mapping from URL to callback / name and vice versa
*/
final class Route implements MiddlewareInterface, RequestHandlerInterface
final class Route implements MiddlewareInterface
{
private ?string $name = null;
/** @var string[] */
private array $methods;
private string $pattern;
private ?string $host = null;
/**
* Contains a chain of middleware wrapped in handlers.
* Each handler points to the handler of middleware that will be processed next.
* @var RequestHandlerInterface|null stack of middleware
*/
private ?RequestHandlerInterface $stack = null;
samdark marked this conversation as resolved.
Show resolved Hide resolved

/**
* @var MiddlewareInterface[]|callable[]
*/
private array $middlewares = [];
private array $defaults = [];
private RequestHandlerInterface $nextHandler;

private function __construct()
yiiliveext marked this conversation as resolved.
Show resolved Hide resolved
{
}

public static function get(string $pattern): self
public static function get(string $pattern, $middleware = null): self
{
$route = new static();
$route->methods = [Method::GET];
$route->pattern = $pattern;
return $route;
return static::methods([Method::GET], $pattern, $middleware);
}

public static function post(string $pattern): self
public static function post(string $pattern, $middleware = null): self
{
$route = new static();
$route->methods = [Method::POST];
$route->pattern = $pattern;
return $route;
return static::methods([Method::POST], $pattern, $middleware);
}

public static function put(string $pattern): self
public static function put(string $pattern, $middleware = null): self
{
$route = new static();
$route->methods = [Method::PUT];
$route->pattern = $pattern;
return $route;
return static::methods([Method::PUT], $pattern, $middleware);
}

public static function delete(string $pattern): self
public static function delete(string $pattern, $middleware = null): self
{
$route = new static();
$route->methods = [Method::DELETE];
$route->pattern = $pattern;
return $route;
return static::methods([Method::DELETE], $pattern, $middleware);
}

public static function patch(string $pattern): self
public static function patch(string $pattern, $middleware = null): self
{
$route = new static();
$route->methods = [Method::PATCH];
$route->pattern = $pattern;
return $route;
return static::methods([Method::PATCH], $pattern, $middleware);
}

public static function head(string $pattern): self
public static function head(string $pattern, $middleware = null): self
{
$route = new static();
$route->methods = [Method::HEAD];
$route->pattern = $pattern;
return $route;
return static::methods([Method::HEAD], $pattern, $middleware);
}

public static function options(string $pattern): self
public static function options(string $pattern, $middleware = null): self
{
$route = new static();
$route->methods = [Method::OPTIONS];
$route->pattern = $pattern;
return $route;
return static::methods([Method::OPTIONS], $pattern, $middleware);
}

public static function methods(array $methods, string $pattern): self
public static function methods(array $methods, string $pattern, $middleware = null): self
{
$route = new static();
$route->methods = $methods;
$route->pattern = $pattern;
if ($middleware !== null) {
$route->middlewares[] = $route->prepareMiddleware($middleware);
}
return $route;
}

public static function anyMethod(string $pattern): self
public static function anyMethod(string $pattern, $middleware = null): self
{
$route = new static();
$route->methods = Method::ANY;
$route->pattern = $pattern;
return $route;
return static::methods(Method::ANY, $pattern, $middleware);
}

public function name(string $name): self
Expand Down Expand Up @@ -155,52 +139,20 @@ private function prepareMiddleware($middleware): MiddlewareInterface
return $middleware;
}

/**
* Adds a handler that should be invoked for a matching route.
* It can be either a PSR middleware or a callable with the following signature:
*
* ```
* function (ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface {
* ```
*
* @param MiddlewareInterface|callable $middleware
* @return Route
*/
public function to($middleware): self
{
$route = clone $this;
$route->middlewares[] = $this->prepareMiddleware($middleware);
return $route;
}

/**
* Adds a handler that should be invoked for a matching route.
* It can be either a PSR middleware or a callable with the following signature:
*
* ```
* function (ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface {
* ```
*
* @param MiddlewareInterface|callable $middleware
* @return Route
*/
public function then($middleware): self
{
return $this->to($middleware);
}

/**
* Prepends a handler that should be invoked for a matching route.
* Last added handler will be invoked first.
* It can be either a PSR middleware or a callable with the following signature:
*
* ```
* function (ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface {
* ```
* * Last addded middleware will be invoked first.
*
* @param MiddlewareInterface|callable $middleware
* @return Route
*/
public function prepend($middleware): self
public function addMiddleware($middleware): self
{
$route = clone $this;
array_unshift($route->middlewares, $this->prepareMiddleware($middleware));
Expand Down Expand Up @@ -251,35 +203,37 @@ public function getDefaults(): array
return $this->defaults;
}

/**
* @internal please use {@see dispatch()} or {@see process()}
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function handle(ServerRequestInterface $request): ResponseInterface
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$middleware = current($this->middlewares);
next($this->middlewares);
if ($middleware === false) {
if ($this->nextHandler !== null) {
return $this->nextHandler->handle($request);
if ($this->stack === null) {
for ($i = count($this->middlewares) - 1; $i >= 0; $i--) {
$handler = $this->wrap($this->middlewares[$i], $handler);
}

throw new LogicException('Middleware stack exhausted');
$this->stack = $handler;
}

return $middleware->process($request, $this);
return $this->stack->handle($request);
}

public function dispatch(ServerRequestInterface $request): ResponseInterface
/**
* Wraps handler by middlewares
*/
private function wrap(MiddlewareInterface $middleware, RequestHandlerInterface $handler): RequestHandlerInterface
{
reset($this->middlewares);
return $this->handle($request);
}
return new class($middleware, $handler) implements RequestHandlerInterface {
private MiddlewareInterface $middleware;
private RequestHandlerInterface $handler;

public function process(ServerRequestInterface $request, RequestHandlerInterface $nextHandler): ResponseInterface
{
$this->nextHandler = $nextHandler;
return $this->dispatch($request);
public function __construct(MiddlewareInterface $middleware, RequestHandlerInterface $handler)
{
$this->middleware = $middleware;
$this->handler = $handler;
}

public function handle(ServerRequestInterface $request): ResponseInterface
{
return $this->middleware->process($request, $this->handler);
}
};
}
}
2 changes: 1 addition & 1 deletion src/RouterFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public function __invoke(ContainerInterface $container): RouterInterface
{
$factory = $this->engineFactory;
/* @var $router RouterInterface */
$router = $factory($container);
$router = $factory();
samdark marked this conversation as resolved.
Show resolved Hide resolved
foreach ($this->routes as $route) {
if ($route instanceof Route) {
$router->addRoute($route);
Expand Down
2 changes: 1 addition & 1 deletion tests/MatchingResultTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public function testFromFailureOnNotFoundFailure(): void

public function testProcessSuccess(): void
{
$route = Route::post('/')->to($this->getMiddleware());
$route = Route::post('/')->addMiddleware($this->getMiddleware());
$result = MatchingResult::fromSuccess($route, []);
$request = new ServerRequest('POST', '/');

Expand Down
2 changes: 1 addition & 1 deletion tests/Middleware/RouterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ public function match(ServerRequestInterface $request): MatchingResult
}

if ($request->getMethod() === 'GET') {
$route = Route::get('/')->to($this->middleware);
$route = Route::get('/')->addMiddleware($this->middleware);
return MatchingResult::fromSuccess($route, ['parameter' => 'value']);
}

Expand Down
67 changes: 35 additions & 32 deletions tests/RouteTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Yiisoft\Http\Method;
use Yiisoft\Router\Middleware\Callback;
use Yiisoft\Router\Route;

final class RouteTest extends TestCase
Expand Down Expand Up @@ -130,17 +131,23 @@ public function testToStringSimple(): void
$this->assertSame('GET /', (string)$route);
}

public function testInvalidTo(): void
public function testInvalidMiddlewareMethod(): void
{
$this->expectException(\InvalidArgumentException::class);
Route::get('/')->to(new \stdClass());
Route::get('/', new \stdClass());
}

public function testToMiddleware(): void
public function testInvalidMiddlewareAdd(): void
{
$this->expectException(\InvalidArgumentException::class);
Route::get('/')->addMiddleware(new \stdClass());
}

public function testAddMiddleware(): void
{
$request = new ServerRequest('GET', '/');

$route = Route::get('/')->to(
$route = Route::get('/')->addMiddleware(
new class() implements MiddlewareInterface {
public function process(
ServerRequestInterface $request,
Expand All @@ -155,11 +162,11 @@ public function process(
$this->assertSame(418, $response->getStatusCode());
}

public function testToCallable(): void
public function testAddCallableMiddleware(): void
{
$request = new ServerRequest('GET', '/');

$route = Route::get('/')->to(
$route = Route::get('/')->addMiddleware(
static function (): ResponseInterface {
return (new Response())->withStatus(418);
}
Expand All @@ -169,46 +176,42 @@ static function (): ResponseInterface {
$this->assertSame(418, $response->getStatusCode());
}

public function testThen(): void
public function testMiddlewareFullStackCalled(): void
{
$request = new ServerRequest('GET', '/');

$route = Route::get('/');

$middleware1 = $this->createMock(MiddlewareInterface::class);
$middleware2 = $this->createMock(MiddlewareInterface::class);

$route = $route->to($middleware1)->then($middleware2);
$routeOne = Route::get('/');

$middleware1
->expects($this->at(0))
->method('process')
->with($request, $route);
$middleware1 = new Callback(function (ServerRequestInterface $request, RequestHandlerInterface $handler) {
return $handler->handle($request);
});
$middleware2 = new Callback(function () {
return new Response(200);
});

// TODO: test that second one is called as well
$routeOne = $routeOne->addMiddleware($middleware2)->addMiddleware($middleware1);

$route->process($request, $this->getRequestHandler());
$response = $routeOne->process($request, $this->getRequestHandler());
$this->assertSame(200, $response->getStatusCode());
}

public function testBefore(): void
public function testMiddlewareStackInterrupted(): void
{
$request = new ServerRequest('GET', '/');

$route = Route::get('/');

$middleware1 = $this->createMock(MiddlewareInterface::class);
$middleware2 = $this->createMock(MiddlewareInterface::class);

$route = $route->to($middleware1)->prepend($middleware2);
$routeTwo = Route::get('/');

$middleware2
->expects($this->at(0))
->method('process')
->with($request, $route);
$middleware1 = new Callback(function () {
return new Response(404);
});
$middleware2 = new Callback(function () {
return new Response(200);
});

// TODO: test that first one is called as well
$routeTwo = $routeTwo->addMiddleware($middleware2)->addMiddleware($middleware1);

$route->process($request, $this->getRequestHandler());
$response = $routeTwo->process($request, $this->getRequestHandler());
$this->assertSame(404, $response->getStatusCode());
}

private function getRequestHandler(): RequestHandlerInterface
Expand Down