From ba8b80be3bca919ab8c2666008f82d702ec6f4c1 Mon Sep 17 00:00:00 2001 From: Bert Heyman Date: Thu, 3 Apr 2025 20:37:57 +0200 Subject: [PATCH] Fix factory recycle for many to many relationships --- .../Factories/BelongsToManyRelationship.php | 5 +++- .../Database/Eloquent/Factories/Factory.php | 30 +++++++++++++++---- .../Database/DatabaseEloquentFactoryTest.php | 16 ++++++++++ 3 files changed, 44 insertions(+), 7 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Factories/BelongsToManyRelationship.php b/src/Illuminate/Database/Eloquent/Factories/BelongsToManyRelationship.php index da004c83bc74..5e619c23436d 100644 --- a/src/Illuminate/Database/Eloquent/Factories/BelongsToManyRelationship.php +++ b/src/Illuminate/Database/Eloquent/Factories/BelongsToManyRelationship.php @@ -50,7 +50,10 @@ public function __construct($factory, $pivot, $relationship) */ public function createFor(Model $model) { - Collection::wrap($this->factory instanceof Factory ? $this->factory->create([], $model) : $this->factory)->each(function ($attachable) use ($model) { + Collection::wrap($this->factory instanceof Factory + ? ($this->factory->getRandomRecycledModelsForCount($this->factory->modelName()) ?? $this->factory->create([], $model)) + : $this->factory + )->each(function ($attachable) use ($model) { $model->{$this->relationship}()->attach( $attachable, is_callable($this->pivot) ? call_user_func($this->pivot, $model) : $this->pivot diff --git a/src/Illuminate/Database/Eloquent/Factories/Factory.php b/src/Illuminate/Database/Eloquent/Factories/Factory.php index ffe9018e67f0..e16907593c86 100644 --- a/src/Illuminate/Database/Eloquent/Factories/Factory.php +++ b/src/Illuminate/Database/Eloquent/Factories/Factory.php @@ -8,6 +8,7 @@ use Illuminate\Contracts\Foundation\Application; use Illuminate\Database\Eloquent\Collection as EloquentCollection; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Support\Carbon; use Illuminate\Support\Collection; @@ -676,6 +677,19 @@ public function getRandomRecycledModel($modelClassName) return $this->recycle->get($modelClassName)?->random(); } + /** + * Retrieve a random collection of models based on the count amount of a given type from previously provided models to recycle. + * + * @template TClass of \Illuminate\Database\Eloquent\Model + * + * @param class-string $modelClassName + * @return \Illuminate\Support\Collection|null + */ + public function getRandomRecycledModelsForCount($modelClassName) + { + return $this->recycle->get($modelClassName)?->random($this->count); + } + /** * Add a new "after making" callback to the model definition. * @@ -979,12 +993,16 @@ public function __call($method, $parameters) if (str_starts_with($method, 'for')) { return $this->for($factory->state($parameters[0] ?? []), $relationship); } elseif (str_starts_with($method, 'has')) { - return $this->has( - $factory - ->count(is_numeric($parameters[0] ?? null) ? $parameters[0] : 1) - ->state((is_callable($parameters[0] ?? null) || is_array($parameters[0] ?? null)) ? $parameters[0] : ($parameters[1] ?? [])), - $relationship - ); + $factory = $factory + ->count(is_numeric($parameters[0] ?? null) ? $parameters[0] : 1) + ->state((is_callable($parameters[0] ?? null) || is_array($parameters[0] ?? null)) ? $parameters[0] : ($parameters[1] ?? [])); + + $relationshipType = $this->newModel()->{$relationship}(); + if ($relationshipType instanceof BelongsToMany) { + return $this->hasAttached($factory, [], $relationship); + } + + return $this->has($factory, $relationship); } } } diff --git a/tests/Database/DatabaseEloquentFactoryTest.php b/tests/Database/DatabaseEloquentFactoryTest.php index b0860d236b03..33182e6fd007 100644 --- a/tests/Database/DatabaseEloquentFactoryTest.php +++ b/tests/Database/DatabaseEloquentFactoryTest.php @@ -740,6 +740,22 @@ public function test_for_method_recycles_models() $this->assertSame(1, FactoryTestUser::count()); } + public function test_has_method_recycles_models() + { + Factory::guessFactoryNamesUsing(function ($model) { + return $model.'Factory'; + }); + + $roles = FactoryTestRoleFactory::new()->count(2)->create(); + $user = FactoryTestUserFactory::new() + ->recycle($roles) + ->hasRoles(2) + ->create(); + + $this->assertTrue($roles->pluck('id')->contains($user->roles[0]->id)); + $this->assertTrue($roles->pluck('id')->contains($user->roles[1]->id)); + } + public function test_has_method_does_not_reassign_the_parent() { Factory::guessFactoryNamesUsing(function ($model) {