LaravelのFakerをカスタマイズして、存在する郵便番号と住所のテストデータをランダムに生成する

この記事は[OPENLOGI Advent Calendar 2021](https://qiita.com/advent-calendar/2021/openlogi)の16日目の記事です

# はじめに
LaravelではFakerというテストデータをランダムに生成するライブラリーがあります
Fakerを使えば、郵便番号や都道府県、市区町村などの住所はランダムに生成できますが、あくまでもランダムなので、実在する住所ではありません
この記事では、Fakerをカスタマイズして、実在する郵便番号と住所のランダムな値を取得するようにしてみます
使用するデータは日本郵便番号株式会社で公開しているものを利用します

# 環境
```bash
$ php -v
PHP 7.4.21 (cli) (built: Jul  1 2021 23:31:08) ( NTS )
Copyright (c) The PHP Group
Zend Engine v3.4.0, Copyright (c) Zend Technologies
    with Zend OPcache v7.4.21, Copyright (c), by Zend Technologies
```

```bash
$ php artisan -V
Laravel Framework 8.75.0
```

```bash
$ composer -V
Composer version 2.1.3 2021-06-09 16:31:20
```

# Fakerの住所関係のメソッドを実行してみる
実装する前にまず、tinkerコマンドでFakerのメソッドを実行してみて、得られる値を確認してみます

```bash
$ php artisan tinker
Psy Shell v0.10.12 (PHP 7.4.21 — cli) by Justin Hileman
>>> use Faker;
>>> $faker = Faker\Factory::create('ja_JP');
=> Faker\Generator {#3556}
>>> $faker->addProvider(new Faker\Provider\ja_JP\Address($faker));
>>> 
>>> $faker->postcode();
=> "6828679"
>>> 
>>> $faker->prefecture();
=> "兵庫県"
>>> 
>>> $faker->city();
=> "井高市"
>>> 
>>> $faker->streetAddress();
=> "佐藤町山口2-1-7"
>>> 
>>> $faker->streetName();
=> "桐山町"
>>> 
>>> $faker->address();
=> "2161479  長野県近藤市南区青田町野村7-6-6"
>>> 
```

こんな感じでFakerを使えば、ランダムな住所を生成できます
prefecture以外は、ランダムなので、ググっても、実在しない住所です
以下より、実際に Fakerをカスタマイズしていきたいと思います

# 手順
以下のステップで行います

1. Laravelの環境構築
2. 日本郵便の住所データを登録するテーブルを作成し、データを入れておく
3. モデルを作成する
4. 日本郵便の住所を返すFakerクラスを作成する
5. 4で作成したクラスをServiceProviderに登録する
6. Fakerを使用してデータを投入する

## 1. Laravelの環境構築
Laravelのプロジェクトを作成します

```bash
composer create-project --prefer-dist laravel/laravel random-address
```

データベースの設定を行います。データベースには sqlite を使用します

```bash
touch database/database.sqlite
```

.envのデータベース接続設定をsqlite用に書き換えます

```.env
DB_CONNECTION=sqlite
# DB_HOST=127.0.0.1
# DB_PORT=3306
# DB_DATABASE=laravel
# DB_USERNAME=root
# DB_PASSWORD=
```

app.phpを以下のように書き換えます

```config/app.php

-    'timezone' => 'UTC',
+    'timezone' => 'Asia/Tokyo',

-    'locale' => 'en',
+    'locale' => 'ja',

-    'faker_locale' => 'en_US',
+    'faker_locale' => 'ja_JP',

```

## 2. 日本郵便の住所データを登録するテーブルを作成し、データを入れておく

郵便番号データを格納するテーブルを作成します

```bash
php artisan make:migration create_japan_postcodes_table
```

作成されたマイグレーションにテーブルの定義を追加します

```database/migrations/2021_12_11_131423_create_japan_postcodes_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateJapanPostcodesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('japan_postcodes', function (Blueprint $table) {
            $table->string('postcode');
            $table->string('prefecture');
            $table->string('city');
            $table->string('street');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('japan_postcodes');
    }
}
```

テーブルに初期データを追加するSeedを作成します

```bash
php artisan make:seeder JapanPostCodesTableSeeder
```

作成されたSeederファイルを編集します
登録する元データの郵便番号ファイルは `storage` 直下に格納しておきます

```database/seeders/JapanPostCodesTableSeeder.php
<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;

class JapanPostCodesTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        // storage直下に日本郵便のサイトからダウンロードしたCSVファイルを解凍して配置
        // ref: https://www.post.japanpost.jp/zipcode/dl/oogaki-zip.html
        $file = new \SplFileObject(storage_path().'/13TOKYO.CSV');
        $japanPostcodes = collect($file)->map(function ($line) {
            $encLine = mb_convert_encoding($line, 'UTF-8', 'SJIS');
            $csvLine = str_getcsv($encLine);
            if (isset($csvLine[2]) && isset($csvLine[6]) && isset($csvLine[7]) && isset($csvLine[8])) {
                // 郵便番号, 都道府県, 市区町村, 町域すべてあるデータしか登録しない
                return [
                    'postcode' => $csvLine[2],
                    'prefecture' => $csvLine[6],
                    'city' => $csvLine[7],
                    'street' => $csvLine[8],
                ];
            }
            return null;
        })->reject(function ($value) {
            return is_null($value);
        })->toArray();

        DB::table('japan_postcodes')->insert($japanPostcodes);
    }
}
```

Seederを実行するための設定を行います

```database/seeders/DatabaseSeeder.php

-        // \App\Models\User::factory(10)->create();
+        $this->call([
+            JapanPostCodesTableSeeder::class,
+        ]);

```

以下のコマンドを実行し、テーブル作成 & 初期データを登録します

```bash
php artisan migrate:fresh --seed
```

## 3. モデルを作成する

```bash
php artisan make:model JapanPostcode
```

作成されたモデルを編集します

```app/Models/JapanPostcode.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

/**
 * @property string postcode
 * @property string prefecture
 * @property string city
 * @property string street
 */
class JapanPostcode extends Model
{
    use HasFactory;
}
```

## 4. 日本郵便の住所を返すFakerクラスを作成する

```app/Faker/JpAddress.php
<?php

namespace App\Faker;

use Faker\Provider\Base;
use App\Models\JapanPostcode;

class JpAddress extends Base
{
    /**
     * addressを配列で返す
     * 
     * @return array
     */
    public static function jpAddressArray(): array
    {
        $japanPostcode = self::getRandomJapanPostcode();
        return [
            'postcode' => $japanPostcode->postcode,
            'prefecture' => $japanPostcode->prefecture,
            'city' => $japanPostcode->city,
            'streetAddress' => "{$japanPostcode->street}1-2-3",
        ];
    }

    /**
     * ランダムにJapanPostcodeを1件返す
     * 
     * @return JapanPostcode
     */
    private static function getRandomJapanPostcode(): JapanPostcode
    {
        return JapanPostcode::query()
            ->orderByRaw('RANDOM()')
            ->first();
    }
}
```

## 5. 4で作成したクラスをServiceProviderに登録する

```bash
php artisan make:provider FakerServiceProvider
```

作成したFakerServiceProviderを編集します

```app/Providers/FakerServiceProvider.php
<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Faker\Factory;
use Faker\Generator;
use App\Faker\JpAddress;

class FakerServiceProvider extends ServiceProvider
{
    /**
     * Register services.
     *
     * @return void
     */
    public function register(): void
    {
        $this->app->singleton(Generator::class, function ($app) {
            $faker = Factory::create($app['config']->get('app.faker_locale', 'en_US'));
            $faker->addProvider(new JpAddress($faker));
            return $faker;
        });
    }
}
```

FakerServiceProviderを登録します


```config/app.php

    'providers' => [
        ...


        App\Providers\FakerServiceProvider::class,
    ],

```

## 6. Fakerを使用してデータを投入する

Fakerで生成するランダムな住所を登録するテーブルを作成します

```bash
php artisan make:migration create_address_table
```

```database/migrations/2021_12_14_181551_create_address_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateAddressTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('addresses', function (Blueprint $table) {
            $table->id();
            $table->string('postcode');
            $table->string('prefecture');
            $table->string('city');
            $table->string('street');
            $table->string('name');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('addresses');
    }
}
```

```bash
php artisan migrate
```

モデルを作成します

```bash
php artisan make:model Address
```

```app/Models/Address.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

/**
 * @property string postcode
 * @property string prefecture
 * @property string city
 * @property string street
 * @property string name
 */
class Address extends Model
{
    use HasFactory;
}
```

ファクトリーを作成します

```bash
php artisan make:factory AddressFactory
```

```database/factories/AddressFactory.php
<?php

namespace Database\Factories;

use App\Models\Address;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Factories\Factory;

class AddressFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array
     */
    public function definition()
    {
        $address = $this->faker->jpAddressArray();

        return [
            'postcode'  => $address['postcode'],
            'prefecture' => $address['prefecture'],
            'city' => $address['city'],
            'street' => $address['streetAddress'],
            'name' => $this->faker->name(),
        ];
    }
}
```

シーダーを作成してデータを投入します

```bash
php artisan make:seeder AddressesTableSeeder
```

```database/seeders/AddressesTableSeeder.php
<?php

namespace Database\Seeders;

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

class AddressesTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        Address::factory()->count(10)->create();
    }
}
```

```database/seeders/DatabaseSeeder.php

        $this->call([
            JapanPostCodesTableSeeder::class,
+           AddressesTableSeeder::class,
        ]);

```

```bash
$ php artisan db:seed --class=AddressesTableSeeder
```

# 最後に
シーダーの実行結果を確認すると、以下のようにランダムに住所情報を登録できていることが確認できます
![スクリーンショット 2021-12-14 19.29.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103774/b924a3fb-6c8c-c93a-2a32-925be0d68ebc.png)

# 参考
- [読み仮名データの促音・拗音を小書きで表記しないもの(zip形式)](https://www.post.japanpost.jp/zipcode/dl/oogaki-zip.html)
- [fzaninotto/faker](https://github.com/fzaninotto/Faker)
- [SplFileObjectでのCSVファイル読み込みでの文字化け対策](https://yk0807.com/larablog/posts/22)