rawfan's avatar
Level 19

Re-using nested relations in model factories

Lets assume I have 3 models: Project, Objective and Contact with the following relations:

Project:

  • hasMany: Objective
  • hasMany: Contact

Objective:

  • belongsTo: Project
  • belongsTo: Contact

Contact:

  • belongsTo: Project
  • hasMany: Objective

The factories for Objective and Contact look something like this:

class ObjectiveFactory extends Factory
{
    public function definition()
    {
        return [
            'name' => fake()->name(),
            'project_id' => Project::factory(),
            'contact_id' => Contact::factory(),
        ];        
    }
}

class ContactFactory extends Factory
{
    public function definition()
    {
        return [
            'name' => fake()->name(),
            'project_id' => Project::factory()
        ];        
    }
}

The seeding code looks like this

Project::factory()->count(5)
    ->has(Objective::factory()->count(10))
    ->create();

This will cause 55 projects to be created, because the ContactFactory will always call the ProjectFactory. The only way I found to solve this doesn't look very elegant:

Project::factory()->count(5)->create()
    ->each(function ($project) {
        $contacts = Contact::factory()
            ->recycle($project)
            ->count(20)
            ->create();
        Objective::factory()
            ->recycle($project)
            ->recycle($contacts)
            ->count(20)
            ->create();
    });

Any better ideas?

0 likes
1 reply
LaryAI's avatar
Level 58

To address the issue of creating redundant projects when using nested relations in model factories, you can leverage the state method in Laravel's factories to pass existing project and contact instances to the ObjectiveFactory. This way, you can avoid creating new projects and contacts unnecessarily.

Here's a more elegant solution:

  1. Define the ObjectiveFactory and ContactFactory with states:
use App\Models\Project;
use App\Models\Contact;
use App\Models\Objective;
use Illuminate\Database\Eloquent\Factories\Factory;

class ObjectiveFactory extends Factory
{
    protected $model = Objective::class;

    public function definition()
    {
        return [
            'name' => $this->faker->name(),
            'project_id' => Project::factory(),
            'contact_id' => Contact::factory(),
        ];
    }

    public function forProject(Project $project)
    {
        return $this->state([
            'project_id' => $project->id,
        ]);
    }

    public function forContact(Contact $contact)
    {
        return $this->state([
            'contact_id' => $contact->id,
        ]);
    }
}

class ContactFactory extends Factory
{
    protected $model = Contact::class;

    public function definition()
    {
        return [
            'name' => $this->faker->name(),
            'project_id' => Project::factory(),
        ];
    }

    public function forProject(Project $project)
    {
        return $this->state([
            'project_id' => $project->id,
        ]);
    }
}
  1. Seed the database with the correct relationships:
use App\Models\Project;
use App\Models\Contact;
use App\Models\Objective;

Project::factory()->count(5)->create()->each(function ($project) {
    $contacts = Contact::factory()
        ->forProject($project)
        ->count(20)
        ->create();

    $contacts->each(function ($contact) use ($project) {
        Objective::factory()
            ->forProject($project)
            ->forContact($contact)
            ->count(10)
            ->create();
    });
});

Explanation:

  1. Factory Definitions:

    • The ObjectiveFactory and ContactFactory are defined with additional methods forProject and forContact to set the project_id and contact_id respectively.
    • These methods use the state method to override the default values with the provided instances.
  2. Seeding:

    • Create 5 projects.
    • For each project, create 20 contacts using the forProject state method to ensure they belong to the current project.
    • For each contact, create 10 objectives using both forProject and forContact state methods to ensure they belong to the current project and contact.

This approach ensures that the correct relationships are maintained without creating redundant projects.

Please or to participate in this conversation.