LaravelのSeederでcsvからデータ移行をする


## 背景

Laravelのseeder機能はとても便利です。
Factoryも用いればそこそこなテストデータの作成ができ、ローカル開発をする分には十分です。

しかし、時にはステージング・テスト環境のものを引っ張ってローカルで確認したい場合があると思います。
今回は、そのデータをCSVとして出力し、Seederでテスト投入する手段について紹介します。

### ディレクトリの用意

以下のディレクトリを用意してください。

- `database/seeders/csv`
- `database/seeders/csv/data`

csvディレクトリ直下にはCSV投入用のSeederをセットします。
dataディレクトリには実際のcsvファイルを配置します。

### 初期設定
Gitのために以下の2ファイルを作成してください。

- `database/seeders/csv/data/.gitkeep`
- `database/seeders/csv/data/.gitignore`

``` database/seeders/csv/data/.gitignore
*.csv
```

### 共通クラス（今回のメイン）

`csv/data`に投入したcsvファイルをローカルに投入するクラスです。
CSV出力する都合nullやdate周りの変換処理を設定しています。

```php: database/seeders/csv/SeederUtil.php
<?php

namespace Database\Seeders\csv;

use Illuminate\Support\Facades\Schema;

class SeederUtil
{
    /**
     * CSVファイルからデータを読み取り、指定されたモデルとテーブルに挿入します
     *
     * @param  string  $modelClass Fully Qualified Model Class Name (e.g. \App\Models\User::class)
     * @param  string  $tableName  テーブル名 (e.g. 'users')
     * @return void
     */
    public static function seedFromCsv(string $modelClass, string $tableName)
    {
        $csvFile = database_path('seeders/csv/data/' . $tableName . '.csv');
        $fileHandle = fopen($csvFile, 'r');
        $header = fgetcsv($fileHandle);

        // テーブルのカラムを取得
        $tableColumns = Schema::getColumnListing($tableName);

        while (($row = fgetcsv($fileHandle)) !== false) {
            $row = array_combine($header, $row);

            // 'NULL'文字列をnullに変換
            $row = array_map(function ($value) {
                return $value === 'NULL' ? null : $value;
            }, $row);

            // 存在しないカラムを除外
            $filteredRow = array_filter($row, function ($key) use ($tableColumns) {
                return in_array($key, $tableColumns);
            }, ARRAY_FILTER_USE_KEY);

            // created_at, updated_atカラムがあれば現在日時を追加
            $now = now();
            if (in_array('created_at', $tableColumns)) {
                $filteredRow['created_at'] = $now;
            }
            if (in_array('updated_at', $tableColumns)) {
                $filteredRow['updated_at'] = $now;
            }

            $modelClass::create($filteredRow);
        }

        fclose($fileHandle);
    }
}
```

### DatabaseSeeder.php

csv投入用に作成します。namespaceにご注意ください。

```php
<?php

namespace Database\Seeders\csv;

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     *
     * @return void
     */
    public function run()
    {
        $this->call(UserSeeder::class);
}
```

### UserSeederの実装

```php: csv/UserSeeder.php
<?php

namespace Database\Seeders\csv;

use Illuminate\Database\Seeder;
use App\Models\User;

class UserSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        SeederUtil::seedFromCsv(User::class, 'users');
    }
}
```

これであとはCSVを配置することでデータ投入ができます。
また、DatabaseSeeder.phpの配置が違うので以下のようなコマンドで実行してください。


```bash
php artisan migrate:fresh
php artisan db:seed --class="Database\Seeders\csv\DatabaseSeeder"
```

一応今はSeederクラスを個別で実装するようにしましたが、DatabaseSeederにまとめてしまってもいいかと思います。
また、もし上記の共通実装では足りないテーブル別の変換処理が必要になった場合は`seedFromCsv`関数をコピペして加工してください。