-
Notifications
You must be signed in to change notification settings - Fork 0
PSR‐7 & PSR‐15 Bridge
Pipeline intends to assist you with PSR-15 (or Similar - removed in v2) implementation of Request Handler and Middleware as a Queue-based Request Handler. This approach is expected to work on the following scenario:
- The Request Handler's implementing concrete class has a
Handler::add($middleware)
method (or similar method name) to collect middleware and stack it. - Inside the
Handler::handle()
method, the Pipeline Bridge is used to transform the stacked middleware into pipes. - The Pipeline API can then be initialized. Keep in mind to pass the
$request
to pipeline'sPipeline::use()
method. The subject to send to the pipeline will be an initial response instance. - When a client invokes the handle method, middleware added using the
Handler::add($middleware)
method will process in order (First Come First Process basis) they were stacked/queued. - The handler can directly return the response to the client or capture the response for further manipulation.
The pipeline bridge converts middlewares coming to the Request Handler into pipes.
use Closure;
use Throwable;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Server\MiddlewareInterface;
use TheWebSolver\Codegarage\Lib\Pipeline;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Http\Message\ServerRequestInterface;
use TheWebSolver\Codegarage\Lib\PipeInterface;
use TheWebSolver\Codegarage\Lib\PipelineBridge;
// Application concrete classes.
use Response; // Implements Psr\Http\Message\ResponseInterface
use NotFoundResponse; // Implements Psr\Http\Message\ResponseInterface
class MainHandler implements RequestHandlerInterface {
/** @var PipeInterface[] */
private array $pipes;
/**
* Fallback can be anything. Possible actions are:
* - A lambda function added to the current route.
* - A controller method converted to first-class callable. Eg: `$controller->show(...)`.
*
* @param Closure(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface $fallback
*/
public function __construct(private readonly Closure $fallback) {}
public function add(string|Closure|MiddlewareInterface $middleware): void {
$this->pipes[] = PipelineBridge::middlewareToPipe($middleware);
}
public function handle(ServerRequestInterface $request): ResponseInterface {
$response = (new Pipeline())
->use($request)
->send(new Response())
->through($this->pipes)
->sealWith(static fn(Throwable $e): ResponseInterface => (new NotFoundResponse()))
->thenReturn();
return $response instanceof NotFoundResponse ? $response : ($this->fallback)($request, $response);
}
}
use Closure;
use Throwable;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Http\Message\ServerRequestInterface;
use TheWebSolver\Codegarage\Pipeline\Bridge;
use TheWebSolver\Codegarage\Pipeline\Pipeline;
use TheWebSolver\Codegarage\Pipeline\Middleware;
use TheWebSolver\Codegarage\Pipeline\Interfaces\PipeInterface;
// Application concrete classes.
use Response; // Implements Psr\Http\Message\ResponseInterface
use NotFoundResponse; // Implements Psr\Http\Message\ResponseInterface
class MainHandler implements RequestHandlerInterface {
/** @var PipeInterface[] */
private array $pipes;
/**
* Fallback can be anything. Possible actions are:
* - A lambda function added to the current route.
* - A controller method converted to first-class callable. Eg: `$controller->show(...)`.
*
* @param Closure(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface $fallback
*/
public function __construct(private readonly Closure $fallback) {}
public function add(string|Closure|MiddlewareInterface $middleware): void {
$this->pipes[] = Middleware::toPipe($middleware);
}
public function handle(ServerRequestInterface $request): ResponseInterface {
// Recommended.
try {
$response = (new Bridge())->for($request, new Response())->through(...$this->pipes)->get();
} catch ( Throwable $e ) {
$response = new NotFoundResponse();
}
// Not recommended. Alternatively, using pipeline directly.
$response = (new Pipeline())
->use($request)
->send(new Response())
->through($this->pipes)
->sealWith(static fn(Throwable $e): ResponseInterface => (new NotFoundResponse()))
->thenReturn();
return $response instanceof NotFoundResponse ? $response : ($this->fallback)($request, $response);
}
}
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Http\Message\ServerRequestInterface;
// Application concrete classes.
use AuthMiddleware; // Implements Psr\Http\Server\MiddlewareInterface
use RouteMiddleware; // Implements Psr\Http\Server\MiddlewareInterface
use ServerRequestFactory; // May implement PSR-17 Psr\Http\Message\ServerRequestFactoryInterface
// Fallback Action that accepts pipeline response as the second argument. This implementation
// is just a demonstration and you may bootstrap it in your application-specific way.
$fallback = static fn(
ServerRequestInterface $request,
ResponseInterface $response
): ResponseInterface => $response->withStatus(code: 200);
// The main handler that uses Pipeline Bridge to handle stacked middlewares.
$handler = new MainHandler($fallback);
// Add middlewares as required by your application. Can accept either a Middleware instance.
$handler->add(new AuthMiddleware());
// Or a middleware classname.
$handler->add(RouteMiddleware::class);
// Or a Closure that has the same signature to be compatible with the "MiddlewareInterface::process()" method.
$handler->add(
static fn(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
=> $handler->handle($request)
);
// Get the final response.
$response = $handler->handle(ServerRequestFactory::fromGlobals());
The pipeline bridge follows Queue Based Request Handler design principle. This approach expects the main handler to stack all middleware, pipe it through the pipeline, and get the final response. This behavior can be achieved with a setter method like: Handler::add()
or Handler::addMiddleware()
. This makes the main handler:
- agnostic of the PSR-7 implementation currently being used by the application.
- provide separation of concern between all stacked middlewares as they do not know each other's composition.
- process and get responses in order middlewares were stacked.
- provide better Developer Experience (DX) as well as loose coupling. The developer implements how fallback should work, how exceptions are handled, and what the final response should be.
- friction-less. It does not have a lot of moving parts. Everything is composed, handled, and executed from within the main handler's
::handle()
method.
The Pipe has been designed in a way that bridges between the Pipeline and Middleware processing. The response captured when a pipe is executed is forwarded to the next middleware in the pipeline queue. The double pass approach is implemented where each middleware is assigned an internal Request Handler whose only job is to return that response whenever $handler->handle($request)
is executed inside MiddlewareInterface::process()
method.
It is designed in a similar way as described in Decoration Based Request Handler (see codes where $responsePrototype
and $innerHandler
is created) but works in a different way. In our case, bridging follows below semantics:
-
$responsePrototype
will be the subject that is being sent through various pipes. -
$innerHandler
will be an anonymous class implementingRequestHandlerInterface
that accepts the$responsePrototype
as its constructor arg. - Whenever pipe is executed, internally it invokes
MiddlewareInterface::process()
passing:- the
$request
sent to the pipeline usingPipeline::use()
method, and - the newly instantiated
$innerHandler
whose only job is to return the response, which in our case will be the subject being sent through pipes.
- the
The
Pipeline::use()
optionally accepts a second argument. It must be a concrete classname that implementsRequestHandlerInterface
and can accept the$responsePrototype
via its constructor. This is done so you can bring your own internal Request Handler (whatever the case may be).
See
MockHandler
class inside PsrStubs file that is used for testing purposes, and BridgeTest::testPSRBridge() method to learn how it is implemented.
Removed in v2
Sometimes, your app might be using the Request/Middleware/Response
design system but may not be compatible with PSR-15 implementation. In such cases, Pipeline Bridge also accepts a Middleware Adapter that follows the same design and/or signature. This middleware adapter also accepts a Closure instance that has the same method signature.
Here, incompatible means covariance/contravariance issue, completely different parameter type hinting, and/or return type hinting. But it has the exact same number of parameters:
$request
&$handler
.
In such a case, there is a setter method in Pipeline Bridge:
PipelineBridge::setMiddlewareAdapter(
interface: MiddlewareAdapterInterface::class,
className: MiddlewareAdapter::class
);
See PsrStubs file to learn more & BridgeTest::testPSRBridge() method to learn how it is implemented.
