FakerPHPで日付を返す関数を定義する

laravelでseederを作成する際に、factoryでModelごとの定義をしていた時に、一意な日付を設定する必要がありました。
例えば、営業日を設定する際に重複が発生しないようにするなどがあるかと思います。

laravelのバージョンは10です。

unique関数について

Seeder側でstateを利用して外部から設定させることも可能ですが、Seederが複雑になりそうです。
Fakerではunique関数を利用することで、関数ごとに返した値がキャッシュされ、一意な値が返ってくるようになります。
参考)https://fakerphp.github.io/#modifiers

うまくいかない一意の日付

このunique関数を利用して、FakerPHPの日付を返す関数を利用できれば、一意な日付を返すことができそうです。
ということで、下記のようなFactoryを作成しました。

XxxFactory.php
public function definition(): array
{
    return [
        // ...
        'bussiness_date' => fake()->unique()->dateTimeThisYear(),
    ];
}

が、重複した日付が登録されてしまいます。

実装の確認

関数名からして原因はあきらかですが、dateTimeThisYearの実装を確認します。

DateTime.php
public function dateTimeThisYear($until = 'last day of december', string $timezone = null): \DateTime
{
    return $this->dateTimeBetween('first day of january', $until, $timezone);
}

https://github.com/FakerPHP/Faker/blob/v1.23.1/src/Faker/Core/DateTime.php#L133-L136

続いて、dateTimeBetweenの実装確認します。

DateTime.php
public function dateTimeBetween($from = '-30 years', $until = 'now', string $timezone = null): \DateTime
{
    $start = $this->getTimestamp($from);
    $end = $this->getTimestamp($until);

    if ($start > $end) {
        throw new \InvalidArgumentException('"$from" must be anterior to "$until".');
    }

    $timestamp = $this->generator->numberBetween($start, $end);

    return $this->setTimezone(
        $this->getTimestampDateTime($timestamp),
        $timezone,
    );
}

https://github.com/FakerPHP/Faker/blob/v1.23.1/src/Faker/Core/DateTime.php#L93-L108

ということで、秒単位で日時を生成しているため、日付で見たときに一意にならないことがわかりました。

日付を返す関数を追加する

FakerPHPには日付を返す関数がないため、時間が00:00:00である\DateTimeを返す関数を追加出ればよいことがわかりました。

FakerProviderファイルの追加

app/Faker/Provider/Date.phpを追加します。

Date.php
<?php

namespace App\Faker\Provider;

use Faker\Provider\Base;

class Date extends Base
{
    public function dateThisYear($until = 'last day of december', string $timezone = null): \DateTime
    {
        return $this->generator->dateTimeBetween('first day of january', $until, $timezone)->setTime(0, 0, 0);
    }
}

先述のdateTimeThisYearとほとんど同じですが、最後に->setTime(0, 0, 0)をして、時間を00:00:00にしています。

ServiceProviderの追加

app/Providers/FakerServiceProvider.phpを追加します。

FakerServiceProvider.php
<?php

namespace App\Providers;

use App\Faker\Provider\Date;
use Illuminate\Support\ServiceProvider;

class FakerServiceProvider extends ServiceProvider
{
    /**
     * Register services.
     */
    public function register(): void
    {
        $faker = fake();
        $faker->addProvider(new Date($faker));
    }

    /**
     * Bootstrap services.
     */
    public function boot(): void
    {
        //
    }
}

providerの登録

config/app.phpproviders配列の最後に追記し、FakerServiceProviderを登録します。

app.php
'providers' => [
    /*
     * Application Service Providers...
     */
    // ...
    App\Providers\FakerServiceProvider::class,
],

動作確認

下記のようにDate.phpで定義したdateThisYear関数を読みだすことで、一意な日付が設定されることが確認できました。

XxxFactory.php
public function definition(): array
{
    return [
        // ...
        'bussiness_date' => fake()->unique()->dateThisYear(),
    ];
}