2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

5分で理解するPSR-15

Posted at

初めに

前略、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()メソッドはServerRequestInterfaceRequestHandlerInterfaceを引数に持ちます。
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の例です。

Middlewareを使わない場合の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

そこで、前処理と後処理をミドルウェアで共通化することで解決を図ります。

例として、以下のようなミドルウェアを作成しました。

RequestHandlerの前処理と後処理をMiddlewareで部品化してみる
<?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にこれらの処理を記述する必要がなくなります。
RequestHandlerhandle()メソッドを呼んでいる理由は後述します)


では、このMiddlewareはどこから呼び出されるのでしょうか。
前述で、ミドルウェアを呼び出す役割はRequestHandlerが担っていると説明しました。
そこで、先ほどのRequestHandlerhandle()メソッドに、ミドルウェアを呼び出す処理を追記します。

Middlewareを使う場合のRequestHandler
<?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を呼んでいる理由

後述すると説明した、「ミドルウェアの中でRequestHandlerhandle()メソッドを呼んでいる理由」について説明します。
一言で言うと、次のミドルウェアを呼び出すためになります。

ミドルウェアはあくまで部品であり、次にどのミドルウェアを呼び出すかは知るべきではありません。そこで、呼び出すミドルウェアの種類や順番はRequestHandlerで定義しておき、handle()メソッドを介すことで次のミドルウェアを呼び出すという作りになっています。
こうすることで、ミドルウェアをモジュールとして使うことができるようになるというわけです。

処理の流れのまとめ

流れを整理すると

  1. RequestHandlermiddlewareListに、呼び出したいミドルウェアをリスト化しておく
  2. RequestHandlerhandle()メソッドを呼び出す
  3. middlewareListの最初のミドルウェアのprocess()メソッドが呼び出される
    3-1. 呼び出されたミドルウェアをmiddlewareListから削除する
  4. ミドルウェアのprocess()メソッドを実行し、次のミドルウェアを呼ぶためにRequestHandlerhandle()メソッドを呼び出す
  5. ミドルウェアリストが空になるまで3と4を繰り返し、最終的にControllerや他の処理を呼び出す
  6. 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によっては独自のミドルウェアを作成することもできると思いますので、その際は使ってみてください。

ここまでご覧いただきありがとうございました!

参考

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?