Currently the sequence of the call stack is starting from the outer most Middleware, through the layers of Middlewares, to the route Controller, then back through the Middlewares from inner most to outer most. Right now, I cannot think of a use case where Middleware can be useful when on the way out. This is because we are writing directly to the output stream along the way and there is no way to alter the output on the way out.
Therefore I want to propose deferred stream writing, i.e. only write after exiting the outer most Middleware, and allow changes to the respond object along the way. as an example, we can use DeferredRendererTrait in Respond class.
trait DeferredRendererTrait
{
public $context;
private $writer;
public function setContext($writer, $context)
{
$this->context = $context;
$this->writer = $writer;
return $this;
}
public function render($request, $response)
{
// must return ResponseInterface
return call_user_func_array($this->writer, [$request, $response, $this->context]);
}
}
Then in MiddlewareAwareTrait
public function callMiddlewareStack(ServerRequestInterface $request, ResponseInterface $response)
{
...
$start = $this->stack->top();
$this->middlewareLock = true;
$response = $start($request, $response);
$this->middlewareLock = false;
// add this line to render the deferred context
$response->render($request, $response);
return $response;
}
In this way, we can modify DeferredRendererTrait::context any where in the call stack. In the controller class, we can also call another route handler from the current one to generate the context and then modify the context in the current handler. For example:
class Controller
{
public getEditForm($request, $response)
{
$context = ... // prepare context data, fetching from DB or otherwise
return $response->setContext(function($request, $response, $context) {
// defer twig view compiling until the end to give middleware
// and other callers a chance to modify the context
$this-view->render($response, 'edit.twig', $context);
}, $context);
}
public postEditForm($request, $response)
{
... // validate inputs
if(!empty($error))
{
// automatically prepare context for edit form
$response = $this->getEditForm();
// modify context to include errors
$response->context['error'] = $error;
// modify context to populate form with old inputs
// instead of using DB data
$response->context['form'] = $request->getParams();
// finally, just return the response
return $response;
}
else
{
return $response->withRedirect(...);
}
}
}