LaravelでCSVファイルの中身に対してバリデーションチェック

どもー。

最近睡眠時間が満ち溢れているかずおです。

エンジニア

かずお

Laravelで少し変わったバリデーションをしたので、記事に残してみました。

エンジニア

かずお

 

さて、今回はタイトルにもある通り、LaravelのフォームでCSVファイルをアップロードする際に、

ファイルだけではなく中身にもバリデーションチェックを行いたかったので、僕が行った対処法を記事にまとめてみました~。

やりたいことの概要

  • アップロードしたCSVファイルの中身についてバリデーションチェックを行いたい。
  • エラーの場合は何行目のどこかがわかるようにエラーメッセージを出したい。

いざ実装!

今回はまずCSVファイルを読み込んで、

データを配列に格納してからその配列に対してバリデーションチェックをかけます。

エラーとなった場合は何行目か明示してエラーメッセージを画面に出したい。

 

では早速参りましょう。

csvファイル

id,name,subject,point
1,ロージー,国語,100
2,カール,数学,98

Controllerの実装

まず、CSVファイルを読み込んでデータ$csvDataに格納しています。

次に、$csvDataに対してvalidateCsvDate($csvData)でバリデーションチェックを行います。

最後に、エラーがあった場合はリダイレクトしてメッセージを表示し、エラーがなかった場合は次の処理に進みます。

public function csvUpload(Request $request) : RedirectResponse
{
    $file = $request->file('csv_file');
     $filePath = $file->storeAs('csv_file', now()->format('YmdHisv') . '.csv');
       
     // CSVの中身データ取得して配列に格納
     $csvData = $this->service->readCsvData($filePath);

     // CSVの中身データでバリデーションチェックを行う ← ★★★今回のポイント
     $validateErrors = $this->service->validateCsvDate($csvData);
     
     if (!is_null($validateErrors)) {
         // バリデーションの エラーメッセージをセット ← ★★★今回のポイント
         session()->flash('errors', $validateErrors);
         return redirect()->route('front.form.index');
     }
 
     // データを保存処理
     $result = $this->service->storeCsvData($csvData);

     return redirect()->route('front.form.index');
}

Serviceの実装

Illuminate\Support\Facades\Validatorを用いて、Requestで書いてることをそのままServiceに持ち込みました。使い方はほぼ変わらないですね。

今回はCSVデータの何行目のエラーかも明示したいのでIlluminate\Support\MessageBagIlluminate\Support\ViewErrorBagでエラーメッセージをカスタマイズしてみました。

use Illuminate\Support\Facades\Validator; 
use Illuminate\Support\MessageBag;
use Illuminate\Support\ViewErrorBag;

/**
 * 配列に格納されたCSVデータの行ごとに対してバリデーションチェックを行う
 * エラーがある場合はエラーメッセージ、ない場合はnullを返す
 */
public function validateCsvDate(array $csvData) : ViewErrorBag|null
{
  // バリエーションルール
    $rules = [
        'id' => [
            'required',
            'integer', // 整数のみ
        ],
        'name' => [
            'required',
        ],
        'subject' => [
            'required',
        ],
        'point' => [
            'nullable',
            'numeric',  //数値のみ
        ],
    ];

  // バリエーション対象項目名
    $attributes = [
        'id'      => 'ID',
        'name'    => 'お名前',
        'subject' => '教科',
        'point'   => '点数',
    ];

    // 各行に対してバリデーションチェックを行い、エラーの場合はメッセージを格納する 
    $upload_error_list = [];

    // すべての行に対してバリデーションチェックを行う
    foreach ($csvData as $key => $value) {
        $validator = Validator::make($value, $rules, __('validation'), $attributes);

        // バリデーションエラーがあった場合
        if($validator->fails()) {
            // エラーメッセージを「xx行目:エラーメッセージ」の形に整える
            $errorMessage = array_map(fn($message) => "{$key + 1}行目:{$message}", $validator->errors()->all());
            $upload_error_list = array_merge($upload_error_list, $errorMessage);
        }
    }

   // すべての行でバリデーションエラーがなかった場合
    if(empty($upload_error_list)) {
        return null;
    }
 
    // Requestのバリデーションエラーと同じ使い方をするため、エラーメッセージをViewErrorBagに入れる
    $errors = new ViewErrorBag();
    $messages = new MessageBag(['upload_errors' => $upload_error_list]);
    $errors->put('default', $messages);

    return $errors;
}

Viewの実装

では画面側でエラーメッセージを出していきます。

$errors->get('upload_errors')でServiceでMessageBagで入れたバリデーションメッセージたちを取りだして、foreachで全部表示させます。

{!! Form::open(['url' => route('front.csv_upload'), 'class' => 'form-horizontal','files' => true]) !!}
  ~省略~
 
    <div class="col-sm-8">
        {{ Form::submit('アップロード', ['class' => 'btn btn-default']) }}
    </div>

  {{-- バリデーションのエラーメッセージをすべて表示 --}}
    @foreach ($errors->get('upload_errors') as $message)
        <span class="error">{{ $message }}</span>
     {{-- 縦に表示するため改行を入れる --}}
        @if (!$loop->last)
            <br />
        @endif
    @endforeach
 
  ~省略~
{!! Form::close() !!}

実際にCSVファイルアップロードして挙動を見る

これで実装は完了したので、実際のバリデーションエラーをなるデータを用意して挙動を見てみましょう。

csvファイル

id,name,subject,point
1,ロージー,国語,100
2,カール,数学,98
三,エラーケース,英語,70    // idが整数値ではないのでアウト
四,エラーケース,英語,八十 // idが整数値ではないのでアウト、pointが数値ではないのでアウト

エラーメッセージを画面に表示

その方法

エラーメッセージを気にせずにバリデーションチェックのみかけたい場合は、以下の方法の方が少し簡潔かもしれませんね。

Controller

// CSVの中身データでバリデーションチェックを行う
$validateErrors = $this->service->validateCsvDate($csvData);     
if (!is_null($validateErrors)) {
    // バリデーションの エラーメッセージをセット
    return redirect('front.form.index')->withErrors($validate)->withInput();
}

Service

public function validateCsvDate(array $csvData) : Validator|null
{
  // バリエーションルール
    $rules = [
        '*id' => [
            'required',
            'integer',
        ],
        '*name' => [
            'required',
        ],
        '*subject' => [
            'required',
        ],
        '*point' => [
            'nullable',
            'numeric',
        ],
    ];

  // バリエーション対象項目名
    $attributes = [
        '*id'      => 'ID',
        '*name'    => 'お名前',
        '*subject' => '教科',
        '*point'   => '点数',
    ];
 
  // 配列丸ごとバリデーションチェックかける
    $validator = Validator::make($csvData, $rules, __('validation'), $attributes);
 
    return $validator->fails() ? $validator : null;
}

補足

今回は説明の便宜上csvファイルの読み込みとバリデーションチェックを別で分けましたが、CSVファイルを読み込む段階でバリデーションチェックかけても良いですね。

さらに、上記の処理をRequestの中に入れて、ファイルのチェックを行った後に更に中身のチェックを行うこともまた一つの手法です。

実はやり方はいろいろあるんです。面白いですね。

最後に

いかがだったしょうか。

CSVファイルアップロード時に、ファイルの拡張子等のチェックとは別に中身のチェックを行いたい時もあるはず。今回は僕がそんな場面に出くわした時の実装例を紹介させていただきました。

 

ロジカルスタジオではエンジニアを募集しています。

今回使ったLaravelやPHPUnitなど、バックエンド技術を使って開発がしてみたいという方は、是非採用サイトからご応募ください!