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

Commit

Permalink
Merge branch 'feature/236' into develop
Browse files Browse the repository at this point in the history
Close #236
  • Loading branch information
weierophinney committed May 1, 2017
2 parents 9935a57 + 47a1a11 commit 486ff29
Show file tree
Hide file tree
Showing 8 changed files with 538 additions and 46 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ All notable changes to this project will be documented in this file, in reverse
it will be marshaled into a `Zend\Stratigility\MiddlewarePipe` instance, using
the same rules as if you specified a single middleware.

- [#236](https://github.com/zendframework/zend-mvc/pull/236) adds the ability to
attach dispatch listeners to middleware when using the `MiddlewareListener`.
Attach shared events to the class identifier `Zend\Mvc\Controller\MiddlewareController`.
This feature helps ensure that listeners that should run for every controller
(e.g., authentication or authorization listeners) will run even for
middleware.

### Deprecated

- Nothing.
Expand Down
123 changes: 123 additions & 0 deletions src/Controller/MiddlewareController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
<?php
/**
* @see https://github.com/zendframework/zend-mvc for the canonical source repository
* @copyright Copyright (c) 2017 Zend Technologies USA Inc. (http://www.zend.com)
* @license https://github.com/zendframework/zend-mvc/blob/master/LICENSE.md New BSD License
*/

namespace Zend\Mvc\Controller;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Zend\EventManager\EventManagerInterface;
use Zend\Http\Request;
use Zend\Mvc\Exception\ReachedFinalHandlerException;
use Zend\Mvc\Exception\RuntimeException;
use Zend\Mvc\MvcEvent;
use Zend\Psr7Bridge\Psr7ServerRequest;
use Zend\Router\RouteMatch;
use Zend\Stratigility\Delegate\CallableDelegateDecorator;
use Zend\Stratigility\MiddlewarePipe;

/**
* @internal don't use this in your codebase, or else @ocramius will hunt you
* down. This is just an internal hack to make middleware trigger
* 'dispatch' events attached to the DispatchableInterface identifier.
*
* Specifically, it will receive a @see MiddlewarePipe and a
* @see ResponseInterface prototype, and then dispatch the pipe whilst still
* behaving like a normal controller. That is needed for any events
* attached to the @see \Zend\Stdlib\DispatchableInterface identifier to
* reach their listeners on any attached
* @see \Zend\EventManager\SharedEventManagerInterface
*/
final class MiddlewareController extends AbstractController
{
/**
* @var MiddlewarePipe
*/
private $pipe;

/**
* @var ResponseInterface
*/
private $responsePrototype;

public function __construct(
MiddlewarePipe $pipe,
ResponseInterface $responsePrototype,
EventManagerInterface $eventManager,
MvcEvent $event
) {
$this->eventIdentifier = __CLASS__;
$this->pipe = $pipe;
$this->responsePrototype = $responsePrototype;

$this->setEventManager($eventManager);
$this->setEvent($event);
}

/**
* {@inheritDoc}
*
* @throws RuntimeException
*/
public function onDispatch(MvcEvent $e)
{
$routeMatch = $e->getRouteMatch();
$psr7Request = $this->populateRequestParametersFromRoute(
$this->loadRequest()->withAttribute(RouteMatch::class, $routeMatch),
$routeMatch
);

$result = $this->pipe->process($psr7Request, new CallableDelegateDecorator(
function () {
throw ReachedFinalHandlerException::create();
},
$this->responsePrototype
));

$e->setResult($result);

return $result;
}

/**
* @return \Zend\Diactoros\ServerRequest
*
* @throws RuntimeException
*/
private function loadRequest()
{
$request = $this->request;

if (! $request instanceof Request) {
throw new RuntimeException(sprintf(
'Expected request to be a %s, %s given',
Request::class,
get_class($request)
));
}

return Psr7ServerRequest::fromZend($request);
}

/**
* @param ServerRequestInterface $request
* @param RouteMatch|null $routeMatch
*
* @return ServerRequestInterface
*/
private function populateRequestParametersFromRoute(ServerRequestInterface $request, RouteMatch $routeMatch = null)
{
if (! $routeMatch) {
return $request;
}

foreach ($routeMatch->getParams() as $key => $value) {
$request = $request->withAttribute($key, $value);
}

return $request;
}
}
12 changes: 7 additions & 5 deletions src/DispatchListener.php
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @link http://github.com/zendframework/zf2 for the canonical source repository
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
* @see https://github.com/zendframework/zend-mvc for the canonical source repository
* @copyright Copyright (c) 2005-2017 Zend Technologies USA Inc. (http://www.zend.com)
* @license https://github.com/zendframework/zend-mvc/blob/master/LICENSE.md New BSD License
*/

namespace Zend\Mvc;
Expand Down Expand Up @@ -76,6 +74,10 @@ public function attach(EventManagerInterface $events, $priority = 1)
*/
public function onDispatch(MvcEvent $e)
{
if (null !== $e->getResult()) {
return;
}

$routeMatch = $e->getRouteMatch();
$controllerName = $routeMatch instanceof RouteMatch
? $routeMatch->getParam('controller', 'not-found')
Expand Down
32 changes: 16 additions & 16 deletions src/MiddlewareListener.php
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @link http://github.com/zendframework/zf2 for the canonical source repository
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
* @see https://github.com/zendframework/zend-mvc for the canonical source repository
* @copyright Copyright (c) 2015-2017 Zend Technologies USA Inc. (http://www.zend.com)
* @license https://github.com/zendframework/zend-mvc/blob/master/LICENSE.md New BSD License
*/

namespace Zend\Mvc;
Expand All @@ -18,7 +16,7 @@
use Zend\EventManager\EventManagerInterface;
use Zend\Mvc\Exception\InvalidMiddlewareException;
use Zend\Mvc\Exception\ReachedFinalHandlerException;
use Zend\Psr7Bridge\Psr7ServerRequest as Psr7Request;
use Zend\Mvc\Controller\MiddlewareController;
use Zend\Psr7Bridge\Psr7Response;
use Zend\Router\RouteMatch;
use Zend\Stratigility\Delegate\CallableDelegateDecorator;
Expand All @@ -45,6 +43,10 @@ public function attach(EventManagerInterface $events, $priority = 1)
*/
public function onDispatch(MvcEvent $event)
{
if (null !== $event->getResult()) {
return;
}

$routeMatch = $event->getRouteMatch();
$middleware = $routeMatch->getParam('middleware', false);
if (false === $middleware) {
Expand Down Expand Up @@ -78,16 +80,12 @@ public function onDispatch(MvcEvent $event)

$caughtException = null;
try {
$psr7Request = Psr7Request::fromZend($request)->withAttribute(RouteMatch::class, $routeMatch);
foreach ($routeMatch->getParams() as $key => $value) {
$psr7Request = $psr7Request->withAttribute($key, $value);
}
$return = $pipe->process($psr7Request, new CallableDelegateDecorator(
function (PsrServerRequestInterface $request, PsrResponseInterface $response) {
throw ReachedFinalHandlerException::create();
},
$psr7ResponsePrototype
));
$return = (new MiddlewareController(
$pipe,
$psr7ResponsePrototype,
$application->getServiceManager()->get('EventManager'),
$event
))->dispatch($request, $response);
} catch (\Throwable $ex) {
$caughtException = $ex;
} catch (\Exception $ex) { // @TODO clean up once PHP 7 requirement is enforced
Expand All @@ -107,6 +105,8 @@ function (PsrServerRequestInterface $request, PsrResponseInterface $response) {
}
}

$event->setError('');

if (! $return instanceof PsrResponseInterface) {
$event->setResult($return);
return $return;
Expand Down
145 changes: 145 additions & 0 deletions test/Controller/MiddlewareControllerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
<?php
/**
* @see https://github.com/zendframework/zend-mvc for the canonical source repository
* @copyright Copyright (c) 2017 Zend Technologies USA Inc. (http://www.zend.com)
* @license https://github.com/zendframework/zend-mvc/blob/master/LICENSE.md New BSD License
*/

namespace ZendTest\Mvc\Controller;

use PHPUnit\Framework\TestCase;
use Psr\Http\Message\ResponseInterface;
use Zend\EventManager\EventManager;
use Zend\EventManager\EventManagerInterface;
use Zend\Http\Request;
use Zend\Http\Response;
use Zend\Mvc\Controller\AbstractController;
use Zend\Mvc\Controller\MiddlewareController;
use Zend\Mvc\Exception\RuntimeException;
use Zend\Mvc\MvcEvent;
use Zend\Stdlib\DispatchableInterface;
use Zend\Stdlib\RequestInterface;
use Zend\Stratigility\MiddlewarePipe;

/**
* @covers \Zend\Mvc\Controller\MiddlewareController
*/
class MiddlewareControllerTest extends TestCase
{
/**
* @var MiddlewarePipe|\PHPUnit_Framework_MockObject_MockObject
*/
private $pipe;

/**
* @var ResponseInterface|\PHPUnit_Framework_MockObject_MockObject
*/
private $responsePrototype;

/**
* @var EventManagerInterface
*/
private $eventManager;

/**
* @var AbstractController|\PHPUnit_Framework_MockObject_MockObject
*/
private $controller;

/**
* @var MvcEvent
*/
private $event;

/**
* {@inheritDoc}
*/
protected function setUp()
{
$this->pipe = $this->createMock(MiddlewarePipe::class);
$this->responsePrototype = $this->createMock(ResponseInterface::class);
$this->eventManager = $this->createMock(EventManagerInterface::class);
$this->event = new MvcEvent();
$this->eventManager = new EventManager();

$this->controller = new MiddlewareController(
$this->pipe,
$this->responsePrototype,
$this->eventManager,
$this->event
);
}

public function testWillAssignCorrectEventManagerIdentifiers()
{
$identifiers = $this->eventManager->getIdentifiers();

self::assertContains(MiddlewareController::class, $identifiers);
self::assertContains(AbstractController::class, $identifiers);
self::assertContains(DispatchableInterface::class, $identifiers);
}

public function testWillDispatchARequestAndResponseWithAGivenPipe()
{
$request = new Request();
$response = new Response();
$result = $this->createMock(ResponseInterface::class);
/* @var $dispatchListener callable|\PHPUnit_Framework_MockObject_MockObject */
$dispatchListener = $this->getMockBuilder(\stdClass::class)->setMethods(['__invoke'])->getMock();

$this->eventManager->attach(MvcEvent::EVENT_DISPATCH, $dispatchListener, 100);
$this->eventManager->attach(MvcEvent::EVENT_DISPATCH_ERROR, function () {
self::fail('No dispatch error expected');
}, 100);

$dispatchListener
->expects(self::once())
->method('__invoke')
->with(self::callback(function (MvcEvent $event) use ($request, $response) {
self::assertSame($this->event, $event);
self::assertSame(MvcEvent::EVENT_DISPATCH, $event->getName());
self::assertSame($this->controller, $event->getTarget());
self::assertSame($request, $event->getRequest());
self::assertSame($response, $event->getResponse());

return true;
}));

$this->pipe->expects(self::once())->method('process')->willReturn($result);

$controllerResult = $this->controller->dispatch($request, $response);

self::assertSame($result, $controllerResult);
self::assertSame($result, $this->event->getResult());
}

public function testWillRefuseDispatchingInvalidRequestTypes()
{
/* @var $request RequestInterface */
$request = $this->createMock(RequestInterface::class);
$response = new Response();
/* @var $dispatchListener callable|\PHPUnit_Framework_MockObject_MockObject */
$dispatchListener = $this->getMockBuilder(\stdClass::class)->setMethods(['__invoke'])->getMock();

$this->eventManager->attach(MvcEvent::EVENT_DISPATCH, $dispatchListener, 100);

$dispatchListener
->expects(self::once())
->method('__invoke')
->with(self::callback(function (MvcEvent $event) use ($request, $response) {
self::assertSame($this->event, $event);
self::assertSame(MvcEvent::EVENT_DISPATCH, $event->getName());
self::assertSame($this->controller, $event->getTarget());
self::assertSame($request, $event->getRequest());
self::assertSame($response, $event->getResponse());

return true;
}));

$this->pipe->expects(self::never())->method('process');

$this->expectException(RuntimeException::class);

$this->controller->dispatch($request, $response);
}
}
Loading

0 comments on commit 486ff29

Please sign in to comment.