PHP Slim4の解読、ミドルウェア編

Slim4のミドルウェアについてかなり深くまで掘り下げていきます。
URLはhttps://your-domain/slim-publicをルートとして進めます。

まずはミドルウェア

use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Slim\Factory\AppFactory;

$app = AppFactory::create();
$app->setBasePath('/slim-public');

// RoutingMiddlewareの追加
$app->addRoutingMiddleware();
$app->addErrorMiddleware(true, true, true);

// ミドルウェア作成
function createMiddleware(string $message){
    return new class($message) implements MiddlewareInterface{
        public function __construct(private string $message) { }
        public function process(Request $request, RequestHandlerInterface $handler): Response
        {
            $response = $handler->handle($request);
            $response->getBody()->write(" $this->message");
            return $response;
        }
    };
}

// 3つミドルウェアを追加。
$app->add(createMiddleware('A'));
$app->add(createMiddleware('B'));
$app->add(createMiddleware('C'));

$app->get('/hello',  function(Request $request, Response $response, array $arg){
    $response->getBody()->write('Hello World');
    return $response;
});

$app->run();

結果

Hello World A B C

今回はミドルウェアを大量に作成するためcreateMiddleware()を定義してます。
Appインスタンスにはミドルウェアを追加するadd()やaddMiddleware()等があります。

実行順番はどうなっているかというと実は後で追加した方から先に実行されていきます。

今回の場合はC -> B -> Aの順番です。

$response = $handler->handle($request);

\$handler->handle()は次のミドルウェアを実行します。
CはBを、BはAを実行することで連鎖的に処理が進んでいきます。

でも表示結果は A B C の順だと思った方もいると思いますが、次のコードを見るとわかりやすいです。

use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Slim\Factory\AppFactory;

$app = AppFactory::create();

$app->setBasePath('/slim-public');

// RoutingMiddlewareの追加
$app->addRoutingMiddleware();
$app->addErrorMiddleware(true, true, true);

// ミドルウェア作成
function createMiddleware(string $message){
    return new class($message) implements MiddlewareInterface{
        public function __construct(private string $message) { }
        public function process(Request $request, RequestHandlerInterface $handler): Response
        {
            $m = $this->message;

            echo "Before($m) ";
            $response = $handler->handle($request);
            echo " After($m)";

            return $response;
        }
    };
}

$app->add(createMiddleware('A'));
$app->add(createMiddleware('B'));
$app->add(createMiddleware('C'));

$app->get('/hello',  function(Request $request, Response $response, array $arg){

    echo "[Hello World]";

    return $response;
});

$app->run();

結果

Before(C) Before(B) Before(A) [Hello World] After(A) After(B) After(C)

Before(C)とAfter(C)の間にBのミドルウェアが実行される。
Before(B)とAfter(B)の間にAのミドルウェアが実行される。
Before(A)とAfter(A)の間にルートコールバックが実行される。

という手順です。
今回は順番を追うために$response->getBody->write()ではなくechoで直接出力してます。

同じことがルートにも出来ます。
$app->get()が返したルートにもミドルウェアを追加します。

use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Slim\Factory\AppFactory;

$app = AppFactory::create();

$app->setBasePath('/slim-public');

// RoutingMiddlewareの追加
$app->addRoutingMiddleware();
$app->addErrorMiddleware(true, true, true);

// ミドルウェア作成
function createMiddleware(string $message){
    return new class($message) implements MiddlewareInterface{
        public function __construct(private string $message) { }
        public function process(Request $request, RequestHandlerInterface $handler): Response
        {
            $m = $this->message;

            echo "Before($m) ";
            $response = $handler->handle($request);
            echo " After($m)";

            return $response;
        }
    };
}

$app->add(createMiddleware('A'));
$app->add(createMiddleware('B'));
$app->add(createMiddleware('C'));

// ルートを取得
$route = $app->get('/hello',  function(Request $request, Response $response, array $arg){

    echo "[Hello World]";

    return $response;
});

$route->add(createMiddleware('X'));
$route->add(createMiddleware('Y'));
$route->add(createMiddleware('Z'));

$app->run();

結果。

Before(C) Before(B) Before(A) Before(Z) Before(Y) Before(X) [Hello World] After(X) After(Y) After(Z) After(A) After(B) After(C)

App側のミドルウェア(C -> B -> A)が実行される
Route側のミドルウェア(Z -> Y -> X)が実行される
ルートコールバックが実行される
の順で実行されます。

App、Route、どちらもミドルウェアを追加出来ます。
どちらも内部ではMiddlewareDispatcherが使用されてます。

MiddlewareDispatcher

AppとRouteが内部で使用しているMiddlewareDispatcherについて。

use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Slim\Factory\AppFactory;
use Slim\Factory\ServerRequestCreatorFactory;
use Slim\MiddlewareDispatcher;
use Slim\ResponseEmitter;

$app = AppFactory::create();

// ミドルウェア作成
function createMiddleware(string $message){
    return new class($message) implements MiddlewareInterface{
        public function __construct(private string $message) { }
        public function process(Request $request, RequestHandlerInterface $handler): Response
        {
            $m = $this->message;

            echo "Before($m) ";
            $response = $handler->handle($request);
            echo " After($m)";

            return $response;
        }
    };
}

// MiddlewareDispatcherの作成
$middleware = new MiddlewareDispatcher(
    new class implements RequestHandlerInterface{
        function handle(Request $request): Response
        {
            // レスポンスの作成はAppにゆだねることに。
            global $app;

            // 通常はルートコールバックで記述している箇所ですが。
            echo " [Hello! I am kernel] ";

            // レスポンスを返す。
            return $app->getResponseFactory()->createResponse();
        }
    }
);

// ミドルウェアを追加(実は$app->add()や$route->add()はMiddlewareDispatcherへのラッパーです)
$middleware->add(createMiddleware('A'));
$middleware->add(createMiddleware('B'));
$middleware->add(createMiddleware('C'));

// リクエストの作成
$request = ServerRequestCreatorFactory::create()->createServerRequestFromGlobals();

// ミドルウェアを実行し、レスポンスを取得
$response = $middleware->handle($request);

// レスポンスを出力(今回は使用しない)
//(new ResponseEmitter())->emit($response);

結果

Before(C) Before(B) Before(A) [Hello! I am kernel] After(A) After(B) After(C)

MiddlewareDispatcherの最初の引数は$kernelで中核的なハンドラを渡します。

new MiddlewareDispatcher( $kernel );

MiddlewareDispatcherはコンストラクタに渡したこの引数が最後に実行されます。

$middleware->handle()は最後に追加されたミドルウェアがハンドルされます。
最後に追加されたのはCミドルウェアなので、連鎖的にC -> B -> A -> $kernelの順で実行されていきます。

MiddlewareDispatcherの中身を見ると、$tipが最後に追加されたミドルウェアであり、
$middleware->handle()メソッドは$tip->handle()を呼び出すようになってます。
最後に追加された方から実行されていくのはこのためです。

MiddlewareDispatcherの第一引数であり、最後に実行される$kernelは今後解説する上でとても重要になります。

ちなみに$app->getMiddlewareDispatcher()$route->getMiddlewareDispatcher()でそれぞれのMiddlewareDispatcherインスタンスを取得出来ます。

今後説明しますが、$appの$kernelはRouteRunnerであり、RouteRunnerは$routeを実行するようになってます。
\$appのミドルウェアが実行されてから、次\$routeのミドルウェアが実行されるのはこのためです。

ミドルウェアの連結

複数のミドルウェアを連結させることが出来ます。

use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Slim\Factory\AppFactory;
use Slim\Factory\ServerRequestCreatorFactory;
use Slim\MiddlewareDispatcher;
use Slim\ResponseEmitter;

$app = AppFactory::create();

// ミドルウェア作成
function createMiddleware(string $message){
    return new class($message) implements MiddlewareInterface{
        public function __construct(private string $message) { }
        public function process(Request $request, RequestHandlerInterface $handler): Response
        {
            $m = $this->message;

            echo "Before($m) ";
            $response = $handler->handle($request);
            echo " After($m)";

            return $response;
        }
    };
}

// 一つ目のMiddlewareDispatcherの作成
$middleware = new MiddlewareDispatcher(
    new class implements RequestHandlerInterface{
        function handle(Request $request): Response
        {
            // レスポンスの作成はAppにゆだねることに。
            global $app;

            // 通常はルートコールバックで記述している箇所ですが。
            echo " [Hello! I am kernel] ";

            // レスポンスを返す。
            return $app->getResponseFactory()->createResponse();
        }
    }
);
$middleware->add(createMiddleware('A'));
$middleware->add(createMiddleware('B'));
$middleware->add(createMiddleware('C'));

// 一つ目を連結($kernelで実行)させるようにもう一つのミドルウェアを作成
$secondMiddleware = new MiddlewareDispatcher($middleware);
$secondMiddleware->add(createMiddleware('X'));
$secondMiddleware->add(createMiddleware('Y'));
$secondMiddleware->add(createMiddleware('Z'));

// リクエストの作成
$request = ServerRequestCreatorFactory::create()->createServerRequestFromGlobals();

// ミドルウェアを実行し、レスポンスを取得
$response = $secondMiddleware->handle($request);

// レスポンスを出力(今回は使用しない)
//(new ResponseEmitter())->emit($response);

結果

Before(Z) Before(Y) Before(X) Before(C) Before(B) Before(A) [Hello! I am kernel] After(A) After(B) After(C) After(X) After(Y) After(Z)

二つのミドルウェアを連結出来ます。

$secondMiddleware = new MiddlewareDispatcher($middleware);

上の例では$middleware$secondMiddlewareの小尾($kernel)に連結させました。

$response = $secondMiddleware->handle($request);

$secondMiddlewareを実行すると、\$secondMiddlewareが終わった後\$middlewareを実行します。
MiddlewareDispatcher自体がPsr\Http\Server\RequestHandlerInterfaceを実装しているのでMiddlewareDispatcherの第一引数に渡せるようになってます。

リクエスト属性

ミドルウェアの実行順番が分かったところで、今後一部で使うのでこの場で紹介します。

use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Slim\Factory\AppFactory;

$app = AppFactory::create();

$app->setBasePath('/slim-public');
$app->addRoutingMiddleware();
$app->addErrorMiddleware(true, true, true);

// 次に実行されるミドルウェア
$app->add(new class implements MiddlewareInterface{
    function process(Request $request, RequestHandlerInterface $handler): Response
    {
        $myData = $request->getAttribute('MY-DATA');
        $response = $handler->handle($request);
        $response->getBody()->write("<br />MyData = {$myData}<br />");
        return $response;
    }
});

// 最初に実行されるミドルウェア
$app->add(new class implements MiddlewareInterface{
    function process(Request $request, RequestHandlerInterface $handler): Response
    {
        $request = $request->withAttribute('MY-DATA', 12345);
        return $handler->handle($request);
    }
});

$app->get('/hello',  function(Request $request, Response $response, array $arg){
    $response->getBody()->write('Hello World');
    return $response;
});

$app->run();

結果

Hello World
MyData = 12345

最初に実行されるミドルウェアをまずご覧ください。

$request = $request->withAttribute('MY-DATA', 12345);

リクエストに独自の属性を追加できます。
次に実行されるミドルウェアを見てください。

$myData = $request->getAttribute('MY-DATA');

リクエストから属性を取得出来ます。

ちなみに、当然ですがミドルウェアの追加順を間違えると失敗します。

certificate Docker Gutenberg Hyper-V openssl PHP React ReduxToolkit REST ubuntu WordPress オレオレ認証局 フレームワーク