Factory では属性の定義にクロージャが使える
Laravel の Eloquent には Factory という機能があって、公式ドキュメントに書いてある 使い方の説明 を見てみると以下のように書いてあります。
Relations & Attribute Closures
You may also attach relationships to models using Closure attributes in your factory definitions. For example, if you would like to create a new
User
instance when creating aPost
, you may do the following:$factory->define(App\Post::class, function ($faker) { return [ 'title' => $faker->title, 'content' => $faker->paragraph, 'user_id' => function () { return factory(App\User::class)->create()->id; } ]; });
These Closures also receive the evaluated attribute array of the factory that defines them:
$factory->define(App\Post::class, function ($faker) { return [ 'title' => $faker->title, 'content' => $faker->paragraph, 'user_id' => function () { return factory(App\User::class)->create()->id; }, 'user_type' => function (array $post) { return App\User::find($post['user_id'])->type; } ]; });
ざっくり説明すると「属性の定義にはクロージャを使うこともできて、それを使ってモデルとのリレーションを定義することもできるよ」っていうようなことが書いてあるのですが、なんでクロージャを使う必要があるのかが書かれてません。
Laravel あるあるで、使えないより使える方が便利っていうだけで実装されてる可能性もありますが、わざわざドキュメントに書くくらいなので、何かメリットがありそうです。
ちなみに、属性の定義に使うクロージャーのことを、ドキュメントでは Attribute Closures
と呼んだり Closure attributes
と呼んだりしています。
英語人の慣習はよくわからないのですが、ここでは仮に「属性クロージャ」と呼ぶことにします。
属性クロージャを使わない場合
ちなみに属性クロージャを使わない場合は以下のようなコードになると思います。
$factory->define(App\Post::class, function ($faker) {
return [
'title' => $faker->title,
'content' => $faker->paragraph,
'user_id' => factory(App\User::class)->create()->id,
];
});
$factory->define(App\Post::class, function ($faker) {
$user = factory(App\User::class)->create();
return [
'title' => $faker->title,
'content' => $faker->paragraph,
'user_id' => $user->id,
'user_type' => $user->type,
];
});
うん。わかりやすい。
これと属性クロージャを使った場合で何が違うのか。ソースコードを追って調べてみました。
Factory が Model インスタンスを作るまで
例にも出てきている変数 $factory
の中身は Illuminate\Database\Eloquent\Factory
クラスのインスタンスです。
この Factory
クラスは、Factory の定義をする役割を担っています。
一方、Factory を使う際に使用するヘルパー関数 factory()
では、Factory
クラスの of()
メソッドを呼び出し、Illuminate\Database\Eloquent\FactoryBuilder
クラスのインスタンスを生成しています。
/**
* Create a model factory builder for a given class, name, and amount.
*
* @param dynamic class|class,name|class,amount|class,name,amount
* @return \Illuminate\Database\Eloquent\FactoryBuilder
*/
function factory()
{
$factory = app(EloquentFactory::class);
$arguments = func_get_args();
if (isset($arguments[1]) && is_string($arguments[1])) {
return $factory->of($arguments[0], $arguments[1])->times($arguments[2] ?? null);
} elseif (isset($arguments[1])) {
return $factory->of($arguments[0])->times($arguments[1]);
}
return $factory->of($arguments[0]);
}
ということは、この FactoryBuilder
の中にいつも使っている make()
や create()
などのメソッドがあるはずです。
/**
* Create a collection of models.
*
* @param array $attributes
* @return mixed
*/
public function make(array $attributes = [])
{
if ($this->amount === null) {
return tap($this->makeInstance($attributes), function ($instance) {
$this->callAfterMaking(collect([$instance]));
});
}
if ($this->amount < 1) {
return (new $this->class)->newCollection();
}
$instances = (new $this->class)->newCollection(array_map(function () use ($attributes) {
return $this->makeInstance($attributes);
}, range(1, $this->amount)));
$this->callAfterMaking($instances);
return $instances;
}
make()
メソッドを見つけました。
インスタンスを何個作るかが入っている $this->amount
は、何も指定しないと null
になるのですが、そののときの動作を見てみると makeInstance()
というメソッドを呼び出して、make()
メソッドに渡された配列 $attributes
をそのまま渡しています。
/**
* Make an instance of the model with the given attributes.
*
* @param array $attributes
* @return \Illuminate\Database\Eloquent\Model
*/
protected function makeInstance(array $attributes = [])
{
return Model::unguarded(function () use ($attributes) {
$instance = new $this->class(
$this->getRawAttributes($attributes)
);
if (isset($this->connection)) {
$instance->setConnection($this->connection);
}
return $instance;
});
}
makeInstance()
の中身を見てみると、$this->class
に作ろうとしているクラスの名前が入っているのですが、普通に new
してインスタンスを作っていることがわかりました。
Eloquent Model のインスタンスを作る際の引数は、代入したい属性が入った配列です。1
ここでは、make()
の引数として渡された配列 $attributes
を getRawAttributes()
というメソッドで処理してから渡しています。
この getRawAttributes()
が、Factory で定義したクロージャを呼び出したり、ステートの適用なんかもやっていそうです。
/**
* Get a raw attributes array for the model.
*
* @param array $attributes
* @return mixed
*
* @throws \InvalidArgumentException
*/
protected function getRawAttributes(array $attributes = [])
{
if (! isset($this->definitions[$this->class][$this->name])) {
throw new InvalidArgumentException("Unable to locate factory with name [{$this->name}] [{$this->class}].");
}
$definition = call_user_func(
$this->definitions[$this->class][$this->name],
$this->faker, $attributes
);
return $this->expandAttributes(
array_merge($this->applyStates($definition, $attributes), $attributes)
);
}
真ん中の call_user_func()
で呼び出しているのは、$factory->define()
の第二引数に渡したクロージャです。2
そうして Factory で定義した値を $definition
として受け取った後、applyStates()
でステートで定義した値を適用して、それを expandAttributes()
で展開してます。
ぱっと見 expandAttributes()
の方が怪しいですが、先に applyStates()
の方を見てみましょう。
/**
* Apply the active states to the model definition array.
*
* @param array $definition
* @param array $attributes
* @return array
*
* @throws \InvalidArgumentException
*/
protected function applyStates(array $definition, array $attributes = [])
{
foreach ($this->activeStates as $state) {
if (! isset($this->states[$this->class][$state])) {
if ($this->stateHasAfterCallback($state)) {
continue;
}
throw new InvalidArgumentException("Unable to locate [{$state}] state for [{$this->class}].");
}
$definition = array_merge(
$definition,
$this->stateAttributes($state, $attributes)
);
}
return $definition;
}
foreach
で有効になっているステートをひとつずつ処理しています。
前半は例外処理なので無視して、後半で stateAttributes()
で得た値を、Factory で定義した値である $definition
とマージしています。
ということは、この時点ではまだ Factory で定義した属性クロージャは呼び出されてなさそうです。
次に怪しかった expandAttributes()
の方を見てみます。
/**
* Expand all attributes to their underlying values.
*
* @param array $attributes
* @return array
*/
protected function expandAttributes(array $attributes)
{
foreach ($attributes as &$attribute) {
if (is_callable($attribute) && ! is_string($attribute) && ! is_array($attribute)) {
$attribute = $attribute($attributes);
}
if ($attribute instanceof static) {
$attribute = $attribute->create()->getKey();
}
if ($attribute instanceof Model) {
$attribute = $attribute->getKey();
}
}
return $attributes;
}
foreach
で属性をひとつずつ処理していますが、ここで属性 $attribute
が is_callable()
なときに、呼び出して値を取得しているのを発見できました。3
つまり、Factory で定義した値に、ステートで定義した値をマージして、make()
の引数で渡した値をマージして、最後に値の中の属性クロージャを呼び出し、それを使ってインスタンスを作っています。
この流れがわかったことで、属性クロージャの役割がわかってきました。
わかったこと
属性クロージャを使わない場合は、初めに Factory で定義した値を取得する段階で、モデルとのリレーションが定義されます。
$factory->define(App\Post::class, function ($faker) {
return [
'title' => $faker->title,
'content' => $faker->paragraph,
'user_id' => factory(App\User::class)->create()->id,
];
});
この例でいうと、初めの段階で App\User
クラスに対応するレコードが作成されてしまいます。
このレコードが必ず使われるならよいのですが、場合によってはステートや make()
に渡す引数で上書きされることもあります。
例えばこんな場合ですね。
$user = factory(App\User::class)->create();
$post = factory(App\User::class)->make([
'user_id' => $user->id,
]);
この場合は、factory()->make()
に渡した方の User
は使われますが、Factory で定義した User
は破棄されて使われません。
ということは、無駄なレコードが 1 行増えてしまうことになります。
一方で、公式ドキュメントに書かれていた書き方だと、
$factory->define(App\Post::class, function ($faker) {
return [
'title' => $faker->title,
'content' => $faker->paragraph,
'user_id' => function () {
return factory(App\User::class)->create()->id;
}
];
});
Factory で定義された属性クロージャは、make()
で渡された値とマージする際に上書きされて消えてしまうので、実行されません。
なので、無駄なレコードは作られません。
また、同じ使い方で考えたときに、公式ドキュメントに載っていたもうひとつの例の方の意味もわかってきます。
$factory->define(App\Post::class, function ($faker) {
return [
'title' => $faker->title,
'content' => $faker->paragraph,
'user_id' => function () {
return factory(App\User::class)->create()->id;
},
'user_type' => function (array $post) {
return App\User::find($post['user_id'])->type;
}
];
});
こう書けば、ステートや make()
に渡す引数で user_id
だけを上書きした場合でも、無駄なレコードが作られず、かつ user_type
も正しく設定されます。
結論
Factory の定義を書く際に、レコードを作るような、外部に影響をもたらす処理を書きたい場合は、属性クロージャを使って書いた方がよいということがわかりました。
そうすれば、本当に必要になったときだけ処理が呼び出されるので、余計な影響が減らせます。