LaravelのFormRequestでバリデートまでのソースコードを読む


## FormRequestでvalidateされるまで

### FormRequestクラスの誕生

```php:Illuminate/Routing/ControllerDispatcher.php
public function dispatch(Route $route, $controller, $method)
{
    $parameters = $this->resolveClassMethodDependencies(
        $route->parametersWithoutNulls(), $controller, $method
    );

    //...
}
```

`resolveClassMethodDependencies()`が怪しいので見ていきます。

```php:Illuminate/Routing/RouteDependencyResolverTrait.php
protected function resolveClassMethodDependencies(array $parameters, $instance, $method)
{
    //...

    return $this->resolveMethodDependencies(
        $parameters, new ReflectionMethod($instance, $method)
    );
}
```

`resolveMethodDependencies()`の返り値をreturnしているようです。

ならば見ていきましょう。

```php:Illuminate/Routing/RouteDependencyResolverTrait.php
public function resolveMethodDependencies(array $parameters, ReflectionFunctionAbstract $reflector)
{
    $instanceCount = 0;

    $values = array_values($parameters);

    foreach ($reflector->getParameters() as $key => $parameter) {
        $instance = $this->transformDependency(
            $parameter, $parameters
        );

        if (! is_null($instance)) {
            $instanceCount++;

            $this->spliceIntoParameters($parameters, $key, $instance);
        } elseif (! isset($values[$key - $instanceCount]) &&
                  $parameter->isDefaultValueAvailable()) {
            $this->spliceIntoParameters($parameters, $key, $parameter->getDefaultValue());
        }
    }

    return $parameters;
}
```

まず`parameters`には基本的にルートモデルバインディングで解決されたものが入っています。

```php
$parameters = ['user' => Userのインスタンス]
```

foreachのkeyとparameterの部分は、`0 => ReflectionParameterクラス`となります。

[ReflectionParameterクラス リファレンス](https://www.php.net/manual/ja/class.reflectionparameter.php)

`transforDependency()`にReflectionParameterクラスと`$parameters`を渡しています。
今回は`$parameters`には何も入っていない体でいきます。

```php:Illuminate/Routing/RouteDependencyResolverTrait.php
protected function transformDependency(ReflectionParameter $parameter, $parameters)
{
    $class = $parameter->getClass();

    if ($class && ! $this->alreadyInParameters($class->name, $parameters)) {
        return $parameter->isDefaultValueAvailable()
            ? $parameter->getDefaultValue()
            : $this->container->make($class->name);
    }
}
```

`$parameter->getClass()`でメソッドにタイプヒンティングしてあるクラス名を取得しています。
stringやintの場合はnullになります。

`alreadyInParameters`はその名の通り、すでにルートモデルバインディングで解決されたやつはtrueを返すやで〜ってやつなので今回は関係ないです。

return文のところは、デフォルト引数だとその値を使って、そうじゃなければクラス名からインスタンスを生成します。
（ここでFormRequestのインスタンスが生成されます）

そのままなんやかんやあってコントローラーメソッドに引き渡されていきます。

### 解決時の処理

FormRequestのインスタンスを生成するだけでバリデーションしてないじゃん！と思ったそこのあなた！

Laravelの魔法がここから始まります。

まずFormRequestはコンテナで解決されているので解決方法を見ていきましょう。そうサービスプロバイダーです。


```php:Illuminate/Foundation/Providers/FormRequestServiceProvider.php
public function boot()
{
    $this->app->afterResolving(ValidatesWhenResolved::class, function ($resolved) {
        $resolved->validateResolved();
    });

    $this->app->resolving(FormRequest::class, function ($request, $app) {
        $request = FormRequest::createFrom($app['request'], $request);

        $request->setContainer($app)->setRedirector($app->make(Redirector::class));
    });
}
```

`resolving`, `afterResolving`は第一引数に渡されたクラスがコンテナで解決されたときに発行されるイベントをもとに実行されます。

さきほど見たようにFormRequestが生成された時点で`resolving`で登録したコールバックが呼ばれます。

ここの`resolving`はFormRequestにコンテナとリダイレクタとリクエストをセットしているぐらいです。

FormRequestは`ValidatesWhenResolved`インタフェースを実装しているので、`afterResolving`のコールバックも呼ばれます。

ここで`FormRequest#validateResolved`が呼ばれます。このメソッドはトレイとに実装されています。

```php:Illuminate/Validation/ValidatesWhenResolvedTrait.php
public function validateResolved()
{
    $this->prepareForValidation();

    if (! $this->passesAuthorization()) {
        $this->failedAuthorization();
    }

    $instance = $this->getValidatorInstance();

    if ($instance->fails()) {
        $this->failedValidation($instance);
    }

    $this->passedValidation();
}
```

`prepareForValidation()`はデフォルトではなにもしないメソッドになってます。
バリデーションの前になにかしたいことがあれば、このメソッドをオーバーライドすればよさそうです。

`getValidatorInstance()`でバリデータを取得していそうです。

```php:Illuminate/Foundation/Http/FormRequest.php
protected function getValidatorInstance()
{
    //...

    $factory = $this->container->make(ValidationFactory::class);

    if (method_exists($this, 'validator')) {
        $validator = $this->container->call([$this, 'validator'], compact('factory'));
    } else {
        $validator = $this->createDefaultValidator($factory);
    }

    //...

    $this->setValidator($validator);

    return $this->validator;
}
```

FormRequestには`validator()`がないので、`$this->createDefaultValidator($factory)`が呼ばれます。

```php:Illuminate/Foundation/Http/FormRequest.php
protected function createDefaultValidator(ValidationFactory $factory)
{
    return $factory->make(
        $this->validationData(), $this->container->call([$this, 'rules']),
        $this->messages(), $this->attributes()
    );
}
```

これで`Illuminate\Contracts\Validation\Validator.php`を作成して、プロパティにセットしつつreturnします。


```php:Illuminate/Validation/ValidatesWhenResolvedTrait.php
public function validateResolved()
{
    $this->prepareForValidation();

    if (! $this->passesAuthorization()) {
        $this->failedAuthorization();
    }

    $instance = $this->getValidatorInstance();

    if ($instance->fails()) {
        $this->failedValidation($instance);
    }

    $this->passedValidation();
}
```

`$instance->fails()`でバリデーションを行って、バリデーションに引っかかればエラーが投げられます。

`$this->passedValidation()`はなにもしないメソッドなので、バリデーション後にしたい処理をオーバーライドして記述すればよいでしょう。

既存のルールの判定メソッドは `Illuminate\Validation\Concerns\ValidatesAttributes` にありました。