From 12184f5ea9a13dcf02cfdb00e6ed904a950588c0 Mon Sep 17 00:00:00 2001 From: Karel FAILLE Date: Tue, 9 Jun 2020 01:58:33 +0200 Subject: [PATCH] Support relations stack & foreign relations --- README.md | 14 +++++ example/app/Models/Recipe.php | 12 ++++ ...09_0000_create_ingredient_recipe_table.php | 32 +++++++++++ example/tests/Factories/IngredientFactory.php | 30 ++++++++++ src/BaseFactory.php | 57 ++++++++++++++----- tests/FactoryTest.php | 51 ++++++++++++++++- 6 files changed, 180 insertions(+), 16 deletions(-) create mode 100644 example/database/migrations/2020_06_09_0000_create_ingredient_recipe_table.php create mode 100644 example/tests/Factories/IngredientFactory.php diff --git a/README.md b/README.md index 83316e4..6ade3c9 100644 --- a/README.md +++ b/README.md @@ -131,6 +131,20 @@ Here were are getting a user instance that has three related recipes attached. T > :warning: **Note**: For this to work, you need to have a new RecipeFactory already created. +You can create many related models instances by chaining `with`s. + +```php +$recipe = RecipeFactory::new() + ->with(Group::class, 'group') + ->with(Ingredient::class, 'ingredients') + ->with(Ingredient::class, 'ingredients', 3) + ->create(); +``` + +Here we are getting a recipe that has a group and four ingredients. + +> :warning: **Note**: Up to the version 1.0.8, only the last `with` relation is built. + In Laravel factories, you could also define a related model in your default data like: ```php diff --git a/example/app/Models/Recipe.php b/example/app/Models/Recipe.php index 7b56b23..44425e9 100644 --- a/example/app/Models/Recipe.php +++ b/example/app/Models/Recipe.php @@ -3,8 +3,20 @@ namespace ExampleApp\Models; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; class Recipe extends Model { protected $fillable = ['name', 'description', 'group_id']; + + public function group(): BelongsTo + { + return $this->belongsTo(Group::class); + } + + public function ingredients(): BelongsToMany + { + return $this->belongsToMany(Ingredient::class); + } } diff --git a/example/database/migrations/2020_06_09_0000_create_ingredient_recipe_table.php b/example/database/migrations/2020_06_09_0000_create_ingredient_recipe_table.php new file mode 100644 index 0000000..62dc440 --- /dev/null +++ b/example/database/migrations/2020_06_09_0000_create_ingredient_recipe_table.php @@ -0,0 +1,32 @@ +unsignedBigInteger('ingredient_id'); + $table->unsignedBigInteger('recipe_id'); + $table->primary(['ingredient_id', 'recipe_id']); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::drop('ingredient_recipe'); + } +} diff --git a/example/tests/Factories/IngredientFactory.php b/example/tests/Factories/IngredientFactory.php new file mode 100644 index 0000000..cb1b161 --- /dev/null +++ b/example/tests/Factories/IngredientFactory.php @@ -0,0 +1,30 @@ + 'Pasta', + 'description' => 'Good pasta!', + ]; + } +} diff --git a/src/BaseFactory.php b/src/BaseFactory.php index 7675792..cf33afa 100644 --- a/src/BaseFactory.php +++ b/src/BaseFactory.php @@ -4,7 +4,9 @@ use Faker\Factory as FakerFactory; use Faker\Generator; +use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Collection; +use InvalidArgumentException; use ReflectionClass; abstract class BaseFactory implements FactoryInterface @@ -15,8 +17,6 @@ abstract class BaseFactory implements FactoryInterface protected Collection $relatedModelFactories; - protected string $relatedModelRelationshipName; - protected Generator $faker; protected array $overwriteDefaults = []; @@ -46,16 +46,7 @@ protected function build(array $extra = [], string $creationType = 'create') return $model; } - $relatedModels = $this->relatedModelFactories->map->make(); - - if ($creationType === 'create') { - $model->{$this->relatedModelRelationshipName}() - ->saveMany($relatedModels); - - return $model; - } - - return $model->setRelation($this->relatedModelRelationshipName, $relatedModels); + return $this->buildRelationsForModel($model, $creationType); } protected function unguardedIfNeeded(\Closure $closure) @@ -74,13 +65,16 @@ public function times(int $times = 1): MultiFactoryCollection })); } + /** @return static */ public function with(string $relatedModelClass, string $relationshipName, int $times = 1): self { $clone = clone $this; - $clone->relatedModelFactories = collect()->times($times, fn () => $this->getFactoryFromClassName($relatedModelClass)); - - $clone->relatedModelRelationshipName = $relationshipName; + $clone->relatedModelFactories = clone $clone->relatedModelFactories; + $clone->relatedModelFactories[$relationshipName] ??= collect(); + $clone->relatedModelFactories[$relationshipName] = $clone->relatedModelFactories[$relationshipName]->merge( + collect()->times($times, fn() => $this->getFactoryFromClassName($relatedModelClass)) + ); return $clone; } @@ -107,4 +101,37 @@ protected function getFactoryFromClassName(string $className): FactoryInterface return new $factoryClass($this->faker); } + + private function buildRelationsForModel(Model $model, string $creationType): Model + { + foreach ($this->relatedModelFactories as $relationshipName => $factories) { + $relation = $model->{$relationshipName}(); + + if (method_exists($relation, 'saveMany')) { + $relatedModels = $factories->map->make(); + $model->setRelation($relationshipName, $relatedModels); + + if ($creationType === 'create') { + $relation->saveMany($relatedModels); + } + + continue; + } + + if (method_exists($relation, 'associate')) { + $relatedModels = $factories->map->$creationType(); + $relatedModels->each(fn($related) => $relation->associate($related)); + + if ($creationType === 'create') { + $model->save(); + } + + continue; + } + + throw new InvalidArgumentException('Unsupported relation `'.$relationshipName.'` of ` type `'.get_class($relation).'`.'); + } + + return $model; + } } diff --git a/tests/FactoryTest.php b/tests/FactoryTest.php index 0bbc126..50b26ed 100644 --- a/tests/FactoryTest.php +++ b/tests/FactoryTest.php @@ -3,6 +3,7 @@ namespace Christophrumpel\LaravelFactoriesReloaded\Tests; use ExampleApp\Models\Group; +use ExampleApp\Models\Ingredient; use ExampleApp\Models\Recipe; use ExampleAppTests\Factories\GroupFactory; use ExampleAppTests\Factories\GroupFactoryUsingFaker; @@ -242,7 +243,7 @@ public function the_factory_is_immutable_when_adding_related_models(): void $firstGroup = $group->with(Recipe::class, 'recipes')->create(); $secondGroup = $group->create(); - $this->assertEquals(1, $firstGroup->recipes()->count()); + $this->assertEquals(5, $firstGroup->recipes()->count()); $this->assertEquals(4, $secondGroup->recipes()->count()); } @@ -335,4 +336,52 @@ public function it_lets_you_add_related_models_when_creating_multiple() $this->assertCount(5, $group->recipes); }); } + + /** @test */ + public function it_lets_you_create_many_related_models_at_once() + { + Config::set('factories-reloaded.factories_namespace', 'ExampleAppTests\Factories'); + + $recipe = RecipeFactory::new() + ->with(Group::class, 'group') + ->with(Ingredient::class, 'ingredients', 3) + ->create(); + + $this->assertCount(1, Group::all()); + $this->assertCount(3, Ingredient::all()); + $this->assertTrue($recipe->group()->exists()); + $this->assertEquals(3, $recipe->ingredients()->count()); + } + + /** @test */ + public function it_lets_you_make_many_related_models_at_once() + { + Config::set('factories-reloaded.factories_namespace', 'ExampleAppTests\Factories'); + + $recipe = RecipeFactory::new() + ->with(Group::class, 'group') + ->with(Ingredient::class, 'ingredients', 3) + ->make(); + + $this->assertCount(0, Group::all()); + $this->assertCount(0, Ingredient::all()); + + $this->assertInstanceOf(Group::class, $recipe->group); + $this->assertEquals(3, $recipe->ingredients->count()); + $this->assertInstanceOf(Ingredient::class, $recipe->ingredients->first()); + } + + /** @test */ + public function it_lets_you_stack_relations() + { + Config::set('factories-reloaded.factories_namespace', 'ExampleAppTests\Factories'); + + $recipe = RecipeFactory::new() + ->with(Ingredient::class, 'ingredients') + ->with(Ingredient::class, 'ingredients', 3) + ->create(); + + $this->assertCount(4, Ingredient::all()); + $this->assertEquals(4, $recipe->ingredients()->count()); + } }