LoginSignup
9
11

More than 3 years have passed since last update.

ルートモデルバインディングってどうやって解決しているのか気になったのでコードの奥地にでかけてみた

Posted at

ルートモデルバインディングまでの道のり

laravelでweb.phpが読み込まれるまでの道のりを読んでおくとさらにわかりやすいかもしれません。

Let's read

ルートが決定されるまで

Illuminate/Routing/Router::dispatchToRoute()から始めていきます。

Illuminate/Routing/Router
public function dispatchToRoute(Request $request)
{
    return $this->runRoute($request, $this->findRoute($request));
}

runRoute()の引数に渡されるfindRoute()を見ていきます。

Illuminate/Routing/Router
protected function findRoute($request)
{
    $this->current = $route = $this->routes->match($request);

    $this->container->instance(Route::class, $route);

    return $route;
}

$this->routesroutes/web.phpなどのルートファイルを読み込んで、その1つ1つのルートをIlluminate/Routing/Routeとして保持しているIlluminate/Routing/RouteCollectionです。

Illuminate/Routing/RouteCollection::match()にリクエストを渡して呼び出しています。

Illuminate/Routing/RouteCollection
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つのルートに絞っていきます。

Illuminate/Routing/RouteCollection.php
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は後退という意味でマッチングの優先順位を下げています。(ルーティングは上に書かれたものから優先される)

partition

コレクションクラスのmerge()$routes$fallbacksを結合します。これで$routesに入っていた値が優先されます。

merge

コレクションクラスのfirst()の引数に渡したコールバックでルートを探していきます。

first

Illuminate/Routing/Route.php
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に格納します。

Illuminate/Routing/RouteCompiler.php
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/CompiledRouteregexプロパティにこの正規表現をセットしています。

最終的に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()の返り値になります。

Illuminate/Routing/RouteCollection.php
// 再掲
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()にリクエストが渡されて呼び出されます。

Illuminate/Routing/Route.php
public function bind(Request $request)
{
    $this->compileRoute();

    $this->parameters = (new RouteParameterBinder($this))
                    ->parameters($request);

    $this->originalParameters = $this->parameters;

    return $this;
}

$this->compileRoute()は先程と同じなので割愛します。

parameters()でuriから値をキャプチャしていきます。

Illuminate/Routing/RouteParameterBinder.php
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/CompiledRouteregexプロパティにセットした正規表現を使って値を取得していきます。
「{}」で囲ったところに値するものを取得します。

$this->matchToKeys()で整形をします。

例
web.phpで定義したルート: '/{hoge}/{fuga}'
実際のパス: '/1/4'
整形された配列: ['hoge' => '1', 'fuga' => '4']

整形された配列をIlluminate/Routing/Routeparametersプロパティに格納します。

Illuminate/Routing/Router::findRoute()に戻ります。

Illuminate/Routing/Router.php
protected function findRoute($request)
{
    $this->current = $route = $this->routes->match($request);

    $this->container->instance(Route::class, $route);

    return $route;
}

マッチしたrouteを変数に格納し、コンテナにバインドしています。

runRoute()に戻ります。

Illuminate/Routing/Router.php
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()に参ります。

Illuminate/Routing/Router.php
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()が呼ばれます。

Illuminate/Routing/Middleware/SubstituteBindings.php
public function handle($request, Closure $next)
{
    $this->router->substituteBindings($route = $request->route());

    $this->router->substituteImplicitBindings($route);

    return $next($request);
}

明示的な結合

substituteBindings()明示的な結合と呼ばれるものを解決しています。

app/Providers/RouteServiceProvider.php
// こんな感じ
public function boot()
{
    parent::boot();

    Route::model('hoge', App\User::class);
}

Illuminate/Routing/Routeparametersプロパティに該当のものがあれば解決してくれます。

$parameters = ['hoge' => '2']ならApp\User::classのprimaryKeyが'2'のものをとってきてくれる。

暗黙的な結合

substituteImplicitBindings()は明示的な結合のように書かなくても、ある一定の法則で書けば勝手に解決してくれますよ〜のやつです。

Illuminate/Routing/Router.php
public function substituteImplicitBindings($route)
{
    ImplicitRouteBinding::resolveForRoute($this->container, $route);
}
Illuminate/Routing/ImplicitRouteBinding.php
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()を見ていきます。

Illuminate/Routing/Route.php
public function signatureParameters($subClass = null)
{
    return RouteSignatureParameters::fromAction($this->action, $subClass);
}

fromAction()$this->actionを渡して呼び出しています。

$this->actionにはマッチしたルートのコントローラーとメソッドが以下の形で保持されています。

$this->action = ['uses' => 'HogeController@method', 'controller' => 'HogeController@method']

Illuminate/Routing/RouteSignatureParameters.php
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()を見ていきます。

Illuminate/Routing/RouteSignatureParameters.php
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クラスをつかってメソッドの引数に指定されている名前を取得して配列で返します。

ReflectionMethodの参考

先程取得した配列をフィルターにかけていきます。

$p->getClass()でタイプヒンティングされているかを検証し、$p->getClass()->isSubclassOfIlluminate/Contracts/Routing/UrlRoutableを実装しているかを検証しています。

Illuminate/Contracts/Routing/UrlRoutableIlluminate/Database/Eloquent/Modelが実装しているのでEloquentは対象になります。

コントローラーのparameter解析が終わったのでforeachに戻ります。

Illuminate/Routing/ImplicitRouteBinding.php
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してデータを引っ張ってきます。

Illuminate/Database/Eloquent/Model.php
public function resolveRouteBinding($value)
{
    return $this->where($this->getRouteKeyName(), $value)->first();
}

$route->setParameter()parametersプロパティを上書きします。

これでルートモデルバインディングは解決です。

実際にコントローラーに引数を渡す

Illuminate/Routing/ControllerDispatcher::dispatch()の中の$controller->{$method}(...array_values($parameters))でparametersの値を渡しています。

Illuminate/Routing/ControllerDispatcher.php
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のところはわかりませんでしたが大体の流れはつかめたと思います。やったね!

9
11
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
9
11