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

Commit aa031e8

Browse files
committed
Merge pull request #236 from Ocramius/fix/allow-middleware-dispatch-to-behave-like-controller-dispatch
Fix: allow middleware dispatch to behave like controller dispatch
2 parents 9935a57 + a122791 commit aa031e8

File tree

7 files changed

+517
-21
lines changed

7 files changed

+517
-21
lines changed
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
<?php
2+
/**
3+
* Zend Framework (http://framework.zend.com/)
4+
*
5+
* @link http://github.com/zendframework/zf2 for the canonical source repository
6+
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
7+
* @license http://framework.zend.com/license/new-bsd New BSD License
8+
*/
9+
10+
namespace Zend\Mvc\Controller;
11+
12+
use Psr\Http\Message\ResponseInterface;
13+
use Psr\Http\Message\ServerRequestInterface;
14+
use Zend\EventManager\EventManagerInterface;
15+
use Zend\Http\Request;
16+
use Zend\Mvc\Exception\ReachedFinalHandlerException;
17+
use Zend\Mvc\Exception\RuntimeException;
18+
use Zend\Mvc\MvcEvent;
19+
use Zend\Psr7Bridge\Psr7ServerRequest;
20+
use Zend\Router\RouteMatch;
21+
use Zend\Stratigility\Delegate\CallableDelegateDecorator;
22+
use Zend\Stratigility\MiddlewarePipe;
23+
24+
/**
25+
* @internal don't use this in your codebase, or else @ocramius will hunt you down. This is just an internal
26+
* @internal hack to make middleware trigger 'dispatch' events attached to the DispatchableInterface identifier.
27+
*
28+
* Specifically, it will receive a @see MiddlewarePipe, a @see ResponseInterface prototype, and then dispatch
29+
* the pipe whilst still behaving like a normal controller. That is needed for any events attached to
30+
* the @see \Zend\Stdlib\DispatchableInterface identifier to reach their listeners on any attached
31+
* @see \Zend\EventManager\SharedEventManagerInterface
32+
*/
33+
final class MiddlewareController extends AbstractController
34+
{
35+
/**
36+
* @var MiddlewarePipe
37+
*/
38+
private $pipe;
39+
40+
/**
41+
* @var ResponseInterface
42+
*/
43+
private $responsePrototype;
44+
45+
public function __construct(
46+
MiddlewarePipe $pipe,
47+
ResponseInterface $responsePrototype,
48+
EventManagerInterface $eventManager,
49+
MvcEvent $event
50+
) {
51+
$this->eventIdentifier = __CLASS__;
52+
$this->pipe = $pipe;
53+
$this->responsePrototype = $responsePrototype;
54+
55+
$this->setEventManager($eventManager);
56+
$this->setEvent($event);
57+
}
58+
59+
/**
60+
* {@inheritDoc}
61+
*
62+
* @throws RuntimeException
63+
*/
64+
public function onDispatch(MvcEvent $e)
65+
{
66+
$routeMatch = $e->getRouteMatch();
67+
$psr7Request = $this->populateRequestParametersFromRoute(
68+
$this->loadRequest()->withAttribute(RouteMatch::class, $routeMatch),
69+
$routeMatch
70+
);
71+
72+
$result = $this->pipe->process($psr7Request, new CallableDelegateDecorator(
73+
function () {
74+
throw ReachedFinalHandlerException::create();
75+
},
76+
$this->responsePrototype
77+
));
78+
79+
$e->setResult($result);
80+
81+
return $result;
82+
}
83+
84+
/**
85+
* @return \Zend\Diactoros\ServerRequest
86+
*
87+
* @throws RuntimeException
88+
*/
89+
private function loadRequest()
90+
{
91+
$request = $this->request;
92+
93+
if (! $request instanceof Request) {
94+
throw new RuntimeException(sprintf(
95+
'Expected request to be a %s, %s given',
96+
Request::class,
97+
get_class($request)
98+
));
99+
}
100+
101+
return Psr7ServerRequest::fromZend($request);
102+
}
103+
104+
/**
105+
* @param ServerRequestInterface $request
106+
* @param RouteMatch|null $routeMatch
107+
*
108+
* @return ServerRequestInterface
109+
*/
110+
private function populateRequestParametersFromRoute(ServerRequestInterface $request, RouteMatch $routeMatch = null)
111+
{
112+
if (! $routeMatch) {
113+
return $request;
114+
}
115+
116+
foreach ($routeMatch->getParams() as $key => $value) {
117+
$request = $request->withAttribute($key, $value);
118+
}
119+
120+
return $request;
121+
}
122+
}

src/DispatchListener.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ public function attach(EventManagerInterface $events, $priority = 1)
7676
*/
7777
public function onDispatch(MvcEvent $e)
7878
{
79+
if (null !== $e->getResult()) {
80+
return;
81+
}
82+
7983
$routeMatch = $e->getRouteMatch();
8084
$controllerName = $routeMatch instanceof RouteMatch
8185
? $routeMatch->getParam('controller', 'not-found')

src/MiddlewareListener.php

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
use Zend\EventManager\EventManagerInterface;
1919
use Zend\Mvc\Exception\InvalidMiddlewareException;
2020
use Zend\Mvc\Exception\ReachedFinalHandlerException;
21-
use Zend\Psr7Bridge\Psr7ServerRequest as Psr7Request;
21+
use Zend\Mvc\Controller\MiddlewareController;
2222
use Zend\Psr7Bridge\Psr7Response;
2323
use Zend\Router\RouteMatch;
2424
use Zend\Stratigility\Delegate\CallableDelegateDecorator;
@@ -45,6 +45,10 @@ public function attach(EventManagerInterface $events, $priority = 1)
4545
*/
4646
public function onDispatch(MvcEvent $event)
4747
{
48+
if (null !== $event->getResult()) {
49+
return;
50+
}
51+
4852
$routeMatch = $event->getRouteMatch();
4953
$middleware = $routeMatch->getParam('middleware', false);
5054
if (false === $middleware) {
@@ -78,16 +82,12 @@ public function onDispatch(MvcEvent $event)
7882

7983
$caughtException = null;
8084
try {
81-
$psr7Request = Psr7Request::fromZend($request)->withAttribute(RouteMatch::class, $routeMatch);
82-
foreach ($routeMatch->getParams() as $key => $value) {
83-
$psr7Request = $psr7Request->withAttribute($key, $value);
84-
}
85-
$return = $pipe->process($psr7Request, new CallableDelegateDecorator(
86-
function (PsrServerRequestInterface $request, PsrResponseInterface $response) {
87-
throw ReachedFinalHandlerException::create();
88-
},
89-
$psr7ResponsePrototype
90-
));
85+
$return = (new MiddlewareController(
86+
$pipe,
87+
$psr7ResponsePrototype,
88+
$application->getServiceManager()->get('EventManager'),
89+
$event
90+
))->dispatch($request, $response);
9191
} catch (\Throwable $ex) {
9292
$caughtException = $ex;
9393
} catch (\Exception $ex) { // @TODO clean up once PHP 7 requirement is enforced
@@ -107,6 +107,8 @@ function (PsrServerRequestInterface $request, PsrResponseInterface $response) {
107107
}
108108
}
109109

110+
$event->setError('');
111+
110112
if (! $return instanceof PsrResponseInterface) {
111113
$event->setResult($return);
112114
return $return;
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
<?php
2+
/**
3+
* Zend Framework (http://framework.zend.com/)
4+
*
5+
* @link http://github.com/zendframework/zf2 for the canonical source repository
6+
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
7+
* @license http://framework.zend.com/license/new-bsd New BSD License
8+
*/
9+
10+
namespace ZendTest\Mvc\Controller;
11+
12+
use PHPUnit\Framework\TestCase;
13+
use Psr\Http\Message\ResponseInterface;
14+
use Zend\EventManager\EventManager;
15+
use Zend\EventManager\EventManagerInterface;
16+
use Zend\Http\Request;
17+
use Zend\Http\Response;
18+
use Zend\Mvc\Controller\AbstractController;
19+
use Zend\Mvc\Controller\MiddlewareController;
20+
use Zend\Mvc\Exception\RuntimeException;
21+
use Zend\Mvc\MvcEvent;
22+
use Zend\Stdlib\DispatchableInterface;
23+
use Zend\Stdlib\RequestInterface;
24+
use Zend\Stratigility\MiddlewarePipe;
25+
26+
/**
27+
* @covers \Zend\Mvc\Controller\MiddlewareController
28+
*/
29+
class MiddlewareControllerTest extends TestCase
30+
{
31+
/**
32+
* @var MiddlewarePipe|\PHPUnit_Framework_MockObject_MockObject
33+
*/
34+
private $pipe;
35+
36+
/**
37+
* @var ResponseInterface|\PHPUnit_Framework_MockObject_MockObject
38+
*/
39+
private $responsePrototype;
40+
41+
/**
42+
* @var EventManagerInterface
43+
*/
44+
private $eventManager;
45+
46+
/**
47+
* @var AbstractController|\PHPUnit_Framework_MockObject_MockObject
48+
*/
49+
private $controller;
50+
51+
/**
52+
* @var MvcEvent
53+
*/
54+
private $event;
55+
56+
/**
57+
* {@inheritDoc}
58+
*/
59+
protected function setUp()
60+
{
61+
$this->pipe = $this->createMock(MiddlewarePipe::class);
62+
$this->responsePrototype = $this->createMock(ResponseInterface::class);
63+
$this->eventManager = $this->createMock(EventManagerInterface::class);
64+
$this->event = new MvcEvent();
65+
$this->eventManager = new EventManager();
66+
67+
$this->controller = new MiddlewareController(
68+
$this->pipe,
69+
$this->responsePrototype,
70+
$this->eventManager,
71+
$this->event
72+
);
73+
}
74+
75+
public function testWillAssignCorrectEventManagerIdentifiers()
76+
{
77+
$identifiers = $this->eventManager->getIdentifiers();
78+
79+
self::assertContains(MiddlewareController::class, $identifiers);
80+
self::assertContains(AbstractController::class, $identifiers);
81+
self::assertContains(DispatchableInterface::class, $identifiers);
82+
}
83+
84+
public function testWillDispatchARequestAndResponseWithAGivenPipe()
85+
{
86+
$request = new Request();
87+
$response = new Response();
88+
$result = $this->createMock(ResponseInterface::class);
89+
/* @var $dispatchListener callable|\PHPUnit_Framework_MockObject_MockObject */
90+
$dispatchListener = $this->getMockBuilder(\stdClass::class)->setMethods(['__invoke'])->getMock();
91+
92+
$this->eventManager->attach(MvcEvent::EVENT_DISPATCH, $dispatchListener, 100);
93+
$this->eventManager->attach(MvcEvent::EVENT_DISPATCH_ERROR, function () {
94+
self::fail('No dispatch error expected');
95+
}, 100);
96+
97+
$dispatchListener
98+
->expects(self::once())
99+
->method('__invoke')
100+
->with(self::callback(function (MvcEvent $event) use ($request, $response) {
101+
self::assertSame($this->event, $event);
102+
self::assertSame(MvcEvent::EVENT_DISPATCH, $event->getName());
103+
self::assertSame($this->controller, $event->getTarget());
104+
self::assertSame($request, $event->getRequest());
105+
self::assertSame($response, $event->getResponse());
106+
107+
return true;
108+
}));
109+
110+
$this->pipe->expects(self::once())->method('process')->willReturn($result);
111+
112+
$controllerResult = $this->controller->dispatch($request, $response);
113+
114+
self::assertSame($result, $controllerResult);
115+
self::assertSame($result, $this->event->getResult());
116+
}
117+
118+
public function testWillRefuseDispatchingInvalidRequestTypes()
119+
{
120+
/* @var $request RequestInterface */
121+
$request = $this->createMock(RequestInterface::class);
122+
$response = new Response();
123+
/* @var $dispatchListener callable|\PHPUnit_Framework_MockObject_MockObject */
124+
$dispatchListener = $this->getMockBuilder(\stdClass::class)->setMethods(['__invoke'])->getMock();
125+
126+
$this->eventManager->attach(MvcEvent::EVENT_DISPATCH, $dispatchListener, 100);
127+
128+
$dispatchListener
129+
->expects(self::once())
130+
->method('__invoke')
131+
->with(self::callback(function (MvcEvent $event) use ($request, $response) {
132+
self::assertSame($this->event, $event);
133+
self::assertSame(MvcEvent::EVENT_DISPATCH, $event->getName());
134+
self::assertSame($this->controller, $event->getTarget());
135+
self::assertSame($request, $event->getRequest());
136+
self::assertSame($response, $event->getResponse());
137+
138+
return true;
139+
}));
140+
141+
$this->pipe->expects(self::never())->method('process');
142+
143+
$this->expectException(RuntimeException::class);
144+
145+
$this->controller->dispatch($request, $response);
146+
}
147+
}

test/DispatchListenerTest.php

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
use Zend\Mvc\MvcEvent;
2020
use Zend\Router\RouteMatch;
2121
use Zend\ServiceManager\ServiceManager;
22+
use Zend\Stdlib\ResponseInterface;
23+
use Zend\View\Model\ModelInterface;
2224

2325
class DispatchListenerTest extends TestCase
2426
{
@@ -83,4 +85,49 @@ public function testUnlocatableControllerViaAbstractFactory()
8385
$this->assertArrayHasKey('error', $log);
8486
$this->assertSame('error-controller-not-found', $log['error']);
8587
}
88+
89+
/**
90+
* @dataProvider alreadySetMvcEventResultProvider
91+
*
92+
* @param mixed $alreadySetResult
93+
*/
94+
public function testWillNotDispatchWhenAnMvcEventResultIsAlreadySet($alreadySetResult)
95+
{
96+
$event = $this->createMvcEvent('path');
97+
98+
$event->setResult($alreadySetResult);
99+
100+
$listener = new DispatchListener(new ControllerManager(new ServiceManager(), ['abstract_factories' => [
101+
Controller\TestAsset\UnlocatableControllerLoaderAbstractFactory::class,
102+
]]));
103+
104+
$event->getApplication()->getEventManager()->attach(MvcEvent::EVENT_DISPATCH_ERROR, function () {
105+
self::fail('No dispatch failures should be raised - dispatch should be skipped');
106+
});
107+
108+
$listener->onDispatch($event);
109+
110+
self::assertSame($alreadySetResult, $event->getResult(), 'The event result was not replaced');
111+
}
112+
113+
/**
114+
* @return mixed[][]
115+
*/
116+
public function alreadySetMvcEventResultProvider()
117+
{
118+
return [
119+
[123],
120+
[true],
121+
[false],
122+
[[]],
123+
[new \stdClass()],
124+
[$this],
125+
[$this->createMock(ModelInterface::class)],
126+
[$this->createMock(ResponseInterface::class)],
127+
[$this->createMock(Response::class)],
128+
[['view model data' => 'as an array']],
129+
[['foo' => new \stdClass()]],
130+
['a response string'],
131+
];
132+
}
86133
}

0 commit comments

Comments
 (0)