I've re-read everything that was discussed in my last issue #46 and I realise that having an example of an alternative that fixes my myriad of concerns over swapping one flawed implementation for another (no offence meant), is preferable over me just offloading my confusing concerns.
I can clearly see my previous issue asked one question, then rambled about other aspects of my concerns, so in an attempt to separate concerns (see what I did there?) I will focus on a single response to this proposal.
I can see why a single pass approach is preferable to the double pass, but in my mind, they are still both flawed and violate the single responsibility principle and also the interface segregation principle.
In both cases, the middleware is allowed to know too much about the entire process of accepting a request, and expecting a response; whether that be the double pass having a pre baked response instance, or the single pass being told about the 'next' item in the chain.
A Middleware should only care about what it is supposed to do in the simplest way possible, enforcing the SRP.
I am proposing that we really look at what the requirement is here, and come up with something that fits the bill properly, rather than rehash previous principles that smell bad. Here is something I started to touch on in my issue, and that @schnittstabil has also touched on.
<?php namespace Psr\Http\Middleware; use Psr\Http\Message\ResponseInterface; interface MiddlewareInterface { }
We need a base interface that can allow type hinting a single point of adding middleware to a queue for example.
<?php namespace Psr\Http\Middleware; use Psr\Http\Message\ResponseInterface; interface ResponseFactoryInterface { public function createResponseInstance(): ResponseInterface; }
We need an interface to allow implementers access to a factory method to provide whatever implementation of a ResponseInterface
is desired for that Middleware
.
<?php namespace Psr\Http\Middleware; use Psr\Http\Message\MessageInterface; use Psr\Http\Message\RequestInterface; interface RequestMiddlewareInterface extends MiddlewareInterface { public function parseRequest(RequestInterface $request): MessageInterface; }
We have an interface that provides Middleware for the inbound flow of a stack. Also note that to allow the Middleware stack to be short circuited, we only type hint a response to be MessageInterface
. This means, normal behaviour for the stack is to proceed on a returned instance of RequestInterface
, and if an instance of ResponseInterface
is given, stop the inbound run of the stack.
<?php namespace Psr\Http\Middleware; use Psr\Http\Message\ResponseInterface; interface ResponseMiddlewareInterface extends MiddlewareInterface { public function parseResponse(ResponseInterface $response): ResponseInterface; }
We have an interface that provides middleware for outbound flow of the stack. This allows us to have middleware that can, for example, change the cache headers of the response based on business rules.
<?php namespace Psr\Http; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; interface ExchangeInterface { public function exchange(RequestInterface $request): ResponseInterface; }
We should also be opinionated on how a request is exchanged with a response, even if only to say "Hey I give you a request, you give me a response!". This would lie a level above in the namespace of course, being that it is not specific to messages or middleware.
I can picture faces of interest and faces of concern over this idea. Let me explain some more detail.
We are violating the SRP and ISP in both implementations thus far, and we need to reimagine this as above to prevent that violation. Let me write the middleware use cases I've seen so far:
These are fundamentally three separate responsibilities, and must be separated. There is no valid argument against this, sorry to annoy anyone there, but that's the truth. To fix this from both current implementations, we segregate the interfaces, therefore separating the concerns.
Add to the above the fact we let the Middleware itself choose whether to continue the stack or not, by providing a callback in the form of 'next', we have a messy situation that needs cleaning up.
Why do we allow a Middleware to handle both inbound and outbound flow in one function, and also pass in a hook to the outside world? That hook, if implemented badly could allow a Middleware to cause all sorts of havoc by accessing things it should never know about!
No, let us be strict here and not let the Middleware know anything about the outside world, other than the request/response (dependent on interface) and it's single responsibility.
There are the obvious ones being separation of concern and interface segregation, I think I've covered those.
MessageInterface
instance.Also, please take note that I have specifically not used ServerRequestInterface
. There is no reason at all that this implementation would not function in a client middleware environment also. Why repeat things by waiting for a client middleware proposal when one will do?
As I've said before, this is no insult to anyone who has put work in so far, I am just coming at this from a fresh perspective. Please discuss this as I fear for this being finalised as another incorrect design that will be around for years to come; I never liked the double pass, and I don't like the single pass either. Something better is needed.
A class should have only one reason to change.
The interface-segregation principle (ISP) states that no client should be forced to depend on methods it does not use. [Wikipedia]
SRP says only that a `car` class should not implement both responsibilities by itself!
Should some of your proposed interfaces be added to PSR-15?
Hi Michael,Thanks for your response. I have read about, and personally discussed the SOLID principles with Uncle Bob, and I do believe that there is more than one final interpretation of those principles. I can see your points, but raise you this:SRP says only that a `car` class should not implement both responsibilities by itself!Currently, the middleware proposal and current used implementation ask for exactly that. That the middleware be responsible for the inbound and outbound flow. Even if it is for the middleware to decide that for itself, it is still a breach of SRP allowing a class to decide that. My point is the choice should not be there in the first place, by way of defined interfaces.
My point about ISP is to follow the definition you stated. If we take away the choice of multiple responsibilities from the middleware, then we must separate the interfaces. And I do know that it is not about PHP interfaces, but that is what they are for, along with the same in JAVA, and class abstractions; they are used to define the contract between classes: Can this middleware affect the request? Can this middleware affect the response? That kind of question comes from the interface the class implements.
the one fundamental thing I want to help try and avoid is having one single function in a Middleware instance be in control (so to speak) of the middleware process.
the stack should handle iteration and short-circuits
(RequestInterface -> ResponseInterface) -> (RequestInterface -> ResponseInterface)
interface ExchangeInterface
{
public function __invoke(RequestInterface $request): ResponseInterface;
}
interface MiddlewareInterface
{
public function __invoke(ExchangeInterface $next): ExchangeInterface;
}
class UnicornMiddleware implements MiddlewareInterface
{
public function __invoke(ExchangeInterface $next): ExchangeInterface {
return new class($next) implements ExchangeInterface {
private $next;
public function __construct($next)
{
$this->next = $next;
}
public function __invoke(RequestInterface $request): ResponseInterface
{
return (($this->next)($request))->withHeader('X-PoweredBy', 'Unicorns');
}
};
}
}
(RequestInterface -> ResponseInterface) -> (RequestInterface -> ResponseInterface)
// using curry:
(RequestInterface -> ResponseInterface) -> RequestInterface -> ResponseInterface
// and again:
((RequestInterface -> ResponseInterface) -> RequestInterface) -> ResponseInterface
interface ExchangeInterface
{
public function __invoke(RequestInterface $request): ResponseInterface;
}
interface MiddlewareInterface
{
public function __invoke(ExchangeInterface $next, RequestInterface $request);}
class UnicornMiddleware implements ServerMiddlewareInterface
{
public function process(ServerRequestInterface $request, DelegateInterface $delegate) {
return $delegate->process($request)->withHeader('X-PoweredBy', 'Unicorns');
}
}
--
You received this message because you are subscribed to a topic in the Google Groups "PHP Framework Interoperability Group" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/php-fig/vCKxyraoqnc/unsubscribe.
To unsubscribe from this group and all its topics, send an email to php-fig+u...@googlegroups.com.
To post to this group, send email to php...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/php-fig/b0794cd8-089a-4753-bbf5-9a3d0e833b06%40googlegroups.com.
For more options, visit https://groups.google.com/d/optout.
You received this message because you are subscribed to the Google Groups "PHP Framework Interoperability Group" group.
To unsubscribe from this group and stop receiving emails from it, send an email to php-fig+u...@googlegroups.com.
To post to this group, send email to php...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/php-fig/etPan.58ac8529.344f83e6.2227%40designermonkey.co.uk.
To view this discussion on the web visit https://groups.google.com/d/msgid/php-fig/8f66492b-db0c-8017-f758-0f14fdce90fd%40garfieldtech.com.
<?php
namespace Manneken\Http\Middleware;
use Ds\Stack as DsStack;
use Psr\Http\Message\MessageInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Interop\Http\Factory\ResponseFactoryInterface as InteropFactory;
use Manneken\Http\ExchangeInterface;
use Manneken\Http\ResponseFactoryInterface;
class Stack implements ExchangeInterface, ResponseFactoryInterface
{
private $stack;
private $factory;
public function __construct(DsStack $stack, InteropFactory $factory)
{
$this->stack = $stack;
$this->factory = $factory;
}
public function addMiddleware(MiddlewareInterface $middleware)
{
$this->stack->push($middleware);
}
public function exchange(RequestInterface $request): ResponseInterface
{
$message = $this->iterateOverInboundStackWithMessage(
$this->getInboundStackFromStack(),
$request
);
if (is_a($message, ResponseInterface::class)) {
return $message;
}
return $this->iterateOverOutboundStackWithMessage(
$this->getOutboundStackFromStack(),
$this->createResponseInstance()
);
}
public function createResponseInstance(): ResponseInterface
{
return $this->factory->createResponse();
}
private function getInboundStackFromStack(): array
{
return array_filter($this->stack->toArray(), function ($middleware) {
return is_a($middleware, RequestMiddlewareInterface::class);
});
}
private function iterateOverInboundStackWithMessage(array $stack, MessageInterface $message): MessageInterface
{
foreach ($stack as $middleware) {
$message = $middleware->processRequest($message);
if (is_a($message, ResponseInterface::class)) {
return $message;
}
}
return $message;
}
private function getOutboundStackFromStack(): array
{
return array_reverse(array_filter($this->stack->toArray(), function ($middleware) {
return is_a($middleware, ResponseMiddlewareInterface::class);
}));
}
private function iterateOverOutboundStackWithMessage(array $stack, MessageInterface $message): MessageInterface
{
foreach ($stack as $middleware) {
$message = $middleware->processRequest($message);
}
return $message;
}
}
<?php
namespace Example;
use Psr\Http\Message\MessageInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Middleware\RequestMiddlewareInterface;
use Psr\Http\Middleware\ResponseFactoryInterface;
class AuthHeaderMiddleware implements RequestMiddlewareInterface, ResponseFactoryInterface
{
public function parseRequest(RequestInterface $request): MessageInterface
{
if ($request->getMethod() === 'POST' && $request->hasHeader('authorization')) {
return $request; // Passthrough here
}
$response = $this->createResponseInstance();
return $response->withStatus(401); // short circuit handled by middleware runner implementing ExchangeInterface
}
}
--
You received this message because you are subscribed to a topic in the Google Groups "PHP Framework Interoperability Group" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/php-fig/vCKxyraoqnc/unsubscribe.
To unsubscribe from this group and all its topics, send an email to php-fig+u...@googlegroups.com.
To post to this group, send email to php...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/php-fig/227f5190-8449-4546-a5c0-1d88f4340edc%40googlegroups.com.
Can you explain what you mean by prototype related issues?
Where would the computation time be added, to a request or response? Can you show me how you’d achieve it with the double pass method, and I can show you the alternative.
$dispatcher = new Dispatcher([
new ResponseTimeMiddleware(),
new AuthMiddleware(),
…
]);
$response = $dispatcher->dispatch(new ServerRequest());
<?php
namespace Example;
use Psr\Http\Message\MessageInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Middleware\RequestMiddlewareInterface;
use Psr\Http\Middleware\ResponseMiddlewareInterface;
class AuthHeaderMiddleware implements RequestMiddlewareInterface, ResponseMiddlewareInterface
{
const HEADER = 'X-Response-Time';
private $startTime;
public function parseRequest(RequestInterface $request): MessageInterface
{
$server = $request->getServerParams();
$this->startTime = isset($server['REQUEST_TIME_FLOAT']) ? $server['REQUEST_TIME_FLOAT'] : microtime(true);
}
public function parseResponse(ResponseInterface $response): ResponseInterface
{
return $response->withHeader(self::HEADER, sprintf('%2.3fms', (microtime(true) - $this->startTime) * 1000));
}
}
John, would you mind pushing your interfaces and Stack to github? Thus, everybody interested can play with your code, which probably would reduce mails here.
--
You received this message because you are subscribed to the Google Groups "PHP Framework Interoperability Group" group.
To unsubscribe from this group and stop receiving emails from it, send an email to php-fig+u...@googlegroups.com.
To post to this group, send email to php...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/php-fig/a2156982-48f3-424f-b14a-4cb81a6e5335%40googlegroups.com.
I am proposing that we really look at what the requirement is here, and come up with something that fits the bill properly, rather than rehash previous principles that smell bad.
[...] `interface MiddlewareInterface {}` [...]
We need a base interface that can allow type hinting a single point of adding middleware to a queue for example.
We are violating the SRP and ISP in both implementations thus far, and we need to reimagine this as above to prevent that violation. Let me write the middleware use cases I've seen so far:
- I want to make a Middleware to handle requests.
- I want to make a Middleware to alter a response.
- I need to make an instance of a response to short circuit the exchange.
class HttpResponsePreparingMiddleware implements MiddlewareInterface
{
public function process(
ServerRequestInterface $request,
DelegateInterface $delegate
): ResponseInterface {
/** Do nothing to the request, hand it on to later middleware. */
/** @var ResponseInterface $response Response received from delegate. */
$response = $delegate->process($request);
/** Check response content is acceptable to recipient. */
if ($request->hasHeader('Accept')) {
$acceptable = $request->getHeader('Accept');
$sending = $response->getHeader('Content-Type');
if(!in_array($sending[0], $acceptable)) {
// `buildErrorResponse()` is not shown here for brevity.
$response = $this->buildErrorResponse(406);
}
}
/** Ensure the HTTP protocol is acceptable to recipient. */
$requestVersion = $request->getProtocolVersion();
if ($response->getProtocolVersion() != $requestVersion) {
$version = ($requestVersion == '1.1') ? '1.1' : '1.0';
$response = $response->withProtocolVersion($version);
}
/** Return a request I know the client will accept. */
return $response;
}
}
These are fundamentally three separate responsibilities, and must be separated. There is no valid argument against this, sorry to annoy anyone there, but that's the truth. To fix this from both current implementations, we segregate the interfaces, therefore separating the concerns.
class ThrowableCatchingMiddleware implements MiddlewareInterface
{
public function process(
ServerRequestInterface $request,
DelegateInterface $delegate
): ResponseInterface {
try {
$this->logger->debug('Request received', ['request' => $request]);
$response = $delegate->process($request);
$this->logger->debug('Response sent', ['response' => $response]);
} catch (Throwable $caught) {
// `buildErrorResponse()` builds a valid HTTP error response for a
// given HTTP error code. If the given code is not a recognised HTTP
// error code, 500 is used.
$response = $this->buildErrorResponse($caught->getCode());
$this->logger->error(
'Exception encountered',
['response' => $response, 'exception' => $caught]
);
return $response;
}
}
}
The stack is not the top level of an application, it is to be used inside an application; the app itself should be responsible for final output whether that be a true response or a handled exception.