withValidatorでいろいろできる

今回はLaravelのFormRequest内で使える`withValidator`メソッドについて紹介します。FormRequestはリクエストパラメータに対してバリデーションルールの定義などをできるクラスですが、今回紹介するwithValidatorを使用することで、より融通の効くカスタムバリデーションルールを定義できます。

## 基本的なバリデーションの定義

まず通常のFormRequestでバリデーションを定義するには`rules`メソッドを使い、必須項目か、一意とするか、最大何バイトかなどが設定できます。以下は単体レコードのinsertもしくはupdateを想定しています。

```php
public function rules()
{
    return [
        'title' => 'required|unique:posts|max:255',
        'body' => 'required',
    ];
}
```
業務でのことですが、とあるapiでリクエストされたの中に以下のような構成のレコードが合わせて送られてきました。

```
[
    {
        "id": 1,
        "title": "title1",
        "theme": "theme1",
        "children": [
            {"child_name": "Brenda", "child_color": "red"},
            {"child_name": "Eddie", "child_color": "white"},
        ]
    },
    {
        "id": 2,
        "title": "title2",
        "theme": "theme2",
        "children": [
            {"child_name": "Anthony", "child_color": "brown"},
            {"child_name": "James", "child_color": "grey"},
        ]
    },
    ...
]
```
複数のオブジェクト配列内に複数の子オブジェクト配列が入っていますね。この場合にバリデーションルールを定義するにはどうすればいいでしょうか。

答えは以下のようになります。
```php
public function rules()
{
    return [
        '*.id' => 'required|integer',
        '*.title' => 'required|string|max:255',
        '*.theme' => 'required|string|max:255',
        '*.children.*.child_name' => 'required|string|max:255',
        '*.children.*.child_color' => 'required|string|max:255',
    ];
}
```
このようにワイルドーカードを使って記述することによって複数のレコード・子レコードにも対応できるんですね。

ここからが本題です。ここまで紹介した記述でほとんどのバリデーションルールを定義できそうですが、`withValidator`メソッドを使うと何ができるのでしょうか？今回は業務で使用した2つについて紹介します。前提として、以下は`upsert`するFormRequestとして定義しました。

## 複合ユニーク制約の定義
複合ユニーク制約を定義しているテーブルがあった場合、それを判別するために使います。そもそも複合ユニーク制約とは、複数のカラムを合わせてユニークとなるデータを定義する制約です。

今回の場合だと`title`と`theme`が複合ユニークだった場合、以下のようになります。
```
"title": "title1" "theme": "theme1", // ok
"title": "title1" "theme": "theme2", // ok
"title": "title2" "theme": "theme1", // ok
"title": "title1" "theme": "theme2", // titleとthemeで同じものがあるのでng
```
同じtitleやthemeが存在しても複数でユニークなっていればokということですね。
そういったバリデーションルールを書きたい場合、以下のように書けます。
```php
public function withValidator($validator)
{
    foreach ($this->request as $key => $item) {
        $id = $item['id'] ?? null;
        $validator->addRules([
            $key . '.title' => [
                Rule::unique('songs')
                    ->where('theme', $item['theme'])
                    ->ignore($id, 'id')
            ],
            // ..
        ]);
    }
}
```
`$this->request`をループで回すので、`key`はインデックス、`item`は値になっています。この場合は複数のレコードが渡ってきているので、`item`に入っているのが1レコードということになりますね。
```php
$validator->addRules([
```
これがルール定義です。`addRules`メソッドとは、動的にバリデーションルールを追加するために使用されます。

このメソッドを使用すると、リクエストの処理中に新しいバリデーションルールを追加できます。リクエストの内容に基づいてバリデーションルールを適用する必要がある場合に有効です。
```php
Rule::unique('songs')
    ->where('theme', $item['theme'])
    ->ignore($id, 'id')
```
`songs`テーブルの`title`がユニークとしてチェックし、そのレコード内の`theme`が存在したものはignore(バリデーションエラー対象から除外)されます。最終行でエラーを無視(解決)して潰していく処理ですね。このループ処理を続けてユニークであれば全て無視されFormRequestでの処理は問題なく通るということです。

一意でなかった場合はここでエラーが発生します。`Rule::unique`メソッドはDBを見ているので、テーブル内に重複があればエラーを出します。

## 本来のエラー箇所以外でエラーを出す
今回の例はレコードの中に子レコードがあり、ある箇所でエラーが起きた場合に別の箇所でもエラーを起こすような実装時に使いました。
```php
public function withValidator($validator)
{
    $validator->after(function ($validator) {
        $data = $validator->getData();
        foreach ($data as $parentIndex => $parent) {
            if (isset($parent['children']) && is_array($parent['children'])) {
                foreach ($parent['children'] as $childIndex => $child) {
                    if ($validator->errors()->has("$parentIndex.children.$childIndex.child_name")) {
                        $validator->errors()->add("$parentIndex.children.$childIndex.child_color", 'child_colorでもエラー');
                    }
                    if ($validator->errors()->has("$parentIndex.children.$childIndex.child_color")) {
                        $validator->errors()->add("$parentIndex.children.$childIndex.child_name", 'child_nameでもエラー');
                    }
                }
            }
        }
    });
}
```
コード自体入れ子で複雑に見えますが、このように書くことによって`child_name`でエラー吐く時に`child_color`カラムでも無理やりエラーを吐かせることができます。
```php
$validator->after(function ($validator) {
```
このメソッドは、標準のバリデーションルールが全て適用された後に追加のカスタムバリデーションロジックを実行するために使用できます。

標準のバリデーションチェックの後に特定の条件を追加したい場合や、複数のフィールド間での依存関係をチェックする必要がある場合に特に有効です。

```php
if ($validator->errors()->has("$parentIndex.children.$childIndex.child_name")) {
    $validator->errors()->add("$parentIndex.children.$childIndex.child_color", 'child_colorでもエラー');
}
```
`$validator->after`ということで`'required|string|max:255',`などのバリデーションに引っ掛かっている前提ですが、もしそのエラーがあれば、`add`メソッドで第一引数にフィールド名、第二引数にエラー内容を文字列で書くことによって同じエラーを出すことができます。

業務の話ですが、enum型として定義している2つのセルで、1つのセルが入力されていない場合に保存をすると2つどちらかを入力しないといけない場合でも1つの箇所にしかエラーが適用されなかったため、両方のセルへエラーを出すために使いました。

結局仕様変更があり最終的にこのコードは消え去りましたが。。

## まとめ
`withValidator`メソッドではいろいろなバリデーションを定義できます！
他にもまだまだ書き方はありますが、それはまたの機会に紹介できたらと思います。

最後まで見ていただきありがとうございました！