PHP Slim4解読、ルーティング編

Slim4のルーティングについて深く掘り下げていきます。
先にミドルウェア編を読んでおいてください。

セキュリティーには一切気を使ってません(サニタイズしてない箇所がある)のでそのままコードの利用はやめてください。

ルーティングを全部取得する

use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use Slim\Factory\AppFactory;
use Slim\Routing\RouteCollectorProxy;

$app = AppFactory::create();

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

$app->addRoutingMiddleware();
$app->addErrorMiddleware(true, true, true);

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

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

$app->group('/user', function(RouteCollectorProxy $group){

    $group->get('/profile',  function(Request $request, Response $response, array $arg){
        $response->getBody()->write('I am pretty!');
        return $response;
    });

});

$routes = $app->getRouteCollector()->getRoutes();

foreach($routes as $route){
    $pattern = $route->getPattern();
    $identifer = $route->getIdentifier();

    echo "{$identifer} = {$pattern}<br />";
}

結果

route0 = /contact
route1 = /about
route2 = /user/profile

Appクラス(厳密には基底クラスのRouteCollectorProxy)はルートを追加するメソッド(get(), post(), put(), etc)やグループを追加するgroup()があります。
get(), post(), put()等はRouteControllerProxy内でmap()を呼び出します。

RouteCollectorProxyはRouteCollectorのインスタンスを持っててこれらのメソッドはそのインスタンスへのプロキシです。
$app->group()は$app->routeCollector->group()、$app->map()は$app->routeCollector->map()を呼び出してます。
RouteCollectorのインスタンスは$app->getRouteCollector()で取得出来ますが、直接使うというよりはプロキシ経由で追加します。

追加されたルート一覧はgetRoutes()で取得出来ます。

$routes = $app->getRouteCollector()->getRoutes();

パスはgetPattern()で取得出来ます。
グループが階層化したらそれらが連結したパスが取得されます。

$route->getPattern()

IDはgetIdentifier()で取得出来ます。

$identifer = $route->getIdentifier();

ちなみにルートのインターフェース型はRouteInterfaceですが実体はRoute型のインスタンスです。
次回詳しくあつかいます。

ルート追加系のメソッドはさらにRouteCollectorのmap()メソッドを実行しますが、この時にRouteインスタンスが作成されます。
RouteCollector内にカウンタを持っていて、これが実行されるたびインクリメントされます。
その値がRouteのコンストラクタの引数で渡され、文字列routeと結合した文字列がRouteのIDになります。
ルートのIDは追加順にroute0, route1, route2 最後の数字が増えていく仕組みです。

IDからルートを取得する

use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use Slim\Factory\AppFactory;
use Slim\Routing\RouteCollectorProxy;

$app = AppFactory::create();

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

$app->addRoutingMiddleware();
$app->addErrorMiddleware(true, true, true);

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

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

$app->group('/user', function(RouteCollectorProxy $group){

    $group->get('/profile',  function(Request $request, Response $response, array $arg){
        $response->getBody()->write('I am pretty!');
        return $response;
    });

});

// ルートのIDからRouteインスタンスを取得出来る。
$lookupRoute = $app->getRouteCollector()->lookupRoute('route2');
echo $lookupRoute->getPattern();

誰の役にも立たないパスからルートを取得する方法

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

$app = AppFactory::create();

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

// ルーティングしない(自分でする)のでいらない。
//$app->addRoutingMiddleware();
//$app->addErrorMiddleware(true, true, true);

$app->get('/item/{pid}',  function(Request $request, Response $response, array $arg){
    $pid = (int)$arg['pid'];
    $response->getBody()->write("item id is ${pid}");
    return $response;
});

// RoutingResultを取得する(IDなどを取得)
$result = $app->getRouteResolver()->computeRoutingResults(
    '/slim-public/item/123',  // $request->getUri()->getPath()
    'GET'                     // $request->getMethod()
);

// ルートのIDとパラメータを取得する
$id = $result->getRouteIdentifier();
$param = $result->getRouteArguments();

// IDからRouteを取得、パラメータを設定
$route = $app->getRouteResolver()->resolveRoute($id);
$route->prepare($param);

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

// Routeを実行(ミドルウェアがある場合は先祖のグループのミドルウェア及び自身のミドルウェアが実行される)
$response = $route->run($request);

// レスポンスを出力
(new ResponseEmitter())->emit($response);

結果(ルーティングしてないのでhttps://your-domain/slim-public/へ直接アクセスしても結果は一緒。)

item id is 123

実はこれを知るのは大変です。

SlimではURIからルートの取得に FastRouteという外部ライブラリが使用されています。
FastRouteについてはググって調べてください。

$app->getRouteResolver()

RouteResolverはその名の通りルートを解決するためのものです。
実際にはSlim\Routing\RouteResolverのインスタンスです。

computeRoutingResults()はURIとメソッド(GETやPOST)から解析した情報をSlim\Routing\RoutingResultsのインスタンスに変換して返します。

$result = $app->getRouteResolver()->computeRoutingResults('/slim-public/item/123', 'GET');

内部ではSlim\Routing\Dispatcherクラスが使用されて解析が行われてます。

Dispacher

Dispatcherは外部ライブラリのFastRouteを使って解析したあと、そのデータをRoutingResultsに変換して返します。
つまりFastRouteの操作を任されたクラスで、Dispatcherのdispatch()で解析結果を返します。
このメソッドの処理は、

  • SlimのRouteCollectorからルートを全部とりだす($app->getRouteCollector()->getRoutes()と読み替えてOK)
  • ルートをFastRouteのコレクタ(FastRoute\RouteCollector)に追加(addRoute()メソッド、この時点で解析済)、しながらディスパッチャを作成する
  • FastRouteのディスパッチャをディスパッチする
  • FastRouteの戻り値を元にSlim\Routing\RoutingResultsを作成して返す。

という処理を行います。

Dispacherで作成されるディスパッチャはSlimで継承されたSlim\Routing\FastRouteDispatcherであり、基底クラスのFastRoute\Dispatcher\GroupCountBasedをほぼ書き換えていてます。
解析はFastRoute\RouteCollectoraddRoute()した時点で行われており、後は取り出すだけのようです。

詳しく知りたい人はFastRouteを理解した上でDispacherのコードを読んでください。

ミドルウェアとルーティングの関係

めっちゃ複雑です。
AppからRouteが実行される経緯を解説します。

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

$app = AppFactory::create();

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

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

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

$app->run();

AppとRouteにはそれぞれ独自のMiddlewareDispacherを持っています。
まずはAppから見ていきます。

AppのMiddlewareDispacher

$app->addRoutingMiddleware();

AppにはaddRoutingMiddleware()メソッドがあり、これはミドルウェアにSlim\Middleware\RoutingMiddlewareのインスタンスを追加します。

このミドルウェアは実行時にパスからルートを取得し、リクエスト属性に追加します。
ものすごく簡素化するとこんな感じです。

public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ServerRequestInterface
{
    $route = /* パスからRouteを取得 */;

    $handler->handle(
        $request->withAttribute(RouteContext::ROUTE, $route)
    );
}

$app->run()を実行すとAppのMiddlewareDispacherが実行されますが、
この時の$kernel(最後に実行されるハンドラでありMiddlewareDispacherの第一引数)は Slim\Routing\RouteRunner です。
RouteRunnerはハンドリングされるとリクエスト属性からルートを取得し、Routeのrun()メソッドを実行します。

public function handle(ServerRequestInterface $request): ResponseInterface{
    $route = $request->getAttribute(RouteContext::ROUTE);
    return $route->run($request);
}

Routeのrun()

Routeのrun()は先祖のグループのミドルウェアを全部実行した後、自分のミドルウェアを実行します。
RouteのMiddlewareDispacherの$kernelはRouteのhandle()メソッドであり、これが最終的なルートコールバック($app->get()の第二引数)の実行場所になります。

このルートコールバックの動作を横取りするにはInvocationStrategyInterfaceを使いますがそれはまた後で。

要約すると

  • Appのrun()を実行
  • App->middlewareDispacherのRoutingMiddlewareを実行、ルートが取得されリクエスト属性にセット
  • App->middlewareDispacherの最後のRouteRunnerが実行、リクエスト属性がゲットしたルートのrun()メソッドが呼び出される
  • Route->middlewareDispacherが実行される、最後はRoute->handle()でルートコールバックが呼び出される。

$app->addRoutingMiddleware()

\$app->addRoutingMiddleware()を実行するとRoutingMiddlewareが追加されます。
これが無ければルーティング出来ないように思えますが、実際にはRouteRunnerはリクエスト属性にルーティングされた形跡が無ければここでもルーティングされます。

Route->handle()で実行されるルートコールバックの処理を制御する

ルートコールバックを呼び出す際に処理を制御したい場合に使用します。

Slim\Interfaces\InvocationStrategyInterfaceを実装したインターフェースを

  • RouteCollectorのsetDefaultInvocationStrategy()に渡す。
  • RouteのsetInvocationStrategy()に渡す

の2通りがあります。

RouteCollector->setDefaultInvocationStrategy()はルートが作成される前にセットしておきます。
Routeインスタンスが作成される時にこの値がコンストラクタで初期化されるからです。
Route全体に適用したい場合に使うのでしょう。

Route->setDefaultInvocationStrategy()はルート単位で設定します。
Route->handle()実行時に最終的にこの値が使用されます。

設定されていなかった場合、デフォルトではSlim\Handlers\Strategies\RequestResponseが使用されます。

デフォルトのRequestResponse

デフォルトのRequestResponseではパラメータをリクエスト属性に追加します。
ルートコールバックではそれが取得出来ます。

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

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

$app->get('/user/{name}/{year}',  function(Request $request, Response $response, array $arg){

    $name = $request->getAttribute('name') ?? 'Anonymous';
    $year = $request->getAttribute('year') ?? 0;

    // ここサニタイズ必要? まだ調べてませんので注意してください。
    $response->getBody()->write("{$name}<br />{$year}");
    return $response;
});

$app->run();

https://your-domain/slim-public/user/kurage/2022

で実行した結果、

kurage
2022

独自に設定すると

use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use Slim\Factory\AppFactory;
use Slim\Interfaces\InvocationStrategyInterface;

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

$app->getRouteCollector()->setDefaultInvocationStrategy(
    new class implements InvocationStrategyInterface {
        function __invoke(callable $callable, Request $request, Response $response, array $routeArguments): Response
        {
            // このコードの前後で引数に変更を与えれる。今回は何も視せずにルートコールバックを呼び出す。
            return $callable($request, $response, $routeArguments);
        }
    }
);

$app->get('/user/{name}/{year}',  function(Request $request, Response $response, array $arg){

    $name = $request->getAttribute('name') ?? 'Anonymous';
    $year = $request->getAttribute('year') ?? 0;

    // ここサニタイズ必要? まだ調べてませんので注意してください。
    $response->getBody()->write("{$name}<br />{$year}");
    return $response;
});

$app->run();

結果(https://your-domain/slim-public/user/kurage/2022 で実行した)

Anonymous
0

元のRequestResponseを上書してしまったためこの機能は使えなくなります。

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