Skip to content

Commit 50a1577

Browse files
authored
[9.x] Allow factories to recycle multiple models of a given type (#44328)
* Eloquent Factories: Allow for recycling multiple models of a given type * Eloquent Factories: Fix code styling * Eloquent Factories: Fix code styling
1 parent bf43e5a commit 50a1577

File tree

3 files changed

+89
-10
lines changed

3 files changed

+89
-10
lines changed

src/Illuminate/Database/Eloquent/Factories/BelongsToRelationship.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ protected function resolver($key)
7070
return function () use ($key) {
7171
if (! $this->resolved) {
7272
$instance = $this->factory instanceof Factory
73-
? ($this->factory->recycle->get($this->factory->modelName()) ?? $this->factory->create())
73+
? ($this->factory->getRandomRecycledModel($this->factory->modelName()) ?? $this->factory->create())
7474
: $this->factory;
7575

7676
return $this->resolved = $key ? $instance->{$key} : $instance->getKey();

src/Illuminate/Database/Eloquent/Factories/Factory.php

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ abstract class Factory
6868
*
6969
* @var \Illuminate\Support\Collection
7070
*/
71-
public $recycle;
71+
protected $recycle;
7272

7373
/**
7474
* The "after making" callbacks that will be applied to the model.
@@ -464,9 +464,8 @@ protected function expandAttributes(array $definition)
464464
return collect($definition)
465465
->map($evaluateRelations = function ($attribute) {
466466
if ($attribute instanceof self) {
467-
$attribute = $this->recycle->has($attribute->modelName())
468-
? $this->recycle->get($attribute->modelName())
469-
: $attribute->recycle($this->recycle)->create()->getKey();
467+
$attribute = $this->getRandomRecycledModel($attribute->modelName())
468+
?? $attribute->recycle($this->recycle)->create()->getKey();
470469
} elseif ($attribute instanceof Model) {
471470
$attribute = $attribute->getKey();
472471
}
@@ -619,21 +618,35 @@ public function for($factory, $relationship = null)
619618
}
620619

621620
/**
622-
* Provide a model instance to use instead of any nested factory calls when creating relationships.
621+
* Provide model instances to use instead of any nested factory calls when creating relationships.
623622
*
624623
* @param \Illuminate\Eloquent\Model|\Illuminate\Support\Collection|array $model
625624
* @return static
626625
*/
627626
public function recycle($model)
628627
{
628+
// Group provided models by the type and merge them into existing recycle collection
629629
return $this->newInstance([
630-
'recycle' => $this->recycle->merge(
631-
Collection::wrap($model instanceof Model ? func_get_args() : $model)
632-
->keyBy(fn ($model) => get_class($model))
633-
),
630+
'recycle' => $this->recycle
631+
->flatten()
632+
->merge(
633+
Collection::wrap($model instanceof Model ? func_get_args() : $model)
634+
->flatten()
635+
)->groupBy(fn ($model) => get_class($model)),
634636
]);
635637
}
636638

639+
/**
640+
* Retrieves a random model of a given type from previously provided models to recycle.
641+
*
642+
* @param string $modelClassName
643+
* @return \Illuminate\Database\Eloquent\Model|null
644+
*/
645+
public function getRandomRecycledModel($modelClassName)
646+
{
647+
return $this->recycle->get($modelClassName)?->random();
648+
}
649+
637650
/**
638651
* Add a new "after making" callback to the model definition.
639652
*

tests/Database/DatabaseEloquentFactoryTest.php

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -701,6 +701,72 @@ public function test_has_method_does_not_reassign_the_parent()
701701
$this->assertSame(2, FactoryTestPost::count());
702702
}
703703

704+
public function test_multiple_models_can_be_provided_to_recycle()
705+
{
706+
Factory::guessFactoryNamesUsing(function ($model) {
707+
return $model.'Factory';
708+
});
709+
710+
$users = FactoryTestUserFactory::new()->count(3)->create();
711+
712+
$posts = FactoryTestPostFactory::new()
713+
->recycle($users)
714+
->for(FactoryTestUserFactory::new())
715+
->has(FactoryTestCommentFactory::new()->count(5), 'comments')
716+
->count(2)
717+
->create();
718+
719+
$this->assertSame(3, FactoryTestUser::count());
720+
}
721+
722+
public function test_recycled_models_can_be_combined_with_multiple_calls()
723+
{
724+
Factory::guessFactoryNamesUsing(function ($model) {
725+
return $model.'Factory';
726+
});
727+
728+
$users = FactoryTestUserFactory::new()
729+
->count(2)
730+
->create();
731+
$posts = FactoryTestPostFactory::new()
732+
->recycle($users)
733+
->count(2)
734+
->create();
735+
$additionalUser = FactoryTestUserFactory::new()
736+
->create();
737+
$additionalPost = FactoryTestPostFactory::new()
738+
->recycle($additionalUser)
739+
->create();
740+
741+
$this->assertSame(3, FactoryTestUser::count());
742+
$this->assertSame(3, FactoryTestPost::count());
743+
744+
$comments = FactoryTestCommentFactory::new()
745+
->recycle($users)
746+
->recycle($posts)
747+
->recycle([$additionalUser, $additionalPost])
748+
->count(5)
749+
->create();
750+
751+
$this->assertSame(3, FactoryTestUser::count());
752+
$this->assertSame(3, FactoryTestPost::count());
753+
}
754+
755+
public function test_no_models_can_be_provided_to_recycle()
756+
{
757+
Factory::guessFactoryNamesUsing(function ($model) {
758+
return $model.'Factory';
759+
});
760+
761+
$posts = FactoryTestPostFactory::new()
762+
->recycle([])
763+
->count(2)
764+
->create();
765+
766+
$this->assertSame(2, FactoryTestPost::count());
767+
$this->assertSame(2, FactoryTestUser::count());
768+
}
769+
704770
/**
705771
* Get a database connection instance.
706772
*

0 commit comments

Comments
 (0)