1
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?

More than 5 years have passed since last update.

league/routeのストラテジーとミドルウェアの使い分け

Last updated at Posted at 2017-01-29

リリースはされていませんが、thephpleague/routeにミドルウェアが追加されていました。アクションメソッドの外側の処理を1つではなく、いくつかのレイヤーによって分けることができます。今までは開発者が定義できるのは、カスタムストラテジーという1つのレイヤーだけでした。

ミドルウェアとカスタムストラテジー、一体どのような関係になっているのでしょうか?それを追ってみたいと思います。ミドルウェアというクロージャーの繋げ方は、下記の記事で解説しています。この内容をわかっていることを前提に解説していきます。

連結する関数の間にクロージャーを挟み込む理由を、ミドルウェアの実装から知る。 - Qiita

※ このバージョンアップでStrategyInterfaceが変わったので、自分でストラテジーを自作していた人は修正が必要になります。API自体が変わっています。

dispatch処理を見る。

<?php
$route = new League\Route\RouteCollection($container);

$route->map('GET', '/', function (ServerRequestInterface $request, ResponseInterface $response) {
    $response->getBody()->write('<h1>Hello, World!</h1>');

    return $response;
});

$response = $route->dispatch($container->get('request'), $container->get('response'));

$container->get('emitter')->emit($response);

上記はleagueのドキュメントから一部を抜粋したものです。ここに書かれている$route->dispatch()を見ていきます。

<?php
// RouteCollection.php
public function dispatch(ServerRequestInterface $request, ResponseInterface $response)
{
    $dispatcher = $this->getDispatcher($request);
    $execChain  = $dispatcher->handle($request);

    // $middlewareはcallableです。アクションメソッドをミドルウェアでラッピングしていきます。
    foreach ($this->getMiddlewareStack() as $middleware) {
        $execChain->middleware($middleware);
    }

    try {
        // 外側のミドルウェアから順に呼ばれていきます。
        // $responseが返ってきます。
        return $execChain->execute($request, $response);
    } catch (Exception $exception) {
        $middleware = $this->getStrategy()->getExceptionDecorator($exception);
        return (new ExecutionChain)->middleware($middleware)->execute($request, $response);
    }
}

例外が発生しない場合は、ストラテジーが関与していませんね。例外の有無は関係なしにミドルウェアを外側から実行します。ストラテジーがどこで実行されるのか探していきます。

$dispatcher = $this->getDispatcher($request);から見ていきます。

登録されたルートを組み立てて、Dispatcherを生成。

<?php
public function getDispatcher(ServerRequestInterface $request)
{
    // デフォルトのストラテジー
    // Dispatcherのストラテジーにもなります。
    if (is_null($this->getStrategy())) {
        $this->setStrategy(new ApplicationStrategy);
    }

    // mapでRouteCollectionに登録したルートを、FastRoute\RouteCollectorにaddRouteで渡します。
    // 渡すと言っても同じインスタンスです。FastRouteで定義されたプロパティにセットです。
    $this->prepRoutes($request);

    // getDataはFastRoute\RouteCollectorからの継承メソッドです。
    return (new Dispatcher($this->getData()))->setStrategy($this->getStrategy());
}

protected function prepRoutes(ServerRequestInterface $request)
{
    $this->buildNameIndex();

    $routes = array_merge(array_values($this->routes), array_values($this->namedRoutes));

    foreach ($routes as $key => $route) {
        // check for scheme condition
        if (! is_null($route->getScheme()) && $route->getScheme() !== $request->getUri()->getScheme()) {
            continue;
        }

        // check for domain condition
        if (! is_null($route->getHost()) && $route->getHost() !== $request->getUri()->getHost()) {
            continue;
        }

        $route->setContainer($this->container);

        if (is_null($route->getStrategy())) {
            $route->setStrategy($this->getStrategy());
        }

        // FastRoute\RouteCollectorからの継承メソッドです。
        $this->addRoute(
            $route->getMethods(),
            $this->parseRoutePath($route->getPath()),
            [$route, 'getExecutionChain']
        );
    }
}

RouteCollectionのストラテジーは、DispatcherRouteにセットされます。

RouteCollectionは外側から操作するルーターです。Routemapを実行した時に内部で生成されて管理されます。1つのルートパスを1つのRouteインスタンスで管理します。

<?php
$route = new League\Route\RouteCollection($container);

$route->map('GET', '/', function (ServerRequestInterface $request, ResponseInterface $response) {
    $response->getBody()->write('<h1>Hello, World!</h1>');

    return $response;
});

Dispatcher->handleがExecutionChainを返す過程

<?php
// Dispatcher.php
// FastRoute\Dispatcher\GroupCountBasedを継承しています。
public function handle(ServerRequestInterface $request)
{
    // 親のメソッドです。dispatchはleague側では実装はしていません。
    $match = $this->dispatch(
        $request->getMethod(),
        $request->getUri()->getPath()
    );

    if ($match[0] === FastRoute::NOT_FOUND) {
        return $this->handleNotFound();
    }

    if ($match[0] === FastRoute::METHOD_NOT_ALLOWED) {
        $allowed = (array) $match[1];
        return $this->handleNotAllowed($allowed);
    }

    // $m[1]は[Routeのインスタンス, 'getExecutionChain']の配列です。
    return $this->handleFound($match[1], (array) $match[2]);
}

// ExecutionChainを返します。
protected function handleFound(callable $route, array $vars)
{
    return call_user_func_array($route, [$vars]);
}

handleはルートが見つかった時はRoute->getExecutionChainを実行するので、ExecutionChainを返すことになります。さきほどのRouteCollection->prepRoutesでハンドラは追加されていました。実行するメソッド名は固定で、レシーバーの$routeだけ変化します。

<?php
// RouteCollection.php
foreach ($routes as $key => $route) {
    // ...
    $this->addRoute(
        $route->getMethods(),
        $this->parseRoutePath($route->getPath()),
        [$route, 'getExecutionChain']
    );
}

handleの中ではルートが見つかった場合はストラテジーは関与されませんが、handleFound以外が実行される時はDispatcherにセットされているストラテジーが使用されます。

<?php
// Dispatcher.php
protected function handleNotFound()
{
    $exception  = new NotFoundException;
    $middleware = $this->getStrategy()->getNotFoundDecorator($exception);
    return (new ExecutionChain)->middleware($middleware);
}

protected function handleNotAllowed(array $allowed)
{
    $exception  = new MethodNotAllowedException($allowed);
    $middleware = $this->getStrategy()->getMethodNotAllowedDecorator($exception);
    return (new ExecutionChain)->middleware($middleware);
}

ストラテジーには、例外クラスを渡してミドルウェア(callable)を返すメソッドを定義されているようです。

ルートが見つかった場合は、ストラテジーからExecutionChainを取得する。

ストラテジーには例外処理の時以外に、Route->getExecutionChainを通じてExecutionChainを返すメソッドがあります。ミドルウェア(callable)を返すのは例外処理の時だけです。どちらにせよ、ストラテジーはミドルウェアに関連するものを返します。

<?php
// Route.php
public function getExecutionChain(array $vars)
{
    return $this->getStrategy()->getExecutionChain($this, $vars);
}

このメソッドはルートが見つかった時に実行されます。過程は次のとおりです。

  1. protectedであるRouteCollection->prepRoutesの中で、addRouteによってRoute->getExecutionChainをハンドラとしてセットします。
  2. Dispatcher->handleFastRoutedispatchを実行します。ルートが見つかった場合はhandleFoundを実行します。
  3. Dispatcher->handleFoundによって、1のハンドラを実行します。この時ルートパスからキャプチャした変数を渡します。

アクションメソッドはどこで実行されるのか?

2.0.0だと、RequestResponseStrategy->dispatchで登録したイベントハンドラであるアクションメソッドが実行されていました。

<?php
// デフォルトのストラテジー
class RequestResponseStrategy extends AbstractStrategy implements StrategyInterface
{
    public function dispatch(callable $controller, array $vars, Route $route = null)
    {
        $response = call_user_func_array($controller, [
            $this->getRequest(),
            $this->getResponse(),
            $vars
        ]);

        return $this->determineResponse($response);
    }
}

2.0.1でもアクションメソッドの実行場所はストラジーのままです。デフォルトクラスとメソッド名は変更されています。RequestResponseStrategyは削除されました。メソッドdispatchの代わりとなったgetExecutionChainは、名前通り大きく変わっています。戻り値がResponseからExecutionChainになっています。

<?php
// ApplicationStrategy.php
public function getExecutionChain(Route $route, array $vars)
{
    // アクションメソッドを実行
    $middleware = function (
        ServerRequestInterface $request, ResponseInterface $response, callable $next
    ) use (
        $route, $vars
    ) {
        // アクションメソッドの実行
        $response = call_user_func_array($route->getCallable(), [$request, $response, $vars]);

        if (! $response instanceof ResponseInterface) {
            throw new RuntimeException(
                'Route callables must return an instance of (Psr\Http\Message\ResponseInterface)'
            );
        }

        // ExecutionChain->buildExecutionChainで定義されている最後に実行されるクロージャーが$nextになります。
        // 今のところreturn $responseしか処理がありません。
        return $next($request, $response);
    };

    $execChain = (new ExecutionChain)->middleware($middleware);

    // TODO
    $stack = array_reverse($route->getMiddlewareStack());

    foreach ($stack as $middleware) {
        $execChain->middleware($middleware);
    }

    return $execChain;
}

StrategyInterfaceの変更点

これだけ変わったということはStrategyInterfaceも変わっています。カスタムストラテジーを自作していた場合は修正が必要です。ストラテジーで$controller(callable)を直接引数で受け取らなくなり、Routeを通して受取るようになっています。

<?php
// 2.0.0 BEFORE
class RequestResponseStrategy extends AbstractStrategy implements StrategyInterface {}
interface StrategyInterface
{
    // @return \Psr\Http\Message\ResponseInterface
    public function dispatch(callable $controller, array $vars, Route $route = null);
}

// 2.0.1 AFTER
class ApplicationStrategy implements StrategyInterface {}
interface StrategyInterface
{
    // @return \League\Route\Middleware\ExecutionChain
    public function getExecutionChain(Route $route, array $vars);

    // 下記3つの戻り値は同じ
    // @return callable
    public function getNotFoundDecorator(NotFoundException $exception);
    public function getMethodNotAllowedDecorator(MethodNotAllowedException $exception);
    public function getExceptionDecorator(Exception $exception);
}

ミドルウェアによってストラテジーが出来るようになったこと

以前はdispatchのメソッドhandleNotFound, handleNotAllowedHttp\Exception->buildJsonResponseが実行していたので、この例外ハンドラの処理は固定でした。それがストラテジーからミドルウェアを受取るようになったので、この2つのハンドラの処理をストラテジーで定義出来るようになりました。get~Decoratorというメソッドです。

ちなみにbuildJsonResponseは、DispatcherからJsonStrategyに記述が移行しました。

ストラテジーで定義する処理はFastRoutedispatchを実行後であることは変わりません。

ストラテジーの立ち位置は変わらない

ストラテジーの処理は変わりましたが、これはミドルウェアを導入するためです。ストラテジーで差し込む処理のタイミングは変わっていません。実行するアクションメソッドが決定した後です。

ストラテジーで定義したミドルウェアは、1番最初に包み込むラッパーです。もう一度、dispatchを見てみましょう。ストラテジーのミドルウェアの中からアクションメソッドが呼ばれます。

<?php
// RouteCollection.php
public function dispatch(ServerRequestInterface $request, ResponseInterface $response)
{
    $dispatcher = $this->getDispatcher($request);
    // ストラテジーから取得したチェーン・ミドルウェアが1番外側にあります。
    $execChain  = $dispatcher->handle($request);

    // ストラテジーのミドルウェアの上から、さらにラッピングします。
    foreach ($this->getMiddlewareStack() as $middleware) {
        $execChain->middleware($middleware);
    }

    try {
        // 外側のミドルウェアから順に呼ばれていきます。
        return $execChain->execute($request, $response);
    } catch (Exception $exception) {
        $middleware = $this->getStrategy()->getExceptionDecorator($exception);
        return (new ExecutionChain)->middleware($middleware)->execute($request, $response);
    }
}

注意点として、例外が補足されているのはミドルウェアの処理だけです。カスタムストラテジーで発生した例外は補足されません。ApplicationStrategyget~Decoratorthrow exception;の1行しか記述されていません。このdispatchメソッド自体を呼び出すところで例外処理をする必要があります。それかストラテジーを自作しましょう。

ミドルウェアとストラテジーの使い分け

ルートが見つかった場合は次の順にメソッドが呼ばれていきます。ミドルウェア、ストラテジーのどちらで処理を書いてもleaguedispatchが実行された後になります。

  1. 追加したミドルウェア
  2. ストラテジーのミドルウェア
  3. アクションメソッド
  4. ExecutionChainの中のミドルウェア

そのため、1のミドルウェアに書かずにストラテジー処理を全て書いてしまうことも可能です。ストラテジーも結局はミドルウェアしているので同じことです。それだと肥大化してしまいます。

感覚的にはOSI参照モデルに近いと思います。役割をレイヤーごとに分けるために、ミドルウェアを使います。ストラテジーは最もアクションメソッドに近いレイヤーで、FastRouteによるdispatchの結果ごとに実行するメソッドが用意されています。つまりルーティング後の処理がストラテジーではないかと思います。といっても3種類の分岐しかありませんが。後処理はどのミドルウェアでも挟み込めるので、ルーティングの内側がレイヤーがストラテジーですね。

これより外側の処理をミドルウェアに任せればいいのではないでしょうか?また、全てのストラテジーの共通処理をミドルウェアに任せるといった使い方もできます。

例えば、レスポンスの種類(HTML, JSON, XML)ごとにストラテジーを用意して、ブラック・ホワイトリストやログイン処理などをミドルウェアに書くなどです。

1
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
1
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?