ルートモデルバインディングまでの道のり
laravelでweb.phpが読み込まれるまでの道のりを読んでおくとさらにわかりやすいかもしれません。
Let's read
ルートが決定されるまで
Illuminate/Routing/Router::dispatchToRoute()
から始めていきます。
public function dispatchToRoute(Request $request)
{
return $this->runRoute($request, $this->findRoute($request));
}
runRoute()
の引数に渡されるfindRoute()
を見ていきます。
protected function findRoute($request)
{
$this->current = $route = $this->routes->match($request);
$this->container->instance(Route::class, $route);
return $route;
}
$this->routes
はroutes/web.php
などのルートファイルを読み込んで、その1つ1つのルートをIlluminate/Routing/Route
として保持しているIlluminate/Routing/RouteCollection
です。
Illuminate/Routing/RouteCollection::match()
にリクエストを渡して呼び出しています。
public function match(Request $request)
{
$routes = $this->get($request->getMethod());
$route = $this->matchAgainstRoutes($routes, $request);
if (! is_null($route)) {
return $route->bind($request);
}
//...
}
$request->getMethod()
でリクエストされたメソッドを取得して、$this->get()
ですべてのルートから取得したメソッドと同じルートを抜き出します。
$this->matchAgatinstRoutes()
で1つのルートに絞っていきます。
protected function matchAgainstRoutes(array $routes, $request, $includingMethod = true)
{
[$fallbacks, $routes] = collect($routes)->partition(function ($route) {
return $route->isFallback;
});
return $routes->merge($fallbacks)->first(function ($value) use ($request, $includingMethod) {
return $value->matches($request, $includingMethod);
});
}
routeのプロパティのisFallback
でroutesを振り分けています。(初期値はfalse)
isFallback
は後退という意味でマッチングの優先順位を下げています。(ルーティングは上に書かれたものから優先される)
コレクションクラスのmerge()
で$routes
に$fallbacks
を結合します。これで$routes
に入っていた値が優先されます。
コレクションクラスのfirst()
の引数に渡したコールバックでルートを探していきます。
public function matches(Request $request, $includingMethod = true)
{
$this->compileRoute();
foreach ($this->getValidators() as $validator) {
if (! $includingMethod && $validator instanceof MethodValidator) {
continue;
}
if (! $validator->matches($this, $request)) {
return false;
}
}
return true;
}
$this->compileRoute()
はIlluminate/Routing/RouteCompiler::compile()
の結果を$this->compiled
に格納します。
public function compile()
{
$optionals = $this->getOptionalParameters();
$uri = preg_replace('/\{(\w+?)\?\}/', '{$1}', $this->route->uri());
return (
new SymfonyRoute($uri, $optionals, $this->route->wheres, ['utf8' => true], $this->route->getDomain() ?: '')
)->compile();
}
getOptionalParameters()
はOptionalParameterを取得します。
preg_replace()
ではOptionalParameterから'?'を取り除きます。
例) {hoge?} -> {hoge}
SymfonyRoute
からはより細かい話になってくるのでざっくり解説します。(というか読めない...)
vendor/symfony/routing/RouteCompiler::compilePattern()
でurlのpathからどのような正規表現で値を抜き出すかを決めてるっぽいです。
コメントで「「{}」で囲まれたすべての変数に一致し....」とあるのでおそらくそうだと思います。(laravelも「{}」の中身を抜き出すので)
// Match all variables enclosed in "{}" and iterate over them. But we only want to match the innermost variable
// in case of nested "{}", e.g. {foo{bar}}. This in ensured because \w does not match "{" or "}" itself.
vendor/symfony/routing/CompiledRoute
のregex
プロパティにこの正規表現をセットしています。
最終的にvendor/symfony/routing/CompiledRoute
が$this->compiled
にセットされます。
foreachでrouteを1つずつバリデーションにかけていきます。バリデーションの内容はgetValidators()
で取得します。
public static function getValidators()
{
return static::$validators = [
new UriValidator, new MethodValidator,
new SchemeValidator, new HostValidator,
];
}
それぞれのクラスのmatch()
を使ってマッチするuriを探していきます。すべてのバリデーションを通過したrouteが返されます。
これがIlluminate/Routing/RouteCollection::matchAgainstRoutes()
の返り値になります。
// 再掲
public function match(Request $request)
{
$routes = $this->get($request->getMethod());
$route = $this->matchAgainstRoutes($routes, $request);
if (! is_null($route)) {
return $route->bind($request);
}
}
マッチするrouteが見つかるとIlluminate/Routing/Route::bind()
にリクエストが渡されて呼び出されます。
public function bind(Request $request)
{
$this->compileRoute();
$this->parameters = (new RouteParameterBinder($this))
->parameters($request);
$this->originalParameters = $this->parameters;
return $this;
}
$this->compileRoute()
は先程と同じなので割愛します。
parameters()
でuriから値をキャプチャしていきます。
public function parameters($request)
{
$parameters = $this->bindPathParameters($request);
//...
return $this->replaceDefaults($parameters);
}
protected function bindPathParameters($request)
{
$path = '/'.ltrim($request->decodedPath(), '/');
preg_match($this->route->compiled->getRegex(), $path, $matches);
return $this->matchToKeys(array_slice($matches, 1));
}
bindPathParameters()
が実際の処理をしていきます。
decodePath()
でURLエンコードされたuriをデコードします。
preg_match()
で$path
からvendor/symfony/routing/CompiledRoute
のregex
プロパティにセットした正規表現を使って値を取得していきます。
「{}」で囲ったところに値するものを取得します。
$this->matchToKeys()
で整形をします。
例
web.phpで定義したルート: '/{hoge}/{fuga}'
実際のパス: '/1/4'
整形された配列: ['hoge' => '1', 'fuga' => '4']
整形された配列をIlluminate/Routing/Route
のparameters
プロパティに格納します。
Illuminate/Routing/Router::findRoute()
に戻ります。
protected function findRoute($request)
{
$this->current = $route = $this->routes->match($request);
$this->container->instance(Route::class, $route);
return $route;
}
マッチしたrouteを変数に格納し、コンテナにバインドしています。
runRoute()
に戻ります。
public function dispatchToRoute(Request $request)
{
return $this->runRoute($request, $this->findRoute($request));
}
protected function runRoute(Request $request, Route $route)
{
$request->setRouteResolver(function () use ($route) {
return $route;
});
$this->events->dispatch(new RouteMatched($route, $request));
return $this->prepareResponse($request,
$this->runRouteWithinStack($route, $request)
);
}
setRouteResolver()
はプロパティである$this->routeResolver
に引数のクロージャをセットしています。
ルートがマッチしましたよ〜というイベントを発火させて、$this->runRouteWithinStack()
に参ります。
protected function runRouteWithinStack(Route $route, Request $request)
{
$shouldSkipMiddleware = $this->container->bound('middleware.disable') &&
$this->container->make('middleware.disable') === true;
$middleware = $shouldSkipMiddleware ? [] : $this->gatherRouteMiddleware($route);
return (new Pipeline($this->container))
->send($request)
->through($middleware)
->then(function ($request) use ($route) {
return $this->prepareResponse(
$request, $route->run()
);
});
}
ここでは決定したルートのアクションを呼ぶ前にミドルウェアを集めて実行しています。(LaravelのPipeline::thenでmiddlewareを処理している流れをまとめてみた)
ミドルウェアはapp/Http/Kernel.php
に定義されているルートミドルウェアが使われています。
その中にルートモデルバインディングを行っている`\Illuminate\Routing\Middleware\SubstituteBindings::class'があります。
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
];
ルートモデルバインディング
ミドルウェアはhandle()
が呼ばれます。
public function handle($request, Closure $next)
{
$this->router->substituteBindings($route = $request->route());
$this->router->substituteImplicitBindings($route);
return $next($request);
}
明示的な結合
substituteBindings()
は明示的な結合と呼ばれるものを解決しています。
// こんな感じ
public function boot()
{
parent::boot();
Route::model('hoge', App\User::class);
}
Illuminate/Routing/Route
のparameters
プロパティに該当のものがあれば解決してくれます。
$parameters = ['hoge' => '2']
ならApp\User::class
のprimaryKeyが'2'のものをとってきてくれる。
暗黙的な結合
substituteImplicitBindings()
は明示的な結合のように書かなくても、ある一定の法則で書けば勝手に解決してくれますよ〜のやつです。
public function substituteImplicitBindings($route)
{
ImplicitRouteBinding::resolveForRoute($this->container, $route);
}
public static function resolveForRoute($container, $route)
{
$parameters = $route->parameters();
foreach ($route->signatureParameters(UrlRoutable::class) as $parameter) {
if (! $parameterName = static::getParameterName($parameter->name, $parameters)) {
continue;
}
$parameterValue = $parameters[$parameterName];
if ($parameterValue instanceof UrlRoutable) {
continue;
}
$instance = $container->make($parameter->getClass()->name);
if (! $model = $instance->resolveRouteBinding($parameterValue)) {
throw (new ModelNotFoundException)->setModel(get_class($instance), [$parameterValue]);
}
$route->setParameter($parameterName, $model);
}
}
$router->parameters()
でuriから抜き出してparameters
プロパティにセットしておいた配列を取得します。
例
web.phpで定義したルート: '/{hoge}/{fuga}'
実際のパス: '/1/4'
整形された配列: ['hoge' => '1', 'fuga' => '4']
signatureParameters()
を見ていきます。
public function signatureParameters($subClass = null)
{
return RouteSignatureParameters::fromAction($this->action, $subClass);
}
fromAction()
に$this->action
を渡して呼び出しています。
$this->action
にはマッチしたルートのコントローラーとメソッドが以下の形で保持されています。
$this->action = ['uses' => 'HogeController@method', 'controller' => 'HogeController@method']
public static function fromAction(array $action, $subClass = null)
{
$parameters = is_string($action['uses'])
? static::fromClassMethodString($action['uses'])
: (new ReflectionFunction($action['uses']))->getParameters();
return is_null($subClass) ? $parameters : array_filter($parameters, function ($p) use ($subClass) {
return $p->getClass() && $p->getClass()->isSubclassOf($subClass);
});
}
今回は$action['uses']
は文字列なのでfromClassMethodString()
を見ていきます。
protected static function fromClassMethodString($uses)
{
[$class, $method] = Str::parseCallback($uses);
if (! method_exists($class, $method) && is_callable($class, $method)) {
return [];
}
return (new ReflectionMethod($class, $method))->getParameters();
}
1行目で「@」でコントローラークラスとメソッドに分けます。
ReflectionMethod
クラスをつかってメソッドの引数に指定されている名前を取得して配列で返します。
先程取得した配列をフィルターにかけていきます。
$p->getClass()
でタイプヒンティングされているかを検証し、$p->getClass()->isSubclassOf
でIlluminate/Contracts/Routing/UrlRoutable
を実装しているかを検証しています。
Illuminate/Contracts/Routing/UrlRoutable
はIlluminate/Database/Eloquent/Model
が実装しているのでEloquentは対象になります。
コントローラーのparameter解析が終わったのでforeachに戻ります。
public static function resolveForRoute($container, $route)
{
$parameters = $route->parameters();
foreach ($route->signatureParameters(UrlRoutable::class) as $parameter) {
if (! $parameterName = static::getParameterName($parameter->name, $parameters)) {
continue;
}
$parameterValue = $parameters[$parameterName];
if ($parameterValue instanceof UrlRoutable) {
continue;
}
$instance = $container->make($parameter->getClass()->name);
if (! $model = $instance->resolveRouteBinding($parameterValue)) {
throw (new ModelNotFoundException)->setModel(get_class($instance), [$parameterValue]);
}
$route->setParameter($parameterName, $model);
}
}
getParameterName
でuriから取得した名前とメソッドの引数名が一致しているものを探します。(キャメルケースとスネークケースの差はここで吸収)
以下の例で$parameterValue
の取得内容を見てみます。
例
web.phpで定義したルート: '/{hoge}/{fuga}'
実際のパス: '/1/4'
整形された配列: ['hoge' => '1', 'fuga' => '4']
$parameterValue /* 1 */ = $parameters[$parameterName /* hoge */];
$instance = $container->make($parameter->getClass()->name);
でタイプヒンティングの名前からモデルインスタンスを生成します。
resolveRouteBinding()
で $parameterValue
を使ってwhereしてデータを引っ張ってきます。
public function resolveRouteBinding($value)
{
return $this->where($this->getRouteKeyName(), $value)->first();
}
$route->setParameter()
でparameters
プロパティを上書きします。
これでルートモデルバインディングは解決です。
実際にコントローラーに引数を渡す
Illuminate/Routing/ControllerDispatcher::dispatch()
の中の$controller->{$method}(...array_values($parameters))
でparametersの値を渡しています。
public function dispatch(Route $route, $controller, $method)
{
$parameters = $this->resolveClassMethodDependencies(
$route->parametersWithoutNulls(), $controller, $method
);
if (method_exists($controller, 'callAction')) {
return $controller->callAction($method, $parameters);
}
return $controller->{$method}(...array_values($parameters));
}
これにて終了です。symfonyのところはわかりませんでしたが大体の流れはつかめたと思います。やったね!