リリースはされていませんが、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
のストラテジーは、Dispatcher
とRoute
にセットされます。
RouteCollection
は外側から操作するルーターです。Route
はmap
を実行した時に内部で生成されて管理されます。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);
}
このメソッドはルートが見つかった時に実行されます。過程は次のとおりです。
-
protected
であるRouteCollection->prepRoutes
の中で、addRoute
によってRoute->getExecutionChain
をハンドラとしてセットします。 -
Dispatcher->handle
でFastRoute
のdispatch
を実行します。ルートが見つかった場合はhandleFound
を実行します。 -
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
, handleNotAllowed
はHttp\Exception->buildJsonResponse
が実行していたので、この例外ハンドラの処理は固定でした。それがストラテジーからミドルウェアを受取るようになったので、この2つのハンドラの処理をストラテジーで定義出来るようになりました。get~Decorator
というメソッドです。
ちなみにbuildJsonResponse
は、Dispatcher
からJsonStrategy
に記述が移行しました。
ストラテジーで定義する処理はFastRoute
のdispatch
を実行後であることは変わりません。
ストラテジーの立ち位置は変わらない
ストラテジーの処理は変わりましたが、これはミドルウェアを導入するためです。ストラテジーで差し込む処理のタイミングは変わっていません。実行するアクションメソッドが決定した後です。
ストラテジーで定義したミドルウェアは、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);
}
}
注意点として、例外が補足されているのはミドルウェアの処理だけです。カスタムストラテジーで発生した例外は補足されません。ApplicationStrategy
のget~Decorator
はthrow exception;
の1行しか記述されていません。このdispatch
メソッド自体を呼び出すところで例外処理をする必要があります。それかストラテジーを自作しましょう。
ミドルウェアとストラテジーの使い分け
ルートが見つかった場合は次の順にメソッドが呼ばれていきます。ミドルウェア、ストラテジーのどちらで処理を書いてもleague
のdispatch
が実行された後になります。
- 追加したミドルウェア
- ストラテジーのミドルウェア
- アクションメソッド
- ExecutionChainの中のミドルウェア
そのため、1のミドルウェアに書かずにストラテジー処理を全て書いてしまうことも可能です。ストラテジーも結局はミドルウェアしているので同じことです。それだと肥大化してしまいます。
感覚的にはOSI参照モデルに近いと思います。役割をレイヤーごとに分けるために、ミドルウェアを使います。ストラテジーは最もアクションメソッドに近いレイヤーで、FastRoute
によるdispatch
の結果ごとに実行するメソッドが用意されています。つまりルーティング後の処理がストラテジーではないかと思います。といっても3種類の分岐しかありませんが。後処理はどのミドルウェアでも挟み込めるので、ルーティングの内側がレイヤーがストラテジーですね。
これより外側の処理をミドルウェアに任せればいいのではないでしょうか?また、全てのストラテジーの共通処理をミドルウェアに任せるといった使い方もできます。
例えば、レスポンスの種類(HTML, JSON, XML)ごとにストラテジーを用意して、ブラック・ホワイトリストやログイン処理などをミドルウェアに書くなどです。