チェックボックスの値の取得を通して、FormRequestの仕様を認識した話

# `Undefined array key "tag_ids"`
このエラーが出たとき、
記事の投稿時に、チェックボックスでタグを選択できるような機能を実装していました。
以下はコントローラとビューの実装です。
```php:app\Http\Controllers\ArticleController.php
    public function store(ArticleStoreRequest $request) {

        $validated = $request->validated();
        /** @var User $user */
        $user = Auth::user();
        $article = $user->articles()->create($validated);
        
        $tag_ids = array_filter($validated['tag_ids']); //この行でエラー
        $article->tags()->attach($tag_ids);

        return to_route('articles.index');
    }
```
```html:resources/views/articles/create.blade.php
<form method="POST" action="{{ route('articles.store') }}">
  @csrf
  <fieldset class="fieldset bg-base-200 border-base-300 rounded-box w-l border p-4">
    <legend class="fieldset-legend">記事投稿</legend>

    <label for="title" class="label">{{ __('Title') }}</label>
    <input type="text" class="input w-lg" placeholder="タイトル" name="title" value="{{ old('title') }}">

    <label for="content" class="label">{{ __('Content') }}</label>
    <textarea name="content" id="content" class="textarea w-lg size-100">{{ old('content') }}</textarea>

    <label for="tag_div">タグ</label>
    <div id="tag_div">
      @foreach ($tags as $tag)
        <input
          type="checkbox"
          aria-label="{{ $tag->name }}"
          class="btn btn-xs mr-2 mb-2"
          value="{{ $tag->id }}"
          id="tag_{{ $tag->id }}"
          name="tag_ids[]"
          {{ is_array(old('tag_ids')) && in_array($tag->id, old("tag_ids")) ? 'checked' : '' }}
        />
      @endforeach
    </div>

    <button type="submit" class="btn btn-neutral">{{ __('Submit') }}</button>

  </fieldset>
</form>
```
# 問題の所在は`FormRequest`クラスにあった！
「`$tag_ids`というタグの配列は、ちゃんとフロントから渡しているはずなのに…」と思っていたのですが、AIに相談したところ、どうやら問題は`FormRequest`クラスでのバリデーションの書き方にあったようです。

`FormRequest`の基本を知りたい方はこちらをどうぞ

https://readouble.com/laravel/12.x/ja/validation.html#:~:text=%E3%81%AE%E5%A0%B4%E5%90%88%E3%81%AF%E3%80%81%E3%80%8C-,%E3%83%95%E3%82%A9%E3%83%BC%E3%83%A0%E3%83%AA%E3%82%AF%E3%82%A8%E3%82%B9%E3%83%88,-%E3%80%8D%E3%82%92%E4%BD%9C%E6%88%90%E3%81%99%E3%82%8B

https://qiita.com/ucan-lab/items/9a294163b0fd8ba25aca

今回は、以下で説明する、`FormRequest`の二つの仕様を理解していなかったために、問題が起こっていたようです。

## ①コントローラの`$request->validated()`には、`rules`に定義したプロパティしか渡ってこない
そのときの私は、以下のように、各要素へのバリデーションしかしていませんでした。

```php:app\Http\Requests\ArticleStoreRequest.php
    public function rules(): array
    {
        return [
            'tag_ids.*' => ['nullable', 'numeric', Rule::exists('tags', 'id')],
        ];
    }
```

`FormRequest`クラスでバリデーションを行う場合、コントローラでは`$request->validated()`という形で、バリデーション済みの値を配列で取得して使います。
その配列の中には、`rules()`内に定義したプロパティだけが含まれます。
つまり、私は`tag_ids`という配列自体を`rules`の中に記載していなかったので、コントローラで`$request->validated()`したときに、その配列の中に`tag_ids`が含まれていなかったのです。

### 対策
**配列にバリデーションを実施する際は、**

- **配列自体へのバリデーション**
- **配列の各要素へのバリデーション**

これらを合わせて書くようにしましょう。

よって、このように修正しました。
```php:app\Http\Requests\ArticleStoreRequest.php
    public function rules(): array
    {
        return [
            'tag_ids'   => ['nullable', 'array'],
            'tag_ids.*' => ['numeric', Rule::exists('tags', 'id')],
        ];
    }
```

以上が、今回の一番大きな問題でした。
しかしもうひとつポイントがあります。

## ②`null`なプロパティは`$request->validated()`に含まれない

今回のチェックボックスは、一つも選択されていなくてもOKとしています。
よって`rules`でも、`tag_ids`には`nullable`を設定しました。

このように配列自体を`nullable`にしている場合には、少し注意が必要です。

`FormRequest`では、バリデーション時に`null`だったプロパティについては、コントローラまで届かない仕様になっています。`$request->validated()`で取得できる配列に、含まれなくなるということです。

チェックボックスがどれも未選択の場合、そもそもブラウザから`tag_ids`という`name`の値は送信されてきません。つまり、`FormRequest`でのバリデーション時には、`tag_ids`は`null`ということになります。
すると、コントローラで`$request->validated()['tag_ids']`としても、何も取得することができません。
`Undefined array key "tag_ids"`というエラーが起こってしまいます。

### 対策
`prepareForValidation`という、バリデーションの前にデータを加工するメソッドを使いましょう。
データの正規化や加工に便利なメソッドです。
今回はこの中で、`null`の場合に空の配列を入れるようにしておきます。

```php:app\Http\Requests\ArticleStoreRequest.php
    public function rules(): array
    {
        return [
            'tag_ids'   => ['nullable', 'array'],
            'tag_ids.*' => ['numeric', Rule::exists('tags', 'id')],
        ];
    }

    protected function prepareForValidation()
    {
        $this->merge([
            'tag_ids' => $this->input('tag_ids', []),
        ]);
    }
```
`input`メソッドでは、第二引数にデフォルト値を設定できますので、ここに空の配列を入れます。
`$this->merge`は、引数に渡した内容を、リクエストデータに反映するためのメソッドです。
これで、`tag_ids`が`null`の場合には空の配列を`tag_ids`に代入してから、バリデーションへ進むことができます。
すると、空の配列がコントローラへ渡るため、`Undefined array key "tag_ids`というエラーは起こらなくなります。

今回は以上2点を押さえることで、FormRequestクラスを適切な形にできました。

# 参考サイト
https://weblook6-q.com/how-to-array-validate/