DispatchStack calls $next only when the stack runs to the end.

This commit is contained in:
PJ Dietz 2015-05-12 07:54:11 -04:00
parent 26a6a25d3b
commit 297e985e84
3 changed files with 77 additions and 35 deletions

View File

@ -5,6 +5,9 @@ namespace WellRESTed\Dispatching;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
/**
* Dispatches an ordered sequence of middleware.
*/
class DispatchStack implements DispatchStackInterface class DispatchStack implements DispatchStackInterface
{ {
private $stack; private $stack;
@ -22,8 +25,6 @@ class DispatchStack implements DispatchStackInterface
/** /**
* Push a new middleware onto the stack. * Push a new middleware onto the stack.
* *
* This method MUST preserve the order in which middleware added.
*
* @param mixed $middleware Middleware to dispatch in sequence * @param mixed $middleware Middleware to dispatch in sequence
* @return self * @return self
*/ */
@ -36,17 +37,17 @@ class DispatchStack implements DispatchStackInterface
/** /**
* Dispatch the contained middleware in the order in which they were added. * Dispatch the contained middleware in the order in which they were added.
* *
* The first middleware added to the stack MUST be the first to be * The first middleware that was added is dispatched first.
* dispatched.
* *
* Each middleware, when dispatched, MUST receive a $next callable that * Each middleware, when dispatched, receives a $next callable that, when
* dispatches the middleware that follows it. The only exception to this is * called, will dispatch the next middleware in the sequence.
* the last middleware in the stack which much receive a $next callable the
* returns the response unchanged.
* *
* If the instance is dispatched with no middleware added, the instance * When the stack is dispatched empty, or when all middleware in the stack
* MUST call $next passing $request and $response and return the returned * call the $next argument they were passed, this method will call the
* response. * $next it receieved.
*
* When any middleware in the stack returns a response without calling its
* $next, the stack will not call the $next it received.
* *
* @param ServerRequestInterface $request * @param ServerRequestInterface $request
* @param ResponseInterface $response * @param ResponseInterface $response
@ -54,20 +55,16 @@ class DispatchStack implements DispatchStackInterface
* @return ResponseInterface * @return ResponseInterface
*/ */
public function dispatch(ServerRequestInterface $request, ResponseInterface $response, $next) public function dispatch(ServerRequestInterface $request, ResponseInterface $response, $next)
{
$chain = $this->getCallableChain();
$response = $chain($request, $response);
return $next($request, $response);
}
// ------------------------------------------------------------------------
private function getCallableChain()
{ {
$dispatcher = $this->dispatcher; $dispatcher = $this->dispatcher;
// No-op function to use as the final middleware's $mext. // This flag will be set to true when the last middleware calls $next.
$next = function ($request, $response) { $stackCompleted = false;
// The final middleware's $next returns $response unchanged and sets
// the $stackCompleted flag to indicate the stack has completed.
$chain = function ($request, $response) use (&$stackCompleted) {
$stackCompleted = true;
return $response; return $response;
}; };
@ -77,11 +74,17 @@ class DispatchStack implements DispatchStackInterface
// contain a dispatcher, the associated middleware, and a $next // contain a dispatcher, the associated middleware, and a $next
// that is the links to the next middleware in the chain. // that is the links to the next middleware in the chain.
foreach (array_reverse($this->stack) as $middleware) { foreach (array_reverse($this->stack) as $middleware) {
$next = function ($request, $response) use ($dispatcher, $middleware, $next) { $chain = function ($request, $response) use ($dispatcher, $middleware, $chain) {
return $dispatcher->dispatch($middleware, $request, $response, $next); return $dispatcher->dispatch($middleware, $request, $response, $chain);
}; };
} }
return $next; $response = $chain($request, $response);
if ($stackCompleted) {
return $next($request, $response);
} else {
return $response;
}
} }
} }

View File

@ -6,11 +6,16 @@ use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
use WellRESTed\MiddlewareInterface; use WellRESTed\MiddlewareInterface;
/**
* Dispatches an ordered sequence of middleware.
*/
interface DispatchStackInterface extends MiddlewareInterface interface DispatchStackInterface extends MiddlewareInterface
{ {
/** /**
* Push a new middleware onto the stack. * Push a new middleware onto the stack.
* *
* This method MUST preserve the order in which middleware added.
*
* @param mixed $middleware Middleware to dispatch in sequence * @param mixed $middleware Middleware to dispatch in sequence
* @return self * @return self
*/ */
@ -19,16 +24,24 @@ interface DispatchStackInterface extends MiddlewareInterface
/** /**
* Dispatch the contained middleware in the order in which they were added. * Dispatch the contained middleware in the order in which they were added.
* *
* The first middleware added to the stack is the first to be dispatched. * The first middleware added to the stack MUST be dispatched first.
* *
* Each middleware, when dispatched, will receive a $next callable that * Each middleware, when dispatched, MUST receive a $next callable that
* dispatches the middleware that follows it. The only exception to this is * dispatches the middleware that follows it, unless it is the last
* the last middleware in the stack which much receive a $next callable the * middleware. The last middleware MUST receive a $next callable that
* returns the response unchanged. * returns the response unchanged.
* *
* If the instance is dispatched with no middleware added, the instance * When any middleware does not call the $next argument it recieved, the
* MUST call $next passing $request and $response and return the returned * stack instance MUST stop propogating through the stack and MUST return
* response. * the response without calling the $next argument passed to dispatch.
*
* This method MUST call the passed $next argument when:
* - The stack is empty (i.e., there is no middleware to dispatch)
* - Each middleware called the $next that it receieved.
*
* This method MUST NOT call the passed $next argument when the stack is
* not empty and any middleware returns a response without calling the
* $next it receieved.
* *
* @param ServerRequestInterface $request * @param ServerRequestInterface $request
* @param ResponseInterface $response * @param ResponseInterface $response

View File

@ -52,7 +52,6 @@ class DispatchStackTest extends \PHPUnit_Framework_TestCase
/** /**
* @covers ::dispatch * @covers ::dispatch
* @covers ::getCallableChain
*/ */
public function testDispachesMiddlewareInOrderAdded() public function testDispachesMiddlewareInOrderAdded()
{ {
@ -76,6 +75,22 @@ class DispatchStackTest extends \PHPUnit_Framework_TestCase
$this->assertEquals(["first", "second", "third"], $callOrder); $this->assertEquals(["first", "second", "third"], $callOrder);
} }
/**
* @covers ::dispatch
*/
public function testCallsNextAfterDispatchingEmptyStack()
{
$nextCalled = false;
$next = function ($request, $response) use (&$nextCalled) {
$nextCalled = true;
return $response;
};
$stack = new DispatchStack($this->dispatcher->reveal());
$stack->dispatch($this->request->reveal(), $this->response->reveal(), $next);
$this->assertTrue($nextCalled);
}
/** /**
* @covers ::dispatch * @covers ::dispatch
*/ */
@ -103,7 +118,7 @@ class DispatchStackTest extends \PHPUnit_Framework_TestCase
/** /**
* @covers ::dispatch * @covers ::dispatch
*/ */
public function testCallsNextAfterDispatchingEmptyStack() public function testDoesNotCallNextWhenStackStopsEarly()
{ {
$nextCalled = false; $nextCalled = false;
$next = function ($request, $response) use (&$nextCalled) { $next = function ($request, $response) use (&$nextCalled) {
@ -111,8 +126,19 @@ class DispatchStackTest extends \PHPUnit_Framework_TestCase
return $response; return $response;
}; };
$middlewareGo = function ($request, $response, $next) use (&$callOrder) {
return $next($request, $response);
};
$middlewareStop = function ($request, $response, $next) use (&$callOrder) {
return $response;
};
$stack = new DispatchStack($this->dispatcher->reveal()); $stack = new DispatchStack($this->dispatcher->reveal());
$stack->add($middlewareGo);
$stack->add($middlewareStop);
$stack->add($middlewareStop);
$stack->dispatch($this->request->reveal(), $this->response->reveal(), $next); $stack->dispatch($this->request->reveal(), $this->response->reveal(), $next);
$this->assertTrue($nextCalled); $this->assertFalse($nextCalled);
} }
} }