Introduction

In this post, I will show you how to create factories for models that have polymorphic relationships with other models in your app.

This post assumes that you are familiar with polymorphic relationships. If that’s not true, head to the documentation to learn more.

Solution

As an example, let’s consider an app where we allow users to rate games or movies. For that, we define two Eloquent models - Game and Movie. Additionally, to avoid repetition, we use a single Rating model for both entities.

A simplified database structure for our app could look like this:

games
    id - integer
    name - string
 
movies
    id - integer
    name - string
 
ratings
    id - integer
    rating - integer
    rateable_id - integer
    rateable_type - string

Now, let’s move on to factories. For both Game and Movie, it would be a simple, regular factory. Something like this:

class GameFactory extends Factory
{
    public function definition(): array
    {
        return [
            'name' => fake()->word(),
        ];
    }
}

I’m only showing the code for the GameFactory, as the MovieFactory would look almost identical.

The factory for our Rating model is going to be a bit more complicated, and we could define it like this:

class RatingFactory extends Factory
{
    public function definition(): array
    {
        return [
            'rateable_id' => Game::factory(),
            'rateable_type' => function (array $attributes) {
                return Game::find($attributes['rateable_id'])->getMorphClass();
            }
        ];
    }
}

It’s a typical pattern to assign a new factory instance to the foreign key of the relationship (hence the 'rateable_id' => Game::factory()). Thanks to this, when we create a new Rating using the factory, a new Game instance will be automatically created for us.

The rateable_type attribute is more interesting. Potentially, we could hardcode the value to App\Models\Game, and it would work just fine because, by default, Laravel uses the fully qualified class name to store the “type” of the related model. However, I like to decouple my app internals from the database and usually change these values.

To do that, you can use the following code (placed in the boot method of one of your service providers) to store simple strings as the “type” in the database:

Relation::enforceMorphMap([
    'game' => 'App\Models\Game',
    'movie' => 'App\Models\Movie',
]);

But after that change, we must provide correct values to our RatingFactory. That’s precisely why we we used the getMorphClass() on the model instance. It allows us to get the valid morph alias at runtime.

Also, we used a closure to access other attributes defined in the factory (in particular, the rateable_id) to find the newly created Game model on which we call the getMorphClass() method.

To use the factory and create a new Rating, we can now call Rating::factory()->create().

Notice that when we use this factory, by default, it will associate the new Rating model with a new Game. What if we would like to use a Movie model instead (or any other that might be added in the future)?

To do that, we could leverage factory states. We can define a method called forMovie in our factory that will only modify the rateable_id and rateable_type attributes accordingly (leaving the remaining ones untouched):

public function forMovie(): Factory
{
    return $this->state(function (array $attributes) {
        return [
            'rateable_id' => Movie::factory(),
            'rateable_type' => function (array $attributes) {
                return Movie::find($attributes['rateable_id'])->getMorphClass();
            }
        ];
    });
}

Then, you can create a new Rating associated with a Movie by calling Rating::factory()->forMovie()->create().

Similarly, you could define a separate method for any other model related to Rating that you might add in the future.

Summary

In summary, creating factories for models with polymorphic relationships in Laravel can be a bit trickier than the standard ones. However, by following the steps outlined in this post, I hope you won’t have any problems with that.

If you have any questions, feel free to leave a comment here or reach out to me on Twitter.