【Laravel】SplFileObjectでCSVファイルのデータを読み込んでDBに反映する



## サンプル
### 入力画面
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/218398/6426acdb-5760-fa7e-dbfb-2617c47f00e4.png)


## コード

### ルーティング処理

```\routes\web.php
Route::get('/csv/','Csv\CsvController@index')->name('csv.index');
Route::post('/csv/finish/','Csv\CsvController@finish')->name('csv.finish');
```

今回は`view`に使うファイルが1つしかないため、`name()`を使って、ルーティング処理ができるようにしている。

### コントローラー
- `index`: 入力画面の処理
- `finish`: CSVファイルデータ登録処理


```\app\Http\Controllers\Csv\CsvController.php

<?php

namespace App\Http\Controllers\Csv;

use DB;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;

class CsvController extends Controller
{
    public function index()
    {
        return view('csv/index',[
            'message' => 'CSVファイルをアップロードしてください',
        ]);
    }

    public function finish(Request $request)
    {

        // ファイルバリデーション
        $file_validation_array = [
            'csv_file' => [
                        'required', // 必須
                        'max:1024', // ファイルサイズ上限は設定値以下か
                        'file', // file属性でアップロードされたファイルか
                        'mimes:csv,txt', // 拡張子がcsvか,
            ],
        ];

        $file_validator = Validator::make($request->all(), $file_validation_array);

        if ($file_validator->fails()) {
            return redirect('/csv/')
                        ->withErrors($file_validator);
        };

        //ファイル名取得
        $csv_name = $request->file('csv_file')->getClientOriginalName();

        // ファイル保存
        $csv_path = $request->file('csv_file')->storeAs('csv_data', $csv_name);

        // CSV情報の取得
        $csv_content = new \SplFileObject(storage_path('app/csv_data/'.$csv_name));
 
        $csv_content->setFlags(
          \SplFileObject::READ_CSV |      // CSVとして行を読み込み
          \SplFileObject::READ_AHEAD |    // 先読み／巻き戻しで読み込み
          \SplFileObject::SKIP_EMPTY |    // 空行を読み飛ばす
          \SplFileObject::DROP_NEW_LINE   // 行末の改行を読み飛ばす
        );        

        // 配列に変換
        $csv_data = [];

        foreach($csv_content as $value) {

            // 文字コード変換
            $value = mb_convert_encoding($value, "UTF-8");

            // 項目行を省く        
            if($value[0] == "名前"){
                continue;
            }

            $csv_data[] = [
                'name' => $value[0],
                'age' => $value[1],
                'email' => $value[2],
            ];
            
        }

        // CSVデータのバリデーション
        $data_validation_array = [
            '*.name' =>['required', 'string'],
            '*.age' =>['required', 'numeric'], 
            '*.email' =>['required', 'string'], 
        ];

        $csv_validator = Validator::make($csv_data, $data_validation_array);

        if ($csv_validator->fails()) {

            // CSVファイルの削除
            Storage::delete('csv_data/'.$csv_name);

            return redirect('/csv/')
                        ->withErrors($csv_validator);
        };

        // 登録処理
        DB::beginTransaction();
        try {

            foreach($csv_data as $value){
                $user_data = DB::table('csv_imports')->insert([
                    'name' => $value['name'],
                    'age' => $value['age'],
                    'email' => $value['email'],
                    'created_at' => date("Y/m/d H:i:s"),
                    'updated_at' => date("Y/m/d H:i:s"),
                ]);
            }

            DB::commit();

            $message = "登録処理が完了しました。";

        } catch (Throwable $e) {

            DB::rollBack();
            $message = "登録処理に失敗しました。";

        }

        // CSVファイルの削除
        Storage::delete('csv_data/'.$csv_name);
        
        return view('/csv/index',[
            'message' => $message,
        ]);
    }

}

```

#### SplFileObject
CSVデータを取得するにあたり、PHPに標準で備わっている **`SplFileObject`** クラスを使用している。
`setFlags`による4つの設定が1つでも欠けると、データを取得できないので注意。

```\app\Http\Controllers\Csv\CsvController.php
// CSV情報の取得
$csv_content = new \SplFileObject(storage_path('app/csv_data/'.$csv_name));

$csv_content->setFlags(
  \SplFileObject::READ_CSV |      // CSVとして行を読み込み
  \SplFileObject::READ_AHEAD |    // 先読み／巻き戻しで読み込み
  \SplFileObject::SKIP_EMPTY |    // 空行を読み飛ばす
  \SplFileObject::DROP_NEW_LINE   // 行末の改行を読み飛ばす
); 
```

公式: [PHP: SplFileObject - Manual](https://www.php.net/manual/ja/class.splfileobject.php)

#### CSVデータのバリデーション
**`SplFileObject`** クラスを使用して取得したデータは配列に入れ直している。
その配列は **`*.設定した項目名`** でバリデーションできる。
配列のバリデーションは他の処理でも使えると思うので、覚えておくのをオススメする。

```\app\Http\Controllers\Csv\CsvController.php
// CSVデータのバリデーション
$data_validation_array = [
    '*.name' =>['required', 'string'],
    '*.age' =>['required', 'numeric'], 
    '*.email' =>['required', 'string'], 
];

$csv_validator = Validator::make($csv_data, $data_validation_array);

if ($csv_validator->fails()) {

    // CSVファイルの削除
    Storage::delete('csv_data/'.$csv_name);

    return redirect('/csv/')
                ->withErrors($csv_validator);
};
```

### View

```\resources\views\search\index(.blade).php
<form method="POST" action="{{ route('csv.finish') }}" enctype="multipart/form-data">
    <input name="csv_file" type="file" accept=".csv,.txt">
        @if($errors->all())
            <p style="color: red;">{{ $errors }}</p>
        @endif
    <p>{{ $message }}</p>
    <button type="submit">アップロードする</button>
    @csrf
</form>
```

#### ルーティング処理
今回は`view`に使うファイルが1つしかない。
なら、どうやって画面遷移するのか。

その答えは、`\routes\web.php`で設定した **`name()`** 。
`name()`を設定することで、今回のように **`{{ route(name()で設定した名前) }}`** で、`name()`を設定したルーティング処理へ遷移できる。
`name()`があると便利な場面が多いので、ルーティング処理の際にはできるだけ書いておこう。

#### ファイルのアップロード
今回のようにファイルをアップロードするフォームを作成する際、`<form>`タグに **`enctype="multipart/form-data"`** 属性をつける必要がある。
これも大切な事なので忘れずに。


### CSVファイル

```sample.csv
名前,年齢,メールアドレス
t,10,test@exaple.com
e,11,test@exaple.com
s,12,test@exaple.com
t,13,test@exaple.com
```

## 参考記事
- [ルーティング 8.x Laravel](https://readouble.com/laravel/8.x/ja/routing.html)
- [ファイルストレージ 8.x Laravel](https://readouble.com/laravel/8.x/ja/filesystem.html)
- [PHP: SplFileObject - Manual](https://www.php.net/manual/ja/class.splfileobject.php)
- [PHP: mb_convert_encoding - Manual](https://www.php.net/manual/ja/function.mb-convert-encoding.php)
- [<input type="file"> - HTML: HyperText Markup Language | MDN](https://developer.mozilla.org/ja/docs/Web/HTML/Element/input/file) 
- [【Laravel】transactionメソッド - Qiita](https://qiita.com/yukachin0414/items/97194eb8e6f51cece9f3)
- [【Laravel】Laravelでよく使うroute()とview()とredirect()まとめ - Qiita](https://qiita.com/sola-msr/items/4f92686d474118d1cceb)
- [Laravel CSVのmimes , mimetypesのバリデーション設定例 │ wonwon eater](https://wonwon-eater.com/laravel-csv-validation/)
- [Laravel クエリビルダ記法まとめ](https://www.ritolab.com/entry/93#aj_16)
- [LaravelでCSVファイルのインポート・エクスポートをやってみた - arms inc. Engineers' Blog](https://tech.arms-soft.co.jp/entry/2019/10/23/090000)
- [php - How to validate array in Laravel? - Stack Overflow](https://stackoverflow.com/questions/42258185/how-to-validate-array-in-laravel)