初めに
前略、PSR-15について調べたのでまとめます。
PSRとは
PSRとは、PHPにおける標準的なインターフェース仕様を定めたガイドラインです。これは、PHP-FIG(PHP Framework Interoperability Group)という団体によって策定されています。
PSR-15とは
PSR-15は、Web APIやWebアプリケーションのリクエストとレスポンス処理を共通化・標準化するためのガイドラインです。
https://www.php-fig.org/psr/psr-15/
例えば、HTTP通信を行う場合、全てのAPIのレスポンスにHTTPの形式に合わせてヘッダーなどを付与する必要があるかと思いますが、フレームワーク(FW)を利用している際に、実装者がController内でHTTP形式に変換する処理を書く機会は少ないと思います。これは、FWが「レスポンスをHTTP形式に変換する」という共通の処理を部品として実装しているためです。
このような共通処理の設計に関するガイドラインを定めたのが「PSR-15」になります。
普段はFW内部に内包されている部分なのであまり意識する機会は少ないですが、PSR-15を理解することで、シンプルなフレームワークの作成も視野に入るかもしれません。
PSR-15を構成する二つのインターフェース
PSR-15では以下の二つのインターフェースが定められています。
MiddlewareInterface
RequestHandlerInterface
MiddlewareInterface
共通化・標準化する処理を定義するためのInterfaceです。
共通化する処理は、このInterfaceを実装したクラスのprocess()
メソッドに定義します。
また、このInterfaceを実装して作成された各クラスは「ミドルウェア」と呼ばれます。
<?php
namespace Psr\Http\Server;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
/**
* Participant in processing a server request and response.
*
* An HTTP middleware component participates in processing an HTTP message:
* by acting on the request, generating the response, or forwarding the
* request to a subsequent middleware and possibly acting on its response.
*/
interface MiddlewareInterface
{
/**
* Process an incoming server request.
*
* Processes an incoming server request in order to produce a response.
* If unable to produce the response itself, it may delegate to the provided
* request handler to do so.
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface;
}
process()
メソッドはServerRequestInterface
とRequestHandlerInterface
を引数に持ちます。
ServerRequestInterface
は、リクエストの情報を持つインターフェースです。RequestHandlerInterface
の役割やそれを引数で受け取る理由は後述します。
RequestHandlerInterfaceとは
リクエストを受け取り、レスポンスを返すため責務を持つInterfaceです。
次のミドルウェアを呼び出す責務を持っています(詳しくは後述)。
<?php
namespace Psr\Http\Server;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
/**
* Handles a server request and produces a response.
*
* An HTTP request handler process an HTTP request in order to produce an
* HTTP response.
*/
interface RequestHandlerInterface
{
/**
* Handles a request and produces a response.
*
* May call other collaborating code to generate the response.
*/
public function handle(ServerRequestInterface $request): ResponseInterface;
}
二つのインターフェースの関係性
それでは、この二つの関係性について説明します。
具体的に実装クラスを書きながみていきましょう。
Middlewareを使わない場合のRequestHandler
前述の通り、リクエストハンドラーの責務は「リクエストを受け取り、レスポンスを返す」ことです。
以下は、リクエストを受け取ってController
を呼び出し、レスポンスを返却するだけのシンプルなRequestHandler
の例です。
<?php
class RequestHandler implements RequestHandlerInterface
{
protected $controllerDispatcher;
public function handle(ServerRequestInterface $actionRequest): ResponseInterface
{
// 前処理
$actionRequest = $this->processRequest($request);
// Controller呼び出し
$actionResponse = new ActionResponse();
$this->controllerDispatcher->dispatch($actionRequest, $actionResponse);
// 後処理
$actionResponse = $this->processResponse($actionResponse)
return $actionResponse->buildHttpResponse();
}
}
例ではController
呼び出しの前後でリクエストに対する前処理とレスポンスに対する後処理を入れています。これにより各Controller
で書くべき処理を少なくしているのですが、前処理や後処理が大きすぎる場合、コードが巨大になって読みづらくなってしまいます。
Middlewareを使う場合のRequestHandler
そこで、前処理と後処理をミドルウェアで共通化することで解決を図ります。
例として、以下のようなミドルウェアを作成しました。
<?php
class ExampleMiddleware implements MiddlewareInterface
{
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
// 前処理
$request = $this->processRequest($request);
// RequestHandler経由で次のミドルウェアを呼び出す
$response = $handler->handle($request);
// 後処理
$response = $this->processResponse($response)
return $response;
}
}
上記のように、前処理と後処理をミドルウェアに分離することで、RequestHandler
にこれらの処理を記述する必要がなくなります。
(RequestHandler
のhandle()
メソッドを呼んでいる理由は後述します)
では、このMiddlewareはどこから呼び出されるのでしょうか。
前述で、ミドルウェアを呼び出す役割はRequestHandler
が担っていると説明しました。
そこで、先ほどのRequestHandler
のhandle()
メソッドに、ミドルウェアを呼び出す処理を追記します。
<?php
class RequestHandler implements RequestHandlerInterface
{
protected $controllerDispatcher;
// ポイント1:呼び出したいミドルウェアをリスト化しておく
protected $middlewareList;
public function handle(ServerRequestInterface $actionRequest): ResponseInterface
{
if (count($this->middlewareList) > 0) {
// ポイント3:array_shiftにより、ミドルウェアをリストから取り出しつつ削除
$middleware = array_shift($this->middlewareList);
// ポイント2:ミドルウェアを呼び出す
return $middleware->process($request, $this);
}
// ポイント3:ミドルウェアを呼び出しつくした場合、Controllerを呼び出しす
$actionResponse = new ActionResponse();
$this->controllerDispatcher->dispatch($actionRequest, $actionResponse);
return $actionResponse->buildHttpResponse();
}
}
ポイントは以下です
- 呼び出したいミドルウェアをリスト化しておく
-
handle()
メソッドが呼び出された際、ミドルウェアリストの最初のミドルウェアを呼び出す - 呼び出されたミドルウェアはリストから削除される(
array_shift
) - すべてのミドルウェアを呼び出し終わったら
Controller
を呼び出す
ミドルウェアの中でRequestHandler
を呼んでいる理由
後述すると説明した、「ミドルウェアの中でRequestHandler
のhandle()
メソッドを呼んでいる理由」について説明します。
一言で言うと、次のミドルウェアを呼び出すためになります。
ミドルウェアはあくまで部品であり、次にどのミドルウェアを呼び出すかは知るべきではありません。そこで、呼び出すミドルウェアの種類や順番はRequestHandler
で定義しておき、handle()
メソッドを介すことで次のミドルウェアを呼び出すという作りになっています。
こうすることで、ミドルウェアをモジュールとして使うことができるようになるというわけです。
処理の流れのまとめ
流れを整理すると
-
RequestHandler
のmiddlewareList
に、呼び出したいミドルウェアをリスト化しておく -
RequestHandler
のhandle()
メソッドを呼び出す -
middlewareList
の最初のミドルウェアのprocess()
メソッドが呼び出される
3-1. 呼び出されたミドルウェアをmiddlewareList
から削除する - ミドルウェアの
process()
メソッドを実行し、次のミドルウェアを呼ぶためにRequestHandler
のhandle()
メソッドを呼び出す - ミドルウェアリストが空になるまで3と4を繰り返し、最終的にControllerや他の処理を呼び出す
-
Controller
から返ってきたレスポンスを、ミドルウェアが順番に処理する(後処理)
ミドルウェアの例
最後にミドルウェアの例をいくつか見てみましょう。
例1:早期リターン
前処理で、早期リターンをするミドルウェアの例です。
<?php
class EarlyReturnMiddleware implements MiddlewareInterface
{
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
if ($this->earlyReturn()) {
// ある条件の時だけ、次のミドルウェアを呼ばずに早期リターン
$response = new Response();
$response->getBody()->write('Early return');
return $response;
}
return $handler->handle($request);
}
}
例2:レスポンスにヘッダーを付与
レスポンスにヘッダーを付与する後処理を行うミドルウェアの例です。
<?php
class AttachResponseHeaderMiddleware implements MiddlewareInterface
{
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$response = $handler->handle($request);
// 後処理でレスポンスにヘッダを付与
$response = $response->withHeader('X-Neos-Test', 'Hello World');
return $response;
}
}
終わりに
PSR-15は、FWに内包されているためあまり意識する機会は少ないかもしれません。
自分の使ってるFWがどんな実装になっているのか気になった際は是非見てみてください。
また、FWによっては独自のミドルウェアを作成することもできると思いますので、その際は使ってみてください。
ここまでご覧いただきありがとうございました!
参考