Skip to content

Commit

Permalink
Merge pull request #51 from shaffe-fr/feature/relations
Browse files Browse the repository at this point in the history
Support relations stack & foreign relations
  • Loading branch information
christophrumpel authored Jun 21, 2020
2 parents ec15d74 + 12184f5 commit 0e7a45f
Show file tree
Hide file tree
Showing 6 changed files with 180 additions and 16 deletions.
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions example/app/Models/Recipe.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateIngredientRecipeTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('ingredient_recipe', function (Blueprint $table) {
$table->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');
}
}
30 changes: 30 additions & 0 deletions example/tests/Factories/IngredientFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

namespace ExampleAppTests\Factories;

use Christophrumpel\LaravelFactoriesReloaded\BaseFactory;
use ExampleApp\Models\Ingredient;
use Faker\Generator;

class IngredientFactory extends BaseFactory
{
protected string $modelClass = Ingredient::class;

public function create(array $extra = []): Ingredient
{
return parent::build($extra);
}

public function make(array $extra = []): Ingredient
{
return parent::build($extra, 'make');
}

public function getDefaults(Generator $faker): array
{
return [
'name' => 'Pasta',
'description' => 'Good pasta!',
];
}
}
57 changes: 42 additions & 15 deletions src/BaseFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -15,8 +17,6 @@ abstract class BaseFactory implements FactoryInterface

protected Collection $relatedModelFactories;

protected string $relatedModelRelationshipName;

protected Generator $faker;

protected array $overwriteDefaults = [];
Expand Down Expand Up @@ -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)
Expand All @@ -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;
}
Expand All @@ -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;
}
}
51 changes: 50 additions & 1 deletion tests/FactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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());
}

Expand Down Expand Up @@ -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());
}
}

0 comments on commit 0e7a45f

Please sign in to comment.