Skip to content
This repository has been archived by the owner on Jan 21, 2020. It is now read-only.

Commit

Permalink
Merging develop to master in preparation for 2.3.0 release
Browse files Browse the repository at this point in the history
  • Loading branch information
weierophinney committed Feb 1, 2018
2 parents 0f45d8e + f96eb72 commit 57c6286
Show file tree
Hide file tree
Showing 5 changed files with 372 additions and 2 deletions.
21 changes: 19 additions & 2 deletions CHANGELOG.md
Expand Up @@ -2,11 +2,28 @@

All notable changes to this project will be documented in this file, in reverse chronological order by release.

## 2.2.1 - TBD
## 2.3.0 - 2018-02-01

### Added

- Nothing.
- [#46](https://github.com/zendframework/zend-expressive-router/pull/46) adds
two new middleware, imported from zend-expressive and re-worked for general
purpose usage:

- `Zend\Expressive\Router\RouteMiddleware` composes a router and a response
prototype. When processed, if no match is found due to an un-matched HTTP
method, it uses the response prototype to create a 405 response with an
`Allow` header listing allowed methods; otherwise, it dispatches to the next
middleware via the provided handler. If a match is made, the route result is
stored as a request attribute using the `RouteResult` class name, and each
matched parameter is also added as a request attribute before delegating
request handling.

- `Zend\Expressive\Router\DispatchMiddleware` checks for a `RouteResult`
attribute in the request. If none is found, it delegates handling of the
request to the handler. If one is found, it pulls the matched middleware and
processes it. If the middleware is not http-interop middleware, it raises an
exception.

### Changed

Expand Down
62 changes: 62 additions & 0 deletions src/DispatchMiddleware.php
@@ -0,0 +1,62 @@
<?php
/**
* @see https://github.com/zendframework/zend-expressive-router for the canonical source repository
* @copyright Copyright (c) 2018 Zend Technologies USA Inc. (http://www.zend.com)
* @license https://github.com/zendframework/zend-expressive-router/blob/master/LICENSE.md New BSD License
*/

namespace Zend\Expressive\Router;

use Interop\Http\Middleware\ServerMiddlewareInterface as LegacyLegacyMiddlewareInterface;
use Interop\Http\ServerMiddleware\MiddlewareInterface as LegacyMiddlewareInterface;
use Interop\Http\Server\MiddlewareInterface as InteropMiddlewareInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Webimpress\HttpMiddlewareCompatibility\HandlerInterface;
use Webimpress\HttpMiddlewareCompatibility\MiddlewareInterface;

use const Webimpress\HttpMiddlewareCompatibility\HANDLER_METHOD;

/**
* Default dispatch middleware.
*
* Checks for a composed route result in the request. If none is provided,
* delegates to the next middleware.
*
* Otherwise, it pulls the middleware from the route result. If the middleware
* is not http-interop middleware, it raises an exception. This means that
* this middleware is incompatible with routes that store non-http-interop
* middleware instances! Make certain you only provide middleware instances
* to your router when using this middleware.
*/
class DispatchMiddleware implements MiddlewareInterface
{
/**
* @param ServerRequestInterface $request
* @param HandlerInterface $handler
* @return ResponseInterface
*/
public function process(ServerRequestInterface $request, HandlerInterface $handler)
{
$routeResult = $request->getAttribute(RouteResult::class, false);
if (! $routeResult) {
return $handler->{HANDLER_METHOD}($request);
}

$middleware = $routeResult->getMatchedMiddleware();

if (! $middleware instanceof LegacyLegacyMiddlewareInterface
&& ! $middleware instanceof LegacyMiddlewareInterface
&& ! $middleware instanceof InteropMiddlewareInterface
) {
throw new Exception\RuntimeException(sprintf(
'Unknown middleware type stored in route; %s expects an http-interop'
. ' middleware instance; received %s',
__CLASS__,
is_object($middleware) ? get_class($middleware) : gettype($middleware)
));
}

return $middleware->process($request, $handler);
}
}
82 changes: 82 additions & 0 deletions src/RouteMiddleware.php
@@ -0,0 +1,82 @@
<?php
/**
* @see https://github.com/zendframework/zend-expressive-router for the canonical source repository
* @copyright Copyright (c) 2018 Zend Technologies USA Inc. (http://www.zend.com)
* @license https://github.com/zendframework/zend-expressive-router/blob/master/LICENSE.md New BSD License
*/

namespace Zend\Expressive\Router;

use Fig\Http\Message\StatusCodeInterface as StatusCode;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Zend\Expressive\Router\RouteResult;
use Zend\Expressive\Router\RouterInterface;
use Webimpress\HttpMiddlewareCompatibility\HandlerInterface;
use Webimpress\HttpMiddlewareCompatibility\MiddlewareInterface;

use const Webimpress\HttpMiddlewareCompatibility\HANDLER_METHOD;

/**
* Default routing middleware.
*
* Uses the composed router to match against the incoming request.
*
* When routing failure occurs, if the failure is due to HTTP method, uses
* the composed response prototype to generate a 405 response; otherwise,
* it delegates to the next middleware.
*
* If routing succeeds, injects the route result into the request (under the
* RouteResult class name), as well as any matched parameters, before
* delegating to the next middleware.
*/
class RouteMiddleware implements MiddlewareInterface
{
/**
* Response prototype for 405 responses.
*
* @var ResponseInterface
*/
private $responsePrototype;

/**
* @var RouterInterface
*/
private $router;

/**
* @param RouterInterface $router
* @param ResponseInterface $responsePrototype
*/
public function __construct(RouterInterface $router, ResponseInterface $responsePrototype)
{
$this->router = $router;
$this->responsePrototype = $responsePrototype;
}

/**
* @param ServerRequestInterface $request
* @param HandlerInterface $handler
* @return ResponseInterface
*/
public function process(ServerRequestInterface $request, HandlerInterface $handler)
{
$result = $this->router->match($request);

if ($result->isFailure()) {
if ($result->isMethodFailure()) {
return $this->responsePrototype->withStatus(StatusCode::STATUS_METHOD_NOT_ALLOWED)
->withHeader('Allow', implode(',', $result->getAllowedMethods()));
}
return $handler->{HANDLER_METHOD}($request);
}

// Inject the actual route result, as well as individual matched parameters.
$request = $request->withAttribute(RouteResult::class, $result);
foreach ($result->getMatchedParams() as $param => $value) {
$request = $request->withAttribute($param, $value);
}

return $handler->{HANDLER_METHOD}($request);
}
}
96 changes: 96 additions & 0 deletions test/DispatchMiddlewareTest.php
@@ -0,0 +1,96 @@
<?php
/**
* @see https://github.com/zendframework/zend-expressive-router for the canonical source repository
* @copyright Copyright (c) 2018 Zend Technologies USA Inc. (http://www.zend.com)
* @license https://github.com/zendframework/zend-expressive-router/blob/master/LICENSE.md New BSD License
*/

namespace ZendTest\Expressive\Router;

use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\ObjectProphecy;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Webimpress\HttpMiddlewareCompatibility\HandlerInterface;
use Webimpress\HttpMiddlewareCompatibility\MiddlewareInterface;
use Zend\Expressive\Router\DispatchMiddleware;
use Zend\Expressive\Router\Exception\RuntimeException;
use Zend\Expressive\Router\Route;
use Zend\Expressive\Router\RouteResult;

use const Webimpress\HttpMiddlewareCompatibility\HANDLER_METHOD;

class DispatchMiddlewareTest extends TestCase
{
/** @var HandlerInterface|ObjectProphecy */
private $handler;

/** @var DispatchMiddleware */
private $middleware;

public function setUp()
{
$this->request = $this->prophesize(ServerRequestInterface::class);
$this->handler = $this->prophesize(HandlerInterface::class);
$this->middleware = new DispatchMiddleware();
}

public function testInvokesDelegateIfRequestDoesNotContainRouteResult()
{
$expected = $this->prophesize(ResponseInterface::class)->reveal();
$this->request->getAttribute(RouteResult::class, false)->willReturn(false);
$this->handler->{HANDLER_METHOD}($this->request->reveal())->willReturn($expected);

$response = $this->middleware->process($this->request->reveal(), $this->handler->reveal());

$this->assertSame($expected, $response);
}

public function testInvokesMatchedMiddlewareWhenRouteResult()
{
$this->handler->{HANDLER_METHOD}()->shouldNotBeCalled();

$expected = $this->prophesize(ResponseInterface::class)->reveal();
$routedMiddleware = $this->prophesize(MiddlewareInterface::class);
$routedMiddleware
->process($this->request->reveal(), $this->handler->reveal())
->willReturn($expected);

$routeResult = RouteResult::fromRoute(new Route('/', $routedMiddleware->reveal()));

$this->request->getAttribute(RouteResult::class, false)->willReturn($routeResult);

$response = $this->middleware->process($this->request->reveal(), $this->handler->reveal());

$this->assertSame($expected, $response);
}

public function invalidMiddleware()
{
return [
// @codingStandardsIgnoreStart
// There are more types we could test, but Route has a number of tests
// in place already, and these are the three it allows that the dispatch
// middleware cannot allow.
'string' => ['middleware'],
'array' => [['middleware']],
'callable' => [function () {}],
// @codingStandardsIgnoreEnd
];
}

/**
* @dataProvider invalidMiddleware
* @param mixed $middleware
*/
public function testInvalidRoutedMiddlewareInRouteResultResultsInException($middleware)
{
$this->handler->{HANDLER_METHOD}()->shouldNotBeCalled();
$routeResult = RouteResult::fromRoute(new Route('/', $middleware));
$this->request->getAttribute(RouteResult::class, false)->willReturn($routeResult);

$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('expects an http-interop');
$this->middleware->process($this->request->reveal(), $this->handler->reveal());
}
}
113 changes: 113 additions & 0 deletions test/RouteMiddlewareTest.php
@@ -0,0 +1,113 @@
<?php
/**
* @see https://github.com/zendframework/zend-expressive-router for the canonical source repository
* @copyright Copyright (c) 2018 Zend Technologies USA Inc. (http://www.zend.com)
* @license https://github.com/zendframework/zend-expressive-router/blob/master/LICENSE.md New BSD License
*/

namespace ZendTest\Expressive\Router;

use Fig\Http\Message\StatusCodeInterface as StatusCode;
use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\ObjectProphecy;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Webimpress\HttpMiddlewareCompatibility\HandlerInterface;
use Webimpress\HttpMiddlewareCompatibility\MiddlewareInterface;
use Zend\Expressive\Router\Route;
use Zend\Expressive\Router\RouteMiddleware;
use Zend\Expressive\Router\RouteResult;
use Zend\Expressive\Router\RouterInterface;

use const Webimpress\HttpMiddlewareCompatibility\HANDLER_METHOD;

class RouteMiddlewareTest extends TestCase
{
/** @var RouterInterface|ObjectProphecy */
private $router;

/** @var ResponseInterface|ObjectProphecy */
private $response;

/** @var RouteMiddleware */
private $middleware;

/** @var ServerRequestInterface|ObjectProphecy */
private $request;

/** @var HandlerInterface|ObjectProphecy */
private $handler;

public function setUp()
{
$this->router = $this->prophesize(RouterInterface::class);
$this->response = $this->prophesize(ResponseInterface::class);
$this->middleware = new RouteMiddleware(
$this->router->reveal(),
$this->response->reveal()
);

$this->request = $this->prophesize(ServerRequestInterface::class);
$this->handler = $this->prophesize(HandlerInterface::class);
}

public function testRoutingFailureDueToHttpMethodCallsNextWithNotAllowedResponseAndError()
{
$result = RouteResult::fromRouteFailure(['GET', 'POST']);

$this->router->match($this->request->reveal())->willReturn($result);
$this->handler->{HANDLER_METHOD}()->shouldNotBeCalled();
$this->request->withAttribute()->shouldNotBeCalled();
$this->response->withStatus(StatusCode::STATUS_METHOD_NOT_ALLOWED)->will([$this->response, 'reveal']);
$this->response->withHeader('Allow', 'GET,POST')->will([$this->response, 'reveal']);

$response = $this->middleware->process($this->request->reveal(), $this->handler->reveal());
$this->assertSame($response, $this->response->reveal());
}

public function testGeneralRoutingFailureInvokesDelegateWithSameRequest()
{
$result = RouteResult::fromRouteFailure();

$this->router->match($this->request->reveal())->willReturn($result);
$this->response->withStatus()->shouldNotBeCalled();
$this->response->withHeader()->shouldNotBeCalled();
$this->request->withAttribute()->shouldNotBeCalled();

$expected = $this->prophesize(ResponseInterface::class)->reveal();
$this->handler->{HANDLER_METHOD}($this->request->reveal())->willReturn($expected);

$response = $this->middleware->process($this->request->reveal(), $this->handler->reveal());
$this->assertSame($expected, $response);
}

public function testRoutingSuccessDelegatesToNextAfterFirstInjectingRouteResultAndAttributesInRequest()
{
$middleware = $this->prophesize(MiddlewareInterface::class)->reveal();
$parameters = ['foo' => 'bar', 'baz' => 'bat'];
$result = RouteResult::fromRoute(
new Route('/foo', $middleware),
$parameters
);

$this->router->match($this->request->reveal())->willReturn($result);

$this->request
->withAttribute(RouteResult::class, $result)
->will([$this->request, 'reveal']);
foreach ($parameters as $key => $value) {
$this->request->withAttribute($key, $value)->will([$this->request, 'reveal']);
}

$this->response->withStatus()->shouldNotBeCalled();
$this->response->withHeader()->shouldNotBeCalled();

$expected = $this->prophesize(ResponseInterface::class)->reveal();
$this->handler
->{HANDLER_METHOD}($this->request->reveal())
->willReturn($expected);

$response = $this->middleware->process($this->request->reveal(), $this->handler->reveal());
$this->assertSame($expected, $response);
}
}

0 comments on commit 57c6286

Please sign in to comment.