勉強日記

チラ裏

Eloquent ORM非依存のfactory作った

LaravelのfactoryがEloquent ORM依存で、自前のDTOクラスに使えなくて困ったので自前ファクトリを作った

コード

app/Util/Fabrik.php

<?php
declare(strict_types=1);
namespace App\Util;
use Closure;
use Faker\Generator as Faker;
use App\Util\FabrikBuilder;
/**
 * ドイツ語で「工場」の意
 * @package App\Util
 */
class Fabrik
{
    /** @var Closure[] */
    private $providers;
    /** @var Closure[] */
    private $ctors;
    /** @var Faker */
    private $faker;
    /**
     * @param Faker $faker
     */
    public function __construct(Faker $faker)
    {
        $this->faker = $faker;
    }
    /**
     * @param string $className クラス名
     * @param Closure $provider 配列つくるやつ
     * @return Fabrik
     */
    public function define(string $className, Closure $provider): Fabrik
    {
        $this->providers[$className] = $provider;
        return $this;
    }
    /**
     * @param string $className クラス名
     * @param Closure $ctor 配列からなんかつくるやつ
     * @return Fabrik
     */
    public function constructBy(string $className, Closure $ctor): Fabrik
    {
        $this->ctors[$className] = $ctor;
        return $this;
    }
    /**
     * テスト用メソッド
     * @param string $className クラス名
     * @param Closure $provider 配列つくるやつ
     * @return bool
     */
    public function hasDefinition(string $className, Closure $provider): bool
    {
        return ($this->providers[$className] ?? null) === $provider;
    }
    /**
     * テスト用メソッド
     * @param string $className クラス名
     * @param Closure $ctor 配列からなんかつくるやつ
     * @return bool
     */
    public function hasCtor(string $className, Closure $ctor): bool
    {
        return ($this->ctors[$className] ?? null) === $ctor;
    }
    /**
     * @param string $className 構築対象のクラス
     */
    public function of(string $className, int $count = 1): FabrikBuilder
    {
        return new FabrikBuilder(
            $this->faker,
            $this->providers[$className],
            $count,
            $this->ctors[$className] ?? null
        );
    }
}

app/Util/FabrikBuilder.php

<?php
declare(strict_types=1);
namespace App\Util;
use Closure;
use Faker\Generator as Faker;
use Illuminate\Support\Collection;
/**
 * @package App\Util
 */
class FabrikBuilder
{
    /** @var Closure */
    private $provider;
    /** @var Closure */
    private $ctor;
    /** @var Faker */
    private $faker;
    /** @var int */
    private $count;
    public function __construct(Faker $faker, Closure $provider, int $count = 1, ?Closure $ctor = null)
    {
        $this->faker = $faker;
        $this->provider = $provider;
        $this->count = $count;
        $this->ctor = $ctor ?? function (array $arr) {
            return $arr;
        };
    }
    /**
     * 構築する
     * - 構築数が1ならctorの戻り値の型
     * - 2以上ならCollection
     * @return mixed|Collection
     */
    public function make(array $override = [])
    {
        return $this->makeWith($this->ctor, $override);
    }
    /**
     * 構築する
     * - 構築数が1ならarray
     * - 2以上ならCollection
     * @return mixed|Collection
     */
    public function makeArray(array $override = [])
    {
        return $this->makeWith(
            function (array $arr) { return $arr; },
            $override
        );
    }
    /**
     * 構築する
     * - 構築数が1ならctorの戻り値の型
     * - 2以上ならCollection
     * @param Closure $ctor
     * @return mixed
     */
    public function makeWith(Closure $ctor, array $override = [])
    {
        $provider = $this->provider;
        if ($this->count === 1) {
            return $ctor(
                array_merge(
                    $provider($this->faker),
                    $override
                )
            );
        } else {
            return collect()->times($this->count)
                ->map(
                    function ($_) use ($ctor, $override, $provider) {
                        return $ctor(
                            array_merge(
                                $provider($this->faker),
                                $override
                            )
                        );
                    }
                );
        }
    }
    // ----------------------------------------
    /**
     * テスト用メソッド
     * @param Faker $faker
     * @return bool
     */
    public function hasFaker(Faker $faker): bool
    {
        return $this->faker === $faker;
    }
    /**
     * テスト用メソッド
     * @param Closure $provider 配列生成関数
     * @return bool
     */
    public function hasProvider(Closure $provider): bool
    {
        return $this->provider === $provider;
    }
    /**
     * テスト用メソッド
     * @param Closure $ctor 構築関数
     * @return bool
     */
    public function hasCtor(Closure $ctor): bool
    {
        return $this->ctor === $ctor;
    }
    /**
     * テスト用メソッド
     * @param int $count 構築数
     * @return bool
     */
    public function hasCount(int $count): bool
    {
        return $this->count === $count;
    }
}

app/Facades/Fabrik.php

<?php
declare(strict_types=1);
namespace App\Facades;
use Illuminate\Support\Facades\Facade;
/**
 * オレオレFactoryのオレオレファサード
 * @package App\Facades
 */
class Fabrik extends Facade
{
    /**
     * Get the registered name of the component.
     *
     * @return string
     */
    protected static function getFacadeAccessor()
    {
        return \App\Util\Fabrik::class;
    }
}

config/app.php

<?php
...

    'aliases' => [

    ...
        // オレオレfactoryファサード
        'Fabrik' => 'App\Facades\Fabrik',
    ],

app/Providers/FabrikServiceProvider

<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App\Util\Fabrik;
use App\Domain\User\User;
use App\Domain\User\UserProfile;
use Faker\Generator as Faker;
/**
 * オレオレfactory "Fabrik"に関するブートストラップロジックを書く
 */
class FabrikServiceProvider extends ServiceProvider
{
    /**
     * Register services.
     *
     * @return void
     */
    public function register()
    {
        $this->app->singleton(Fabrik::class);
    }
    /**
     * Bootstrap services.
     *
     * @return void
     */
    public function boot()
    {
        \Fabrik::define(User::class, function (Faker $faker) {
            $faker->unique();
            return [
                'id' => null,
                'email' => $faker->safeEmail,
                'password' => $faker->password(),
                'role' => 1,
                'activationToken' => $faker->password(),
                'rememberToken' => $faker->password()
            ];
        })->constructBy(User::class, function (array $params) {
            return new User(
                $params['id'],
                $params['email'],
                $params['password'],
                $params['role'],
                $params['activationToken'],
                $params['rememberToken']
            );
        });

    }
}

利用側

  • factory()のインタフェースに寄せたつもり
  • ブートストラップロジックの中で、所望のオブジェクトの構築方法を定義する
    • 生の配列の生成
    • 配列からオブジェクトの構築
<?php

    \Fabrik::define(User::class, function (Faker $faker) {
        $faker->unique();
        return [
            'id' => null,
            'email' => $faker->safeEmail,
            'password' => $faker->password(),
            'role' => 1,
            'activationToken' => $faker->password(),
            'rememberToken' => $faker->password()
        ];
    })->constructBy(User::class, function (array $params) {
        return new User(
            $params['id'],
            $params['email'],
            $params['password'],
            $params['role'],
            $params['activationToken'],
            $params['rememberToken']
        );
    });
  • こんな感じにオブジェクトを生成する
<?php

    // User
    $user = \Fabrik::of(User::class)->make();
    
    // id=1なUser
    $user = \Fabrik::of(User::class)->make(['id' => 1]);
    
    // UserのCollection
    $users = \Fabrik::of(User::class, 3)->make();
    
    // array
    $arr = \Fabrik::of(User::class)->makeArray();