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();