Laravel Polymorphic Database Seeder Factory
Asked Answered
D

10

7

How can I create a database seeder factory for the following configuration?

User

// create_users_table.php
Schema::create('users', function (Blueprint $table) {
    $table->increments('id');
    ...
}

// User.php
public function notes()
{
    return $this->morphMany('App\Note', 'noteable');
}

Complex

// create_complex_table.php
Schema::create('complex', function (Blueprint $table) {
    $table->increments('id');
    ...
}

// Complex.php
public function notes()
{
    return $this->morphMany('App\Note', 'noteable');
}

Notes

// create_notes_table.php
Schema::create('notes', function (Blueprint $table) {
    $table->increments('id');
    $table->integer('noteable_id');
    $table->string('noteable_type');
    ...
}

// Note.php
public function noteable()
{
    return $this->morphTo();
}

I am struggling to see the most robust way of ensuring that I am not just filling in random id's that may not exist.

Demise answered 10/3, 2018 at 4:47 Comment(0)
R
15

I have improved upon HyperionX's answer and removed the static elements from it.

$factory->define(App\Note::class, function (Faker $faker) {
    $noteable = [
        App\User::class,
        App\Complex::class,
    ]; // Add new noteables here as we make them
    $noteableType = $faker->randomElement($noteables);
    $noteable = factory($noteableType)->create();

    return [
        'noteable_type' => $noteableType,
        'noteable_id' => $noteable->id,
        ...
    ];
});

Basically, we pick one of the noteable classes at random, then call it's own factory to get an instance of noteable, thus we get rid of the staticness of the OP's answer.

Raglan answered 14/8, 2018 at 5:50 Comment(0)
C
7

If you are using a morph map the given solutions won't work because the type won't be the same as the class name.

This will work in combination with a morph map.

Until Laravel 7

$factory->define(App\Note::class, function (Faker $faker) {
    $noteable = $faker->randomElement([
        App\User::class,
        App\Complex::class,
    ]);

    return [
        'noteable_id' => factory($noteable),
        'noteable_type' => array_search($noteable, Relation::$morphMap),
        ...
    ];
});

From Laravel 8

public function definition(): array
{
    /** @var class-string<\App\Models\User|\App\Models\Complex> $noteable */
    $noteable = $this->faker->randomElement([
        App\Models\User::class,
        App\Models\Complex::class,
    ]);

    return [
        'noteable_type' => array_search($noteable, Relation::$morphMap),
        'noteable_id' => $noteable::factory(),
    ];
}

More information about morph map could be found here: https://laravel.com/docs/8.x/eloquent-relationships#custom-polymorphic-types

Cacao answered 17/4, 2020 at 15:46 Comment(1)
Excellent and concise solution that works for all use cases!Chelicera
C
3

Updated Answer

If you don't care about controlling which morphable model is created:

public function definition()
{
    return [
        'content' => $this->faker->paragraph(),
        'noteable_id' => function (array $attributes) {
            return $attributes['noteable_type']::factory();
        }),
        'noteable_type' => $this->faker->randomElement([
            Complex::factory(),
            User::factory()
        ]),
        'title' => $this->faker->sentence()
    ];
}

Original answer

I have another solution that does not imply the use of the randomElement, which is good by the way, but can be problematic when you need control on the morphable model that is being created. You still need to create model factories for the Note, User and Complex models. Then the run method of the Databaseeder class would look like this:

public function run()
{
    $userNotes = Note::factory()->count(10)->for(
        User::factory(), 'noteable'
    )->create();

    $complexNotes = Note::factory()->count(10)->for(
        Complex::factory(), 'noteable'
    )->create();
}

There is another approach by using the factory states. You still need to define model factories for your 3 models. Additionally you define two state transformation methods in the NoteFactory class.

<?php

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;

class NoteFactory extends Factory
{
    public function definition()
    {
        return [
            'content' => $this->faker->paragraph(),
            'title' => $this->faker->sentence()
        ];
    }

    public function forComplex()
    {
        return $this->state(function (array $attributes) {
            return [
                'noteable_type' => Complex::class,
                'noteable_id' => Complex::factory()
            ];
        });
    }

    public function forUser()
    {
        return $this->state(function (array $attributes) {
            return [
                'noteable_type' => User::class,
                'noteable_id' => User::factory()
            ];
        });
    }
}

In this case the Databaseeder class run method would look like this:

public function run()
{
    $userNotes = Note::factory()->count(10)->forUser()->create();

    $complexNotes = Note::factory()->count(10)->forComplex()->create();
}
Colonist answered 3/12, 2021 at 9:49 Comment(0)
D
2

Although a bit more static than I would like, here is my solution:

I created exactly 20 models of each class, that way I could ensure the Notes that are created don't try to link to something that may not exist, leaving a dangling Note object.

// NotesFactory.php
$factory->define(App\Note::class, function (Faker $faker) {
    $noteable = [
        App\User::class,
        App\Complex::class,
    ];

    return [
        'noteable_id' => $faker->numberBetween(0,20),
        'noteable_type' => $faker->randomElement($noteable),
        ...
    ];
});
Demise answered 10/3, 2018 at 5:30 Comment(0)
C
2
class CandidateFactory extends Factory {
   
    protected $model = \App\Models\Candidate::class;

    public function definition() {
        $applicants = [
            Contact::class,
            Advertiser::class,
        ];
        /** @var Model $applicant */
        $applicant = Arr::random( $applicants )::factory()->create();

        return [
            'applicant_type' => $applicant->getMorphClass(),
            'applicant_id'   => $applicant->getKey(),
        ];
    }
}

Then

    /**
     * @test
     */
    public function candidate_has_applicants() {
        $candidate = Candidate::factory()->create();
        $this->assertInstanceOf( Candidate::class, $candidate );

        $this->assertInstanceOf( Authenticatable::class, $candidate->applicant );
    }
Concert answered 28/9, 2021 at 7:7 Comment(0)
S
1

You could also do this without temporary variables like this:

$factory->define(App\Note::class, function (Faker $faker) {
    return [
        'noteable_type' => $faker->randomElement([
            App\User::class,
            App\Complex::class,
        ]),
        'noteable_id' => function (array $note) {
            return factory($note['noteable_type']);
        },
        ...
    ];
})
Seismology answered 25/2, 2020 at 18:35 Comment(0)
A
1

You can create 10 user with 10 notes then create 10 complex with 10 notes. like this 👇

public function run()
{
    factory(User::class,10)->create()->each(
        fn($user)=>
        factory(Note::class,10)
        ->create(["noteable_id"=>$user,"noteable_type"=>User::class]), 
    );
    factory(Complex::class,10)->create()->each(
        fn($complex)=>
        factory(Note::class,10)
        ->create(["noteable_id"=>$complex,"noteable_type"=>Complex::class]), 
    );
}
Alb answered 31/10, 2021 at 21:51 Comment(0)
A
0

If you already created instances of the concrete Notables, perhaps via different factories, you may don't want to create any more new instances of it. In this case, you could extend Barracuda's solution:

$factory->define(App\Note::class, function (Faker $faker) {
    $noteable = [
        User::class,
        Complex::class,
    ];

    $noteableType = $faker->randomElement($noteable);
    if ($noteableType === User::class) {
        $noteableId = User::all()->random()->id;
    } else {
        $noteableId = Complex::all()->random()->id;
    }

    return [
        'noteable_type' => $noteableType,
        'noteable_id' => $noteableId,
        ...
    ];
});

I realise this would require a little bit of additional overhead to maintain the if/else branches but you won't have the problem of using IDs that don't exist.

Perhaps there is also a more elegant and generic way to get the Model::all() based on the Model::class, however I am unaware of it.

Antimony answered 29/7, 2020 at 20:42 Comment(0)
M
0

My preference is to use it this way, according to the factory structure, in new versions of Laravel.

$notable = $this->faker->randomElement([
    [
        'id' => User::all()->random(),
        'type' => User::class,
    ],
    [
        'id' => Complex::all()->random(),
        'type' => Complex::class,
    ]
]);

return [
    'notable_id' => $notable['id'],
    'notable_type' => $notable['type'],
    ...
];
Montherlant answered 30/11, 2021 at 20:53 Comment(0)
O
0

You can extend model factory with state and pass to it related model, in state grab needed information and return merged results with default definition()

class NoteFactory extends Factory
{
    protected $model = Note::class;
    
    public function definition()
    {
        return [
            'title'      => $this->faker->sentence,
            'note'       => $this->faker->sentence,
            //...
        ];
    }
    
    //new method that specify related model 
    public function forModel($model)
    {
        return $this->state(function () use ($model){
            return [
                'noteable_type' => $model->getMorphClass(), //return App\Lead etc
                'noteable_id'   => $model->id,
            ];
        });
    }
}

//usage
Note::factory()
    ->forModel($lead) //created method passing instance of App\Lead object
    ->create();
Oporto answered 29/4, 2022 at 11:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.