【Laravel6.x】バリデーション周りで覚えておきたいこと（まとめ）

## はじめに
Laravelを触り始めて１年半ほどたったので、自分の健忘録を兼ねて、バリデーション周りで覚えておきたいことをまとめます。

[公式ドキュメント](https://laravel.com/docs/6.x/validation)や他の記事を参考にしつつ、独自に必要と思うものを追加しています。

Laravelのバージョンは6系です。

間違いなどコメント欄で指摘してもらえると助かります。

## 1. 基本的な書き方

Laravelのバリデーションは複数の書き方があるが、Validateメソッドを使うとシンプルに書ける。

また、細かな挙動をマニュアルに設定したい場合は、Validatorファサードを使うこともできる。

### 1-1. Validateメソッドを使ったController

ブログ記事投稿用のバリデーションを例にした簡単なサンプル。

```php:app/Http/Controllers/BlogController.php

<?php

namespace App\Http\Controllers;

use App\Models\Blog;
use Illuminate\Http\Request;

class BlogController extends Controller
{

// ... 省略 ...

/**
 * ブログ記事投稿
 *
 * @param  Request  $request
 * @return Response
 */
public function store(Request $request)
{
    // 簡単なバリデーション
    $validated = $request->validate([
        'title' => 'required|string|max:255',
        'body' => 'required|string|max:1000',
    ]);

    // バリデーション後の処理
    $blog = new Blog();
    $blog->fill($validated)->save();

```

コントローラーで受け取ったリクエストを`$request->validate([ルールの配列])`とするだけで、バリデーションが行える。

### 1-2. ルールの書き方

バリデーションロジックは、デリミタを使う方法か配列での指定が存在している。

```php
// デリミタを使用
'title' => 'required|string|max:255',
'body' => 'required|string|max:1000',

// 配列で指定
'title' => ['required', 'string', 'max:255'],
'body' => ['required', 'string', 'max:1000'],
```

インプットされたデータが配列の場合、キーを`xxx.yyy`の形で記述出来る。

```php
'category.child' => 'string',
'category.parent' => 'string',
'tags.*' => 'string', // * でワイルドカードが使える
```

### 1-2. エラーを画面に表示する

バリデーションエラーがある場合、リクエストを送った画面にリダイレクトされ、セッションを使ってエラーが返される。

以下は、エラーをブログ投稿画面に表示する例。

```html:resources/views/post/create.blade.php

<h1>ブログ記事投稿</h1>

<!-- エラーをまとめて表示する記述 -->
@if ($errors->any())
  <div class="alert alert-danger">
      <ul>
          @foreach ($errors->all() as $error)
              <li>{{ $error }}</li>
          @endforeach
      </ul>
  </div>
@endif

<!-- 送信フォーム部分 -->
<form action="/blogs" method="post">
  @csrf
  <p>タイトル：<input type="text" name="title"></p>
  <p>本文：<textarea name="body"></textarea></p>
  <input type="submit" value="送信">
</form>
```

個別に表示させることも下のような感じで簡単にできる。

```html:resources/views/post/create.blade.php
<!-- 個別にエラーを表示する例 -->
<input type="text" name="title" class="@error('title') is-invalid @enderror">
@error('title')
    <div class="alert alert-danger">{{ $message }}</div>
@enderror
```

### 1-3. エラーをJSONで返す（API向け）

Validationメソッドでは内部でリクエストを判定して、APIへのリクエストはエラーをJSONで返すことができる。

※ コントローラーのバリデーション部分は、同じコードを使うことができる。

リクエストヘッダーに`Accept:application/json`を指定すると、下のような形式でエラーが返される。

```javascript:JSONで返されたバリデーションエラーの例（ステータスは422）
{
    "message": "The given data was invalid.",
    "errors": {
        "title": [
            "The title must be a string."
        ]
    }
}
```

### 1-4. さまざまなバリデーションルール

Laravelにはたくさんのバリデーションルールが用意されていて、[公式ドキュメント](https://laravel.com/docs/6.x/validation#available-validation-rules)に一覧がある。

以下は、個人的によく使うもののリスト。

```php
// 必須か否かなど
'key' => 'required', // 必須。nullや空文字・空の配列などもエラーとなる
'key' => 'nullable', // 不要。nullや空文字・空の配列などが許される
'key' => 'sometimes', // フィールドがあれば、後述のルールが適応される。なければ適応されない。

// 数値系
'key' => 'boolean', // 0か1
'key' => 'integer', // 整数。'1'や'-2'など
'key' => 'numeric', // 数値。'0.1'や'-0.333'など
'key' => 'min:0', // 0以上。
'key' => 'max:10', // 10以下。

// 文字列系
'key' => 'string', // 文字列。
'key' => 'string|min:3', // 3文字以上。
'key' => 'string|max:10', // 10文字以下。
'key' => 'digits:3', // 数字のみの文字列。この場合3桁。'001'など最初が0でもいい
'key' => 'email', // メールアドレスとして正しいか。

// 日付系
'key' => 'date', // 日時として認識できる文字列。'2000-01-01'や'2020-05-15T01:04:54+09:00'など。PHPのstrtotimeで認識できるフォーマットが許容される。
'key' => 'date_format:Y-m', // 日時のフォーマットを指定。この場合'2000-01'などの年月に限定している
'key' => 'after_or_equal:2000-01-01', // 指定した日時以降の日時
'key' => 'before:2000-01-01', // 指定した日時よりも前の日時
```

### 1-5. ユーザー登録で使うバリデーション例（`unique`,`confirmed`）

`unique`,`confirmed`は、下のような感じで使える。

```ｈｔｍｌ:登録画面のインプットタグ例
<input type=text name=email>
<input type=text name=password>
<input type=text name=password_confirmation>
```

```php:ユーザー登録バリデーションの例
// 指定したテーブルで一意。この場合、Usersテーブルで同じ値がないことが要求される。
'email' => 'unique:users',

// 入力ミスを防ぐための確認用フィールド。この場合、passwordとpassword_confirmationの値が同じであること。
'password' => 'confirmed',

// xxxxx_confirmationのルールは記述しない。
'password_confirmation' => 'confirmed', // これはNG

// 実践的にはこんな感じ
'email' => 'required|unique:users|email|max:255',
'password' => 'required|confirmed|min:8|max:255|alpha_num',
```

### 1-6. Validatorファサード使ったバリデーション

`$request->validate()`の書き方はシンプルでよいが、挙動が内部で決まっているので、エラーの返し方やリダイレクト先などをマニュアルに変更したいなどのときは`Validator`ファサードを使うことができる。

```php:app/Http/Controllers/BlogController.php

<?php

namespace App\Http\Controllers;

use App\Models\Blog;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator; // 追加

class BlogController extends Controller
{

// ... 省略 ...

    /**
     * ブログ記事投稿
     *
     * @param  Request  $request
     * @return Response
     */
    public function store(Request $request)
    {
        $validator = Validator::make($request->all(), [
            'title' => 'required|string|max:255',
            'body' => 'required|string|max:1000',
        ]);

        if ($validator->fails()) {
            return redirect('blogs/create')
                        ->withErrors($validator)
                        ->withInput();
        }

        // バリデーション後の処理
        $blog = new Blog();
        $blog->fill($request->validated())->save();
      
```
こちらの記事により詳しい解説が載っています。
[【laravel】Validatorによるバリデーション \- Qiita](https://qiita.com/gone0021/items/c613ef7e006b6f5d47ce)

## 2. FormRequestを利用したバリデーション
FormRequestクラスを使うと、コントローラーでバリデーションを書かなくて済む（0行になる）。

下は、実際にFormRequestを使ったブログ記事投稿コントローラーの例。

```php:app/Http/Controllers/BlogController.php

/**
 * ブログ記事投稿
 *
 * @param  StoreBlogPost  $request
 * @return Response
 */
public function store(StoreBlogPost $request) // 独自に作成したStoreBlogPostを使っている
{
    // バリデーション後の処理（すでにリクエストはバリデート済み）
    $blog = new Blog();
    $blog->fill($request->validated())->save();

    // ... 省略 ...
```

### 2-1. FormRequestの作成

下のコマンドでFormRequestを継承したクラスが`app/Http/Requests`に作成される

```shell
php artisan make:request StoreBlogPost
```



### 2-2. FormRequestの編集

作成されたFormRequestの`rules`メソッドにバリデーションルールを記述する。

`authorize`メソッドには、認証ロジックを記述できる。
認証をここで行わない場合はtrueを返すか、メソッドごと削除すればよい。

```php:app/Http/Requests/StoreBlogPost.php
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class StoreBlogPost extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true; // 認証をここで行わないときはtrueを返せばOK
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        // ここでルールを記述すればOK
        return [
            'title' => 'required|string|max:255',
            'body' => 'required|string|max:1000',
        ];
    }
}
```

### 2-3. FormRequestを使ってエラーをJSONで返す（API向け） 
FormRequestの場合も、内部でリクエストを判定してAPIへのリクエストにはJSONでエラーを返すことができる。

リクエストのヘッダーに`Accept:application/json`を指定するだけなので、FormRequestのコードは同じものが使える。

### 2-４. JSONのエラーメッセージをカスタマイズする（API向け） 

エラーメッセージの内容やレスポンスフォーマットを変えたい場合は、下のように親クラスを作って継承することもできる。

#### STEP1. 親クラスを作る

```shell
php artisan make:request ApiRequest
```

#### STEP2. 親クラスを編集する

```php:app/Http/Requests/ApiRequest.php
<?php

namespace App\Http\Requests;

use Illuminate\Contracts\Validation\Validator;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\Exceptions\HttpResponseException;

abstract class ApiRequest extends FormRequest // フォームリクエストを継承した抽象クラス
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    abstract public function rules();

    /**
     * Handle a failed validation attempt.
     *
     * @param  \Illuminate\Contracts\Validation\Validator $validator
     * @return void
     *
     * @throws \Illuminate\Http\Exceptions\HttpResponseException
     */
    public function failedValidation(Validator $validator)
    {
        $res = response()->json([
            'message' => 'インプットされたデータが不正です。', // messageの中身を日本語にする例
            'errors' => $validator->errors(),
        ], 422);
        throw new HttpResponseException($res);
    }
}
```

#### STEP3. 子クラスを作る

```shell
php artisan make:request Api/StoreBlogPost
```

#### STEP4. 子クラスを編集する

```php:app/Http/Requests/Api/StoreBlogPost.php
<?php

namespace App\Http\Requests\Api;

use App\Http\Requests\ApiRequest; // 追加

class StoreBlogPost extends ApiRequest // 作成したApiRequestを継承
{
    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules() // rulesの記述でだけでOK
    {
        return [
            'title' => 'required|string|max:255',
            'body' => 'required|string|max:1000',
        ];
    }
}
```

バリデーションを新しく作るときは、毎回、この`ApiRequest`を継承したclassを作る。
これで`rules()`メソッドを書くだけで、カスタマイズしたフォーマットでエラーを返せるようになる。

```json:カスタマイズされたエラーメッセージ（JSON）
{
    "message": "インプットされたデータが不正です。",
    "errors": {
        "title": [
            "titleは、必ず指定してください。"
        ]
    }
}
```

## 3. その他の必要なテクニック

個人的に、開発で必要になることが多いテクニック。

### 3-1. 編集リクエストのUniqueルール

登録済みのデータを編集するときに、登録時と同じようにUniqueルールを使うと実はNG。

というのも、編集対象のデータがすでにデータベースに存在しているため、一意でない判定になるから。

下のように、編集対象のデータをUniqueルールの対象外にすると、編集でUniqueが正しく動作するようになる。

```php
// unique:[テーブル名],[カラム名],[除外するID]
'nickname' => 'unique:users,nickname,' . $this->user->id,
```

上は編集のrouteが`/users/{user}`で、[モデル結合ルート](https://readouble.com/laravel/master/ja/routing.html#route-model-binding)を使っている例なので、注意。

例えば、編集のrouteが`/users/{id}`で、モデル結合ルートを使っていない場合は下。

```php
'nickname' => 'unique:users,nickname,' . $this->id,
```



### 3-2. 編集リクエストのUniqueルール（複合ユニーク）

テーブルに複合ユニークが使われていると、より複雑になる。

salesテーブルで、yearカラムとmonthカラムの両方が同じ場合に、バリデーションエラーを出す例（複合ユニーク）。

```php
// unique:[テーブル名],[カラム名],[除外する値],[除外するカラム名],[where制約カラム名],[where制約値]
'year' => 'unique:sales,year,' . $this->sale->id . ',id,month,' . $this->month,
```

詳しくは下の記事が参考になります。
[【Laravel】複数カラムのユニーク制約のバリデーションを実装する方法 \- Qiita](https://qiita.com/katsunory/items/02693caf0e8c71d609ee)

### 3-3. バリデーション前後に入力されたデータを変換する

バリデートする前に入力されたデータを変換したい場合は、FormRequestの`prepareForValidation`をオーバーライドすればOK。

```php:電話番号のハイフンを取り除いて10〜11桁の数字であることをバリデートする例
/**
 * Prepare the data for validation.
 *
 * @return void
 */
protected function prepareForValidation()
{
    $this->merge([
        'phone_number' => str_replace("-", "", $this->phone_number),
    ]);
}

/**
 * Get the validation rules that apply to the request.
 *
 * @return array
 */
public function rules()
{
    return [
        ...
        'phone_number' => 'digits_between:10,11',
        ...
    ];
}

```

バリデーション後にデータ加工したい場合は`passedValidation`をオーバーライドしましょう。

### 3-4. より複雑なバリデーションを追加する

Laravelで用意された標準ルールでは表現できない複雑なバリデーションを行いたい場合は、FormRequestの`withValidator`を使えば、独自のロジックを実装できる。

```php:配列のそれぞれの値が、前の２つの和と同じかバリデートする例
    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'array' => 'array|min:3', // 配列の要素が3個以上存在する
        ];
    }

    /**
     * Configure the validator instance.
     *
     * @param  \Illuminate\Validation\Validator  $validator
     * @return void
     */
    public function withValidator($validator) // ここでバリデーションのロジックを定義
    {
        $validator->after(function ($validator) {
            $arr = $this->array;
            for ($i = 2; $i < count($arr); $i++) {
                $sum = $arr[$i - 1] + $arr[$i - 2];
                if ($arr[$i] !== $sum) {
                    // バリデーションエラーを出している
                    $validator->errors()->add('array', 'Something is wrong with this array!');
                    break;
                }
            }
        });
    }

```

## 4. アプリ全体で使えるルールの追加・カスタマイズ

用意されたバリデーションルール以外に、アプリケーション全体で使えるルールを追加することもできる。

### 4-1. ルールを追加する（カタカナに限定するルール）

氏名のフリガナをカタカナで入力してもらいたいときなど、カタカナルールを作ってみる。

方法はいろいろあるが、ここでは[公式ドキュメント](https://laravel.com/docs/6.x/validation#using-extensions)にある方法（`Validator::extend`）を使う。

#### STEP1. ルールをプロバイダに登録

アプリ全体で使えるように、`app/Providers/AppServiceProvider.php`を下のように編集。

```php:app/Providers/AppServiceProvider.php

use Illuminate\Support\Facades\Validator; // 追加

// ... 省略 ...

    public function boot()
    {
        // bootメソッドの中に登録したいルールとロジックを記述する
        Validator::extend('katakana', function ($attribute, $value, $parameters, $validator) {
            return preg_match('/^[ァ-ヾー]+$/u', $value); // 正規表現でカタカナのみマッチさせている
        });
    }

```

#### STEP2. エラーメッセージを登録

エラーメッセージを`resources/lang/en/validation.php`に登録します。

※ ここでは英語。日本語にする方法は後述。

```php:resources/lang/en/validation.php

return [

// ... 省略 ...

    'katakana' => 'The :attribute must be a katakana.', // return内のどこでもいい

```

#### STEP3. ルールを使う

いつものように使えます。

```php
'myouzi_katakana' => 'katakana|max:255'
```

```json:返ってくるエラー（APIの例）
{
    "message": "リクエストデータが不正です。",
    "errors": {
        "myouji_katakana": [
            "The myouji katakana must be a katakana."
        ]
    }
}
```

### 4-2. 半角英数ルール(alpha_xxx)をマルチバイト文字に対応させる

Laravelには半角英数系のルールとして、`alpha`,`alpha_num`,`alpha_dash`があるが、日本語などマルチバイト文字に対応していない。

たとえば、`alpha`ルールを指定したフィールドに日本語が入力されてリクエストされた場合、エラーが出ないので困ったことになってしまう。

これを機能させるには、下のように`Validator::resolver`で標準ルールを上書きすることで対応できる。

※ 上書きするのに`Validator::extend`は使えない。

#### STEP1. 上書き用のクラスを作成する。

どこに作ってもいいが、今回は`app/Validators/CustomValidator.php`として作成する。

作成するクラスは、`Illuminate\Validation\Validator`を継承する。

```php:app/Validators/CustomValidator.php

namespace App\Validators;

use Illuminate\Validation\Validator;

class CustomValidator extends Validator
{
    /**
     * alpah
     *
     * @param string $attribute
     * @param string $value
     * @return true
     */
    public function validateAlpha($attribute, $value)
    {
        return preg_match("/^[a-z]+$/i", $value);
    }

    /**
     * alpah_num
     *
     * @param string $attribute
     * @param string $value
     * @return true
     */
    public function validateAlphaNum($attribute, $value)
    {
        return preg_match("/^[a-z0-9]+$/i", $value);
    }

    /**
     * alpah_dash
     *
     * @param string $attribute
     * @param string $value
     * @return true
     */
    public function validateAlphaDash($attribute, $value)
    {
        return preg_match("/^[a-z0-9_-]+$/i", $value);
    }
}

```

#### STEP2. ルールをプロバイダに登録

```php:app/Providers/AppServiceProvider.php

use App\Validators\CustomValidator; // 追加

// ... 省略 ...

    public function boot()
    {
        // 追加
        Validator::resolver(function ($translator, $data, $rules, $messages, $attributes) {
            return new CustomValidator($translator, $data, $rules, $messages, $attributes);
        });
    }
```

これで完了。

すでに`alpha`,`alpha_num`,`alpha_dash`が、それぞれ日本語などのマルチバイト文字でエラーを出すようになっている。

※ 標準ルールには半角英数系以外にも地雷があるようなので注意！
参考： [使っちゃダメ！Laravelの地雷バリデーションと標準ルールを禁止（オーバーライド）する方法 \- Qiita](https://qiita.com/kd9951/items/6514c4462fc0e0288afc)

## 5. エラーメッセージのカスタマイズ

個別に変更したり、全体を変更したりできる。

### 5-1. デフォルトのメッセージを個別にカスタマイズする

エラーメッセージをコントローラーやFormRequestクラスで個別に変更したい場合は、下のような方法が使える。

```php:Validatorファサードを使っている場合
$messages = [
    'required' => 'The :attribute field is required.',
];

$validator = Validator::make($input, $rules, $messages);
```
参考：[Validatorファサード](https://laravel.com/docs/6.x/validation#custom-error-messages)


```php:FormRequestを使っている場合
// 追加
public function messages()
{
    return [
        'title.required' => 'A title is required',
        'body.required'  => 'A message is required',
    ];
}
```
参考：[FormRequest](https://laravel.com/docs/6.x/validation#customizing-the-error-messages)

### 5-2. バリデーションメッセージ全体を日本語化する

Githubに[Laravel-Lang](https://github.com/Laravel-Lang)という世界各国の言語に対応するためのパッケージがあり、日本語のファイルも用意されている（[ここ](https://github.com/Laravel-Lang/lang/tree/master/src/ja)）ので、これを利用する。

#### STEP1. 日本語用ファイルをダウンロード

[日本語のバリデーション用ファイル](https://raw.githubusercontent.com/Laravel-Lang/lang/master/src/ja/validation.php)をダウンロードして、`/resources/lang/ja`フォルダを作成して設置する。

上で独自に作成した`katakana`ルールに対応したい場合は、下を追加することで対応できる。

```php:/resources/lang/ja/validation.php
'katakana' => ':attributeは、カタカナでなければなりません。', // 追加
```

#### STEP2. 設定を日本語に変更する

`/config/app.php`を下のように編集。

```php:/config/app.php
  /*
   |--------------------------------------------------------------------------
   | Application Locale Configuration
   |--------------------------------------------------------------------------
   |
   | The application locale determines the default locale that will be used
   | by the translation service provider. You are free to set this value
   | to any of the locales which will be supported by the application.
   |
   */

   'locale' => 'ja', // ここを変更
```

これで完了。

エラーメッセージが日本語で表示される。

```json:エラーメッセージ（JSON）
{
    "message": "リクエストデータが不正です。",
    "errors": {
        "password": [
            "passwordには、アルファベッドのみ使用できます。"
        ]
    }
}
```

`"passwordには、アルファベッドのみ使用できます。"`のpasswordなどを日本語化したい場合には、`/resources/lang/ja/validation.php`内の`attributes`に`'日本語化したいキー名'=>'日本語訳'`を追加していく。

```php:/resources/lang/ja/validation.php

// ユーザー登録用の'attributes'設定例
'attributes' => [
    'email' => 'メールアドレス',
    'password' => 'パスワード',
    'password_confirmation' => 'パスワード（確認用）',
    'name' => 'お名前'
],
```
これで、下のようなエラーメッセージを表示することができる。

```json:エラーメッセージ（JSON）
{
    "message": "リクエストデータが不正です。",
    "errors": {
        "password": [
            "パスワードには、アルファベッドのみ使用できます。"
        ]
    }
}
```

## おわりに
- バリデーション周りでだいたい困らない
- 体系的にまとまっている

を目指したら長くなりました。