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

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

[laravelでweb.phpが読み込まれるまでの道のり](https://qiita.com/wim/items/b177523ab3b746637711)を読んでおくとさらにわかりやすいかもしれません。

## Let's read

### ルートが決定されるまで

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

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

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

```php:Illuminate/Routing/Router
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()`にリクエストを渡して呼び出しています。

```php: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つのルートに絞っていきます。

```php: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](https://readouble.com/laravel/5.8/ja/collections.html#method-partition)

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

[merge](https://readouble.com/laravel/5.8/ja/collections.html#method-merge)

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

[first](https://readouble.com/laravel/5.8/ja/collections.html#method-first)

```php: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`に格納します。

```php: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](https://laravel.com/docs/5.8/routing#parameters-optional-parameters)を取得します。

`preg_replace()`ではOptionalParameterから'?'を取り除きます。

```
例) {hoge?} -> {hoge}
```


`SymfonyRoute`からはより細かい話になってくるのでざっくり解説します。（というか読めない...）

`vendor/symfony/routing/RouteCompiler::compilePattern()`でurlのpathからどのような正規表現で値を抜き出すかを決めてるっぽいです。

コメントで「「{}」で囲まれたすべての変数に一致し....」とあるのでおそらくそうだと思います。（laravelも「{}」の中身を抜き出すので）

```php
// 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()`で取得します。

```php
public static function getValidators()
{
    return static::$validators = [
        new UriValidator, new MethodValidator,
        new SchemeValidator, new HostValidator,
    ];
}
```

それぞれのクラスの`match()`を使ってマッチするuriを探していきます。すべてのバリデーションを通過したrouteが返されます。

これが`Illuminate/Routing/RouteCollection::matchAgainstRoutes()`の返り値になります。

```php: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()`にリクエストが渡されて呼び出されます。

```php: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から値をキャプチャしていきます。

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

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

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

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


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

```php: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()`に戻ります。

```php: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()`に参ります。

```php: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を処理している流れをまとめてみた)](https://qiita.com/wim/items/869bf668b98b37a6e2fc)

ミドルウェアは`app/Http/Kernel.php`に定義されているルートミドルウェアが使われています。
その中にルートモデルバインディングを行っている`\Illuminate\Routing\Middleware\SubstituteBindings::class'があります。

```php
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()`が呼ばれます。

```php: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()`は[明示的な結合](https://readouble.com/laravel/5.8/ja/routing.html)と呼ばれるものを解決しています。

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

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

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

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

#### 暗黙的な結合

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

```php:Illuminate/Routing/Router.php
public function substituteImplicitBindings($route)
{
    ImplicitRouteBinding::resolveForRoute($this->container, $route);
}
```

```php: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()`を見ていきます。

```php: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']`

```php: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()`を見ていきます。

```php: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の参考](https://3v4l.org/cFOVa)

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

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

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

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

```php: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']
```

```php
$parameterValue /* 1 */ = $parameters[$parameterName /* hoge */];
```

`$instance = $container->make($parameter->getClass()->name);`でタイプヒンティングの名前からモデルインスタンスを生成します。

`resolveRouteBinding()`で `$parameterValue`を使ってwhereしてデータを引っ張ってきます。

```php: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の値を渡しています。

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