26
25

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

バリデーションエラー/POST送信時のLaravelの挙動を追う

Last updated at Posted at 2019-12-18

本記事はうるる Advent Calendar 2019 19日目の記事です。

はじめに

Laravelでは、フォームリクエストにルールを定義することで、フォームバリデーションを容易に作成することができます。

バリデーションに引っかかった場合、エラー内容はフラッシュメッセージとしてセッションに保存され、ビューテンプレート上に表示させることができます。(あるいは、AJAXリクエストが使用されている場合はステータスコード422が返却されます)

この時、Laravelの裏側で起きている中身については、書籍や記事などの情報が少なかったので、調査してみました。

サンプル

ごく単純なフォーム送信を、サンプルコードとして扱います。
フォームのname属性としてはname・numberを要素に持ち、それぞれに対してバリデーションルールを設定しています。
また、バリデーションルールは今回はフォームリクエストに定義しています。

<?php

namespace App\Http\Controllers\Vali;

use App\Http\Controllers\Controller;
use App\Http\Requests\ValidateRequest;

class ValidatesController extends Controller
{
    public function index()
    {
        if ($this->sessionExists()) {
            session()->forget(['name', 'number']);
        }
        return view('validateForm');
    }

    public function store(ValidateRequest $request)
    {
        session([
            'name' => $request->input('name'),
        ]);
        session([
            'number' => $request->input('number'),
        ]);
        return redirect()->route('validateSuccess');
    }

    public function show()
    {
        if (! $this->sessionExists()) {
            return redirect()->route('validate');
        }
        return view('validateSuccess', [
            'name' => session('name'),
            'number' => session('number')
        ]);
    }

    private function sessionExists(): bool
    {
        return (session()->exists('name') || session()->exists('number'));
    }
}
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class ValidateRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'name' => 'required',
            'number' => 'required|integer',
        ];
    }
}

テンプレートとしては、フォーム入力時(validateForm.blade.php)・バリデーション通過後(validateSuccess.blade.php)の2種類を用意しています。
(bodyタグ以下のみ記載)

<body>
    <div>
        <h2>フォーム</h2>
        <p>内容を入力してください</p>
    </div>
    <div>
        <form action="{{ route('validateStore') }}" method="post">
            {{ csrf_field() }}
            <div>
                <label for="name">名前:</label><br>
                <input type="text" name="name" size="30">
            </div>
            <div>
                <label for="kind">数値:</label><br>
                <input type="number" name="number">
            </div>
            <div>
                <input type="submit" value="送信">
            </div>
        </form>
    </div>
    <div>
        @if(count($errors) > 0)
            {{ $errors }}
        @endif
    </div>
</body>
<body>
    <div>
        <h2>バリデーション成功</h2>
        <p>{{ $name }}</p>
        <p>{{ $number }}</p>
    </div>
</body>

ルーティングは、下記の3つを用意しています。

Route::get('validate', 'Vali\ValidatesController@index')->name('validate');

Route::post('validate', 'Vali\ValidatesController@store')->name('validateStore');

Route::get('success', 'Vali\ValidatesController@show')->name('validateSuccess');

処理の動きとしては、下記のようになります。

【フォーム入力時】
スクリーンショット 2019-12-18 23.25.20.png

【バリデーション成功時】
スクリーンショット 2019-12-19 3.35.06.png

【バリデーション失敗時(フォームに何も入力しなかった場合)】
スクリーンショット 2019-12-18 8.27.05.png

これらの一連の処理について、内部で何が起きているのかを順に説明していきます。

バリデーションエラー/POST送信時の内部の動きを追う

リクエストはどこへ行くのか

送信されたHTTPリクエストは、エントリポイントであるpublic/index.phpで最初の処理が行われます。これはLaravelのライフサイクルの話です。public/index.phpは、下記のような記述がなされているファイルです。(コメントは省略)

<?php

define('LARAVEL_START', microtime(true));

require __DIR__.'/../vendor/autoload.php';

$app = require_once __DIR__.'/../bootstrap/app.php';

$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);

$response = $kernel->handle(
    $request = Illuminate\Http\Request::capture()
);

$response->send();

$kernel->terminate($request, $response);

上記の中で、HTTPリクエストはIlluminate\Http\Request::capture()の部分でRequestオブジェクトが生成されます。そこで、このメソッドの記述をみてみます。

/**
 * Create a new Illuminate HTTP request from server variables.
 *
 * @return static
 */
public static function capture()
{
    static::enableHttpMethodParameterOverride();

    return static::createFromBase(SymfonyRequest::createFromGlobals());
}

上記は、Illuminate\Http\Requestクラスに定義されています。この記述だけだと、何が起きているのか分かりません。この処理の実体は、Illuminate\Http\Requestクラスが継承しているSymfony\Component\HttpFoundation\Requestクラスにあります。

/**
 * Creates a new request with values from PHP's super globals.
 *
 * @return static
 */
public static function createFromGlobals()
{
    $request = self::createRequestFromFactory($_GET, $_POST, [], $_COOKIE, $_FILES, $_SERVER);

    if (0 === strpos($request->headers->get('CONTENT_TYPE'), 'application/x-www-form-urlencoded')
        && \in_array(strtoupper($request->server->get('REQUEST_METHOD', 'GET')), ['PUT', 'DELETE', 'PATCH'])
    ) {
        parse_str($request->getContent(), $data);
        $request->request = new ParameterBag($data);
    }

    return $request;
}

ここで、createRequestFromFactory()の引数として、スーパーグローバル変数が渡されていることが分かります。この部分で、HTTPリクエスト情報がLaravelに渡されています。
そして、createRequestFromFactory()によって同クラスのインスタンスとして、各プロパティに格納されます。

private static function createRequestFromFactory(array $query = [], array $request = [], array $attributes = [], array $cookies = [], array $files = [], array $server = [], $content = null)
{
    if (self::$requestFactory) {
        $request = (self::$requestFactory)($query, $request, $attributes, $cookies, $files, $server, $content);

        if (!$request instanceof self) {
            throw new \LogicException('The Request factory must return an instance of Symfony\Component\HttpFoundation\Request.');
        }

        return $request;
    }

    return new static($query, $request, $attributes, $cookies, $files, $server, $content);
}

例えば、バリデーション時に参照されるPOSTリクエストの値については、同ファイルのプロパティである$requestに情報が格納されます。(厳密には、Symfony\Component\HttpFoundation\ParameterBagクラスに引き渡されています)

バリデーションはどこで行われているのか

続いて、実際にバリデーションロジックを実行している箇所です。

今回はフォームリクエストを利用していますが、その際には下記のIlluminate\Foundation\Providers\FormRequestServiceProviderの処理が動きます。

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));
    });
}

まず、FormRequest::classのインスタンス(フォームリクエストは継承している親クラスのインスタンス)が生成された直後に、resolvingメソッドの第2引数のクロージャが実行されます。
ここでは、Illuminate\Http\RequestクラスのcreateFromメソッドによって、リクエストインスタンスが生成されます。

そして、resolvingメソッドの実行後にafterResolvingメソッドが実行されますが、こちらでは第1引数であるValidatesWhenResolvedクラスのインスタンス生成が終わった直後にクロージャが実行され、validateResolved()メソッドが実行されます。

validateResolved()メソッドは、Illuminate\Validation\ValidatesWhenResolvedTraitに実体が記述されており、中身は下記のようになっています。

/**
* Validate the class instance.
*
* @return void
*/
public function validateResolved()
{
    $this->prepareForValidation();

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

    $instance = $this->getValidatorInstance();

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

この処理については、内部でより細かい処理が動いているので、分けて説明していきます。

Validatorクラスのインスタンスが作成されるまで

上記処理の中で、$instanceに格納される$this->getValidatorInstance()は、Illuminate\Foundation\Http\FormRequestに実体が記述されています。

/**
* Get the validator instance for the request.
*
* @return \Illuminate\Contracts\Validation\Validator
*/
protected function getValidatorInstance()
{
    if ($this->validator) {
        return $this->validator;
    }

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

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

    if (method_exists($this, 'withValidator')) {
        $this->withValidator($validator);
    }

    $this->setValidator($validator);

    return $this->validator;
}

上記処理においては、Illuminate\Contracts\Validation\Validatorクラスのインスタンスが返却されています。
実際にインスタンスを作成しているのは、同じくFormRequestクラスに定義されている、createDefaultValidator()メソッドです。

/**
* Create the default validator instance.
*
* @param \Illuminate\Contracts\Validation\Factory $factory
* @return \Illuminate\Contracts\Validation\Validator
*/
protected function createDefaultValidator(ValidationFactory $factory)
{
    return $factory->make(
        $this->validationData(), $this->container->call([$this, 'rules']),
        $this->messages(), $this->attributes()
    );
}

ここで、$factory->make()の引数については、下記のようになっています(今回の処理の場合)

// $this->validationData()の返り値(フォームの入力値)
array: [
  "_token" => "省略"
  "name" => "hoge"
  "number" => null
]

// $this->container->call([$this, 'rules’])の返り値(バリデーションルール)
// フォームリクエストに定義した関数rule()の、returnによる返却値が返される。
array: [
  "name" => "required"
  "number" => "required|integer"
]

// $this->messages()・$this->attributes()は、空配列が返却される
array: []

今回の場合、フォームのname属性の各要素に対する入力値の情報が$this->validationData()によって渡され、それぞれ定義したバリデーションルールが$this->container->call([$this, 'rules’])によって渡されていることが分かります。
フォームの入力値については、$this->validationData()内部で、リクエストインスタンスをall()で取得する処理によって中身が取得されています。
バリデーションルールは現時点では、フォームリクエストに記述したそのままの形が返却されています。

上記の引数が渡された$factory->make()メソッドは、Illuminate\Validation\Factoryクラスに実体が記述されています。その中でも実処理が行われているのは、同クラスに定義されているresolve()メソッドです。

/**
 * Resolve a new Validator instance.
 *
 * @param  array  $data
 * @param  array  $rules
 * @param  array  $messages
 * @param  array  $customAttributes
 * @return \Illuminate\Validation\Validator
 */
protected function resolve(array $data, array $rules, array $messages, array $customAttributes)
{
    if (is_null($this->resolver)) {
        return new Validator($this->translator, $data, $rules, $messages, $customAttributes);
    }

    return call_user_func($this->resolver, $this->translator, $data, $rules, $messages, $customAttributes);
}

上記によって、Validatorクラスのインスタンスが作成されます。
実際に作成されるインスタンスは、下記のような情報を含んでいます。

Validator {
  ...
  #failedRules: []
  #messages: null
  #data: array: [
    "_token" => "省略"
    "name" => "hoge"
    "number" => null
  ]
  #initialRules: array: [
    "name" => "required"
    "number" => "required|integer"
  ]
  #rules: array: [
    "name" => array:1 [
      0 => "required"
    ]
    "number" => array: [
      0 => "required"
      1 => "integer"
    ]
  ]
...
}

この時、

  • フォームのname属性の要素名
  • フォームの入力値
  • それぞれに設定されたルール(バリデーション)

という3つの情報が含まれていることに着目してください。

バリデーション処理が実行される箇所

バリデーションインスタンスによって、バリデーションインスタンスが作成される処理までを見てきました。
再度、バリデーション処理の本体であるvalidateResolved()メソッドに話を戻します。

public function validateResolved()
{
    $this->prepareForValidation();

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

    $instance = $this->getValidatorInstance();

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

$instance->fails()によって、条件分岐構文に処理が移されています。fails()は、Illuminate\Validation\Validatorクラスに定義されたメソッドです。

/**
* Determine if the data fails the validation rules.
*
* @return bool
*/
public function fails()
{
    return ! $this->passes();
}

処理を見ると、$this->passes()の真偽値を返却しているようです。そこで、同クラスに定義されたpasses()の中身を見てみます。

/**
* Determine if the data passes the validation rules.
*
* @return bool
*/
public function passes()
{
    $this->messages = new MessageBag;

    [$this->distinctValues, $this->failedRules] = [[], []];

    // We'll spin through each rule, validating the attributes attached to that
    // rule. Any error messages will be added to the containers with each of
    // the other error messages, returning true if we don't have messages.
    foreach ($this->rules as $attribute => $rules) {
        $attribute = str_replace('\.', '->', $attribute);

        foreach ($rules as $rule) {
            $this->validateAttribute($attribute, $rule);

                if ($this->shouldStopValidating($attribute)) {
                    break;
                }
        }
    }


    // Here we will spin through all of the "after" hooks on this validator and
    // fire them off. This gives the callbacks a chance to perform all kinds
    // of other validation that needs to get wrapped up in this operation.
    foreach ($this->after as $after) {
        call_user_func($after);
    }

    return $this->messages->isEmpty();
}

処理を見ると、$this->messagesの有無によって、真偽値を返却していることが分かります。
$this->messagesには、バリデーションに引っかかった際のエラーメッセージが格納されていきます。
つまり、エラーメッセージの有無によって、trueを返すかfalseを返すかが分かれているのです。

もう少し処理を詳しく見ていくと、どうやら$this->rulesを展開しているforeach構文の中で、実際にバリデーションロジックの処理が行われていることが分かります。
ここで、$rulesというのはValidatorクラスのプロパティになります。$rulesの中身としては、先ほどみたValidatorインスタンスの「rules」と同値になります。

array: [
  "name" => array:1 [
    0 => "required"
  ]
  "number" => array:2 [
    0 => "required"
    1 => "integer"
  ]
]

そのため、foreach構文の中においては、$attributeがフォームのname属性の各要素名を表し、$rulesがそれに対して設定されたバリデーションルールを示すこととなります。

validateAttribute()メソッドの中身

foreach構文の中で、実際にバリデーション処理が実行されているのは$this->validateAttribute($attribute, $rule)の部分です。

/**
 * Validate a given attribute against a rule.
 *
 * @param  string  $attribute
 * @param  string  $rule
 * @return void
 */
protected function validateAttribute($attribute, $rule)
{
    $this->currentRule = $rule;

    [$rule, $parameters] = ValidationRuleParser::parse($rule);

    if ($rule == '') {
        return;
    }

    // First we will get the correct keys for the given attribute in case the field is nested in
    // an array. Then we determine if the given rule accepts other field names as parameters.
    // If so, we will replace any asterisks found in the parameters with the correct keys.
    if (($keys = $this->getExplicitKeys($attribute)) &&
        $this->dependsOnOtherFields($rule)) {
        $parameters = $this->replaceAsterisksInParameters($parameters, $keys);
    }

    // input value to form
    $value = $this->getValue($attribute);

    // If the attribute is a file, we will verify that the file upload was actually successful
    // and if it wasn't we will add a failure for the attribute. Files may not successfully
    // upload if they are too large based on PHP's settings so we will bail in this case.
    if ($value instanceof UploadedFile && ! $value->isValid() &&
        $this->hasRule($attribute, array_merge($this->fileRules, $this->implicitRules))
    ) {
        return $this->addFailure($attribute, 'uploaded', []);
    }

    // If we have made it this far we will make sure the attribute is validatable and if it is
    // we will call the validation method with the attribute. If a method returns false the
    // attribute is invalid and we will add a failure message for this failing attribute.
    $validatable = $this->isValidatable($rule, $attribute, $value);

    if ($rule instanceof RuleContract) {
        return $validatable
                ? $this->validateUsingCustomRule($attribute, $value, $rule)
                : null;
    }

    $method = "validate{$rule}";

    if ($validatable && ! $this->$method($attribute, $value, $parameters, $this)) {
        $this->addFailure($attribute, $rule, $parameters);
    }
}

処理が長いため、重要な箇所だけ抜粋して取り上げていきます。
まず、処理内において、同クラスに定義されたgetValue()メソッドによって、フォームの入力値が取得されます。

// input value to form
// $attribute: フォームのname属性の要素名
$value = $this->getValue($attribute);

続いて、こちらも同クラスに定義されたisValidatable()メソッドによって、バリデーション可能かどうかを判定しています。返り値では真偽値が返却されます。

// If we have made it this far we will make sure the attribute is validatable and if it is
// we will call the validation method with the attribute. If a method returns false the
// attribute is invalid and we will add a failure message for this failing attribute.
// $rule: バリデーションルール
// $attribute: フォームのname属性の要素名
// $value: フォームの入力値
$validatable = $this->isValidatable($rule, $attribute, $value);

この時$ruleが、フォームリクエストに設定したバリデーションルールを表していることに着目してください。今回の実装ではたとえば、formのname要素に対して「required」のルールを付与していました。

処理が煩雑になっているので深くは追いませんが、上記の「required」は、処理内でValidationRuleParser::parse($rule)に引き渡されることにより、「Required」という形に変換されます。そして、変数$methodに、下記のように格納されます。

$method = "validate{$rule}";

上記で返却されるメソッド名は、validateRequiredになります。

バリデーション実部分

さて、いよいよバリデーション実部分です。
バリデーション処理は、下記の箇所で行われます。

if ($validatable && ! $this->$method($attribute, $value, $parameters, $this)) {
    $this->addFailure($attribute, $rule, $parameters);
}

上記の$this->$methodでは、整形されたメソッド名(例では「validateRequired」)です。それでは、このメソッド名はどこから呼び出されているのかというと、Validatorクラスの冒頭でuseしている、Illuminate\Validation\Concerns\ValidatesAttributesトレイトになります。

/**
 * Validate that a required attribute exists.
 *
 * @param  string  $attribute
 * @param  mixed   $value
 * @return bool
 */
public function validateRequired($attribute, $value)
{
    if (is_null($value)) {
        return false;
    } elseif (is_string($value) && trim($value) === '') {
        return false;
    } elseif ((is_array($value) || $value instanceof Countable) && count($value) < 1) {
        return false;
    } elseif ($value instanceof File) {
        return (string) $value->getPath() !== '';
    }

    return true;
}

メソッド名として取得したvalidateRequired()が、定義されていることが分かります。
上記に限らず、Laravelのバリデーションルールとして定義された実処理は、ValidatesAttributesトレイトに記述されています。

たとえば、requireルールの場合、入力値($value)が空欄の場合に、falseが返却されます。そうでなければtrueが返却されます。
その他のルールについても、ルールに合致していればtrueが返却され、ルールに合致していなければ(バリデーションに引っかかれば)falseが返される仕組みです。

エラーメッセージが付与される処理

上記メソッドの返り値がfalseだった場合、条件分岐構文によって、下記の処理が実行されます。

$this->addFailure($attribute, $rule, $parameters);

addFailure()メソッドの中で、$this->messageにバリデーションエラーメッセージを格納していきます。実際にこの処理が行われているのは、同メソッド内の下記の部分です。

$this->messages->add($attribute, $this->makeReplacements(
    $this->getMessage($attribute, $rule), $attribute, $rule, $parameters
));

たとえば、フォームのnumber要素にrequiredのルールを設定し、このバリデーションルールに引っかかった場合、デフォルト状態では「The number field is required.」のようなメッセージが返却されるかと思います。

これらのメッセージはどこに定義されているのかというと、resources/lang/en/validation.phpに、各バリデーションルールに応じた初期状態のエラーメッセージがまとめられています。

validation.phpにおいては、フォームの要素部分はプレースホルダで記述がされています。処理の中で、実際にバリデーションルールに引っかかった要素名が、各エラーメッセージに割り振られるというわけです。

エラーメッセージが付与された後

上記の処理によって、バリデーション失敗時に$this->messagesにエラーメッセージが格納されることで、Illuminate\Validation\Validatorクラスのメソッドpasses()はfalseを返却します。なぜなら、passes()は下記を返却するメソッドだからです。

return $this->messages->isEmpty();

passes()がfalseを返却すると、同クラスのメソッドfails()はtrueを返却します。そして、大元の処理であるvalidateResolved()メソッド(Illuminate\Validation\ValidatesWhenResolvedTraitでは、failedValidation()メソッドが実行されます。

public function validateResolved()
{
    $this->prepareForValidation();

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

    $instance = $this->getValidatorInstance();

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

長くなりましたが、ここまでがバリデーション実処理部分になります。

バリデーションエラーメッセージの出力

最後に、バリデーションに引っかかった際、どのような動きによってエラーメッセージが出力されるのか、ということについて見ていきます。

Laravelのドキュメントを見ると、バリデーションのエラーメッセージの出力に関しては以下のように説明されています。

Laravelは自動的にユーザーを以前のページヘリダイレクトします。付け加えて、バリデーションエラーは全部自動的にフラッシュデータとしてセッションへ保存されます。

Laravelドキュメント(日本語訳)

この部分について、内部ではどのような動きをしているのかを追っていきたいと思います。
まずは、エラーメッセージがセッションに保存される箇所からです。

バリデーションエラーがセッションに保存されるまで

上でも見たように、バリデーションに引っかかって$instance->fails() = falseが返却されると、failedValidation()メソッドが実行されます。これは、Illuminate\Foundation\Http\FormRequestクラスに定義されています。

/**
 * Handle a failed validation attempt.
 *
 * @param  \Illuminate\Contracts\Validation\Validator  $validator
 * @return void
 *
 * @throws \Illuminate\Validation\ValidationException
 */
protected function failedValidation(Validator $validator)
{
    throw (new ValidationException($validator))
                ->errorBag($this->errorBag)
                ->redirectTo($this->getRedirectUrl());
}

上記をみると、ValidationExceptionのエラーが投げられていることが確認できます。
そこで、エラーハンドルに関する記述がなされているIlluminate\Foundation\Exceptions\Handlerクラスの記述を見てみます。
今回の処理に関係している記述は、まずは下記です。

/**
 * Render an exception into a response.
 *
 * @param  \Illuminate\Http\Request  $request
 * @param  \Exception  $e
 * @return \Illuminate\Http\Response|\Symfony\Component\HttpFoundation\Response
 */
public function render($request, Exception $e)
{
    if (method_exists($e, 'render') && $response = $e->render($request)) {
        return Router::toResponse($request, $response);
    } elseif ($e instanceof Responsable) {
        return $e->toResponse($request);
    }

    $e = $this->prepareException($e);

    if ($e instanceof HttpResponseException) {
        return $e->getResponse();
    } elseif ($e instanceof AuthenticationException) {
        return $this->unauthenticated($request, $e);
    } elseif ($e instanceof ValidationException) {
        return $this->convertValidationExceptionToResponse($e, $request);
    }

    return $request->expectsJson()
                    ? $this->prepareJsonResponse($request, $e)
                    : $this->prepareResponse($request, $e);
}

ここで、elseif ($e instanceof ValidationException)の箇所に着目してください。バリデーションエラーが投げられた際はこちらの条件に該当し、convertValidationExceptionToResponse()メソッドが実行されていることが分かります。

protected function convertValidationExceptionToResponse(ValidationException $e, $request)
{
    if ($e->response) {
        return $e->response;
    }

    return $request->expectsJson()
                ? $this->invalidJson($request, $e)
                : $this->invalid($request, $e);
    }

convertValidationExceptionToResponse()メソッドは、上記のような記述のメソッドです。(説明は省略しています。)
この中で、今回の処理ではinvalid()が実行されます。

/**
 * Convert a validation exception into a response.
 *
 * @param  \Illuminate\Http\Request  $request
 * @param  \Illuminate\Validation\ValidationException  $exception
 * @return \Illuminate\Http\Response
 */
protected function invalid($request, ValidationException $exception)
{
    return redirect($exception->redirectTo ?? url()->previous())
                ->withInput(Arr::except($request->input(), $this->dontFlash))
                ->withErrors($exception->errors(), $exception->errorBag);
}

withErrors()メソッドに着目します。これは、Illuminate\Http\RedirectResponseクラスに定義されたメソッドで、下記のような記述になっています。

/**
 * Flash a container of errors to the session.
 *
 * @param  \Illuminate\Contracts\Support\MessageProvider|array|string  $provider
 * @param  string  $key
 * @return $this
 */
public function withErrors($provider, $key = 'default')
{
    $value = $this->parseErrors($provider);

    $errors = $this->session->get('errors', new ViewErrorBag);

    if (! $errors instanceof ViewErrorBag) {
        $errors = new ViewErrorBag;
    }

    $this->session->flash(
        'errors', $errors->put($key, $value)
    );

    return $this;
}

こちらの記述によって、エラーメッセージがフラッシュデータとして、セッションに保存されていることが分かるかと思います。$this->session->flash()の部分で、確かに「errors」をキーとして、セッションに保存されています。

また、$errors->put($key, $value)の箇所では、MessageBagインスタンスを内包するViewErrorBagクラスが返却されており、内部にバリデーションエラーメッセージが格納されています。

自動的にユーザーを以前のページヘリダイレクトする動きについて

続いて、バリデーションエラー発生時に自動的に以前のページへリダイレクトされる処理についてです。
再び、FormRequestクラスのfailedValidation()メソッドに着目してください。

protected function failedValidation(Validator $validator)
{
    throw (new ValidationException($validator))
                ->errorBag($this->errorBag)
                ->redirectTo($this->getRedirectUrl());
}

上記処理において、redirectTo()の箇所でリダイレクトするURLを設定しています。redirectTo()自体は、Illuminate\Validation\ValidationExceptionクラスのメソッドであり、こちらのクラスの$redirectToプロパティに、引数として渡されたURLを設定します。

そこで、引数部分の$this->getRedirectUrl()に着目します。こちらのメソッドは、FormRequestクラスに下記のように定義されています。

/**
 * Get the URL to redirect to on a validation error.
 *
 * @return string
 */
protected function getRedirectUrl()
{
    $url = $this->redirector->getUrlGenerator();

    if ($this->redirect) {
        return $url->to($this->redirect);
    } elseif ($this->redirectRoute) {
        return $url->route($this->redirectRoute);
    } elseif ($this->redirectAction) {
        return $url->action($this->redirectAction);
    }

    return $url->previous();
}

設定されたリダイレクトルートに応じて、返り値を振り分ける処理です。ただし以前のページに自動的にリダイレクトする動きの場合、一番最後に記述のあるprevious()メソッドが重要な役割を持ちます。previous()メソッドは、Illuminate\Routing\UrlGeneratorに定義されたメソッドです。

/**
 * Get the URL for the previous request.
 *
 * @param  mixed  $fallback
 * @return string
 */
public function previous($fallback = false)
{
    $referrer = $this->request->headers->get('referer');

    $url = $referrer ? $this->to($referrer) : $this->getPreviousUrlFromSession();

    if ($url) {
        return $url;
    } elseif ($fallback) {
        return $this->to($fallback);
    }

    return $this->to('/');
}

$this->request->headers->get('referer')の部分で、リクエストインスタンスより、Refererヘッダー情報を取得しています。この箇所によって、直前のページ(ここでは、フォーム送信が行われたページ)のURL情報が取得されます。

ページリダイレクトが行われている箇所

上記で、ValidationExceptionクラスの$redirectToプロパティに、直前のページのURLが設定されるまでの動きを見てきました。
最後に、実際にページリダイレクトが行われている処理についてです。再度、Illuminate\Foundation\Exceptionsクラスのinvalid()メソッドの処理を記します。

protected function invalid($request, ValidationException $exception)
{
    return redirect($exception->redirectTo ?? url()->previous())
                ->withInput(Arr::except($request->input(), $this->dontFlash))
                ->withErrors($exception->errors(), $exception->errorBag);
}

冒頭で、Laravelのヘルパ関数のredirect()が記述され、引数として$exception->redirectToが渡されていますね。$exception->redirectToには直前のページのURLが設定されていますから、このページURLにリダイレクトされることが分かります。

補足:ビューの$errorsにエラー情報の紐付けを行う箇所

補足的にはなりますが、セッションにフラッシュメッセージとして保存されたバリデーションエラーを、ビューの$errorsに紐付ける処理については、Illuminate\View\Middleware\ShareErrorsFromSessionクラスのhandle()メソッド内で行われています。

/**
 * Handle an incoming request.
 *
 * @param  \Illuminate\Http\Request  $request
 * @param  \Closure  $next
 * @return mixed
 */
public function handle($request, Closure $next)
{
    // If the current session has an "errors" variable bound to it, we will share
    // its value with all view instances so the views can easily access errors
    // without having to bind. An empty bag is set when there aren't errors.
    $this->view->share(
         'errors', $request->session()->get('errors') ?: new ViewErrorBag
    );

    // Putting the errors in the view for every view allows the developer to just
    // assume that some errors are always available, which is convenient since
    // they don't have to continually run checks for the presence of errors.

    return $next($request);
}

確かに、$this->view->share()の部分で、セッションから'errors'のデータが取得されていることが分かります。

ざっとまとめると

少し長くなってしまいましたが、バリデーション時の挙動をざっとまとめてしまうと、

  • バリデーションルールごとの判定メソッドを実行し
  • 引っかかれば、エラーメッセージに格納し
  • エラーメッセージがあれば、バリデーションエラーを投げる

といった動きをしていることが分かりました。

26
25
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
26
25

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?