Laravelでcsvファイルのvalidationをする

#概要
・開発でcsvインポート機能を作った際にバリデーションでちょっと手間取った
・力技かもしれないがとりあえず頑張った証拠として残しておこうと思った。
・ライブラリ等は使ってません。
これが正解かもわからないですがもっといい方法あるよとかあれば教えていただけると嬉しいです。

##仕様
PHP 7.4
Laravel 8
mysql 8

##DBテーブル詳細
仮にこんなテーブルがあるとします。
prohectsテーブル
![スクリーンショット 2021-06-06 14.00.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1610149/89272c64-24b1-4db7-fc3d-97e96c9c99b3.png)

またこのprojectsテーブルにはpositionsテーブルとskillsテーブルが多対多で紐づいています。
positionsテーブル
![スクリーンショット 2021-06-06 14.03.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1610149/45298ea4-f1b6-ac75-ce7c-a9636274b760.png)

skillsテーブル
![スクリーンショット 2021-06-06 14.03.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1610149/63e265fe-2a4e-b567-0c4a-c5d6c62fc430.png)

これらのテーブルにcsvから一気に登録します。
テーブルの中身よりもcsvのデータに対しても普通にバリデーションできているところがミソです。

##csvファイル
csvファイルはこんな感じです。色ついてるところは気にしなくて大丈夫です。
リレーションしているところはめんどくさいのでIDで記入しました。skillとpositionは複数登録できるのでexplodeして配列で入れます。
![スクリーンショット 2021-06-06 14.12.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1610149/064e4b60-c530-89be-870c-d7a6a072dd06.png)

##実装
今回はRequestクラスを使ってRequestクラス内で完結するやり方です。

```php:TestRequest.php
<?php

namespace App\Http\Requests\Test;

use App\Models\Agent;
use App\Models\Position;
use App\Models\Skill;
use App\Models\Station;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use SplFileObject;

class TestRequest 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
     */
    public function rules(): array
    {
        $skills = Skill::all();
        $skill_ids = $skills->pluck('id')->toArray();
        $positions = Position::all();
        $position_id = $positions->pluck('id')->toArray();

        $agents = Agent::all();
        $agent_ids = $agents->pluck('id')->toArray();
        $stations = Station::all();
        $station_ids = $stations->pluck('id')->toArray();

        return [
            'csv_file' => ['required', 'file', 'mimetypes:text/plain', 'mimes:csv,txt'],
            'csv_array' => ['required', 'array'],
            'csv_array.*.name' => ['required', 'unique:projects', 'string'],
            'csv_array.*.station_id' => ['required', 'integer', Rule::in($station_ids)],
            'csv_array.*.description' => ['required', 'string', 'max:500'],
            'csv_array.*.required_condition' => ['nullable', 'string', 'max:500'],
            'csv_array.*.better_condition' => ['nullable', 'string', 'max:500'],
            'csv_array.*.work_start' => ['nullable', 'string', 'max:5'],
            'csv_array.*.work_end' => ['nullable', 'string', 'max:5'],
            'csv_array.*.weekly_attendance' => ['nullable', 'integer', 'max:5'],
            'csv_array.*.skill_ids' => ['required', 'array', 'max:10'],
            'csv_array.*.skill_ids.*' => ['required', 'integer', Rule::in($skill_ids)],
            'csv_array.*.position_ids' => ['required', 'array', 'max:10'],
            'csv_array.*.position_ids.*' => ['nullable', 'integer', Rule::in($position_id)],
        ];
    }

    protected function prepareForValidation()
    {
        $file_path = $this->file('csv_file')->path();
        // CSV取得
        $file = new SplFileObject($file_path);
        $file->setFlags(
            SplFileObject::READ_CSV |         // CSVとして行を読み込み
            SplFileObject::READ_AHEAD |       // 先読み／巻き戻しで読み込み
            SplFileObject::SKIP_EMPTY |       // 空行を読み飛ばす
            SplFileObject::DROP_NEW_LINE      // 行末の改行を読み飛ばす
        );
        foreach ($file as $index => $line) {
            // ヘッダーを取得
            if (empty($header)) {
                $header = $line;
                continue;
            }
            $csv_array[$index]['station_id'] = (int)$line[0];
            $csv_array[$index]['name'] = $line[1];
            $csv_array[$index]['description'] = $line[2] == "" ? null : $line[2];
            $csv_array[$index]['required_condition'] = $line[3] == "" ? null : $line[3];
            $csv_array[$index]['better_condition'] = $line[4] == "" ? null : $line[4];
            $csv_array[$index]['work_start'] = $line[5] == "" ? null : $line[5];
            $csv_array[$index]['work_end'] = $line[6] == "" ? null : $line[6];
            $csv_array[$index]['weekly_attendance'] = (int)$line[7] === 0 ? null : (int)$line[7];
            $csv_array[$index]['skill_ids'] = explode(',', $line[8]);
            $csv_array[$index]['position_ids'] = explode(',', $line[9]);
        }
        $this->merge([
            'csv_array' => $csv_array,     //requestに項目追加
        ]);
    }
}
```


ざっくり解説すると
Requestで入ってきたcsvデータを配列に入れて整理しcsv_arrayという名前で項目を追加してバリデーションしていく感じです。

prepareForValidation関数はLaravelで提供されている関数でバリデーション前にRequestの中身を加工することができます。

csvのデータは配列に加工しているので.*で取り出すことができます。
項目を追加しているので元々のcsvファイル自体のバリデーションも同時に行えます。

intにしたり中身が空だった時にnullにしたりしているところは各仕様に合わせて変更してください。
stringで避ければそのままいれるだけで大丈夫です。
以上です。

##参考
 [【Laravel】バリデーション前にデータ加工する方法](https://blog.capilano-fw.com/?p=579)