Cascading Laravel Factories

Pavol Perdík
Mar 17 · 4 min read

In this article we address the problem of generating fake data for complex data model with a lot of relations and dependencies between them.


When writing tests, seeders or just playing around in the artisan tinker session, you may need to quickly create some fake data. Laravel comes with the concept of factories, that provides very convenient way of generating the data.

Things start to complicate when you have relations between the models. Imagine you have these three models and relations between them:

Example data model

Imagine you would like to test something — let’s say some Project behaviour. You need to create a Project:

factory(Project::class)->create()

But this won’t work supposing every project must be attached to the client (let’s imagine this is the case of your database model). You are required to create a Client model first:

$client = factory(Client::class)->create()

and then you can create a Project:

factory(Project::class)->create([
'client_id' => $client->getKey()
])

But what if the the Client model also needs some other related models before it can be created and they can also have some other dependencies. You end up with super complex code just to provide you some fake data.

Note: Laravel factories supports also feature called factory callbacks, which are not an option here, because our data model forces us to create the project with client_id set (it’s not nullable). Or are they? We’ll get back to that later.


Cascading Laravel Factories

What you can do is to create the dependent models within factory:

So we can simply call:

factory(Project::class)->create()

and the Client will be created as well.

And this can be cascaded endlessly — so with one simple command:

factory(Task::class)->create()

you can create not only one Task, but also a Project and a Client.

Reusing existing models

This is enough in some scenarios. But imagine you are trying to create a collection of projects. You probably don’t want to create a new client for every project. You might want to attach projects to some random existing client instead, like this:

'client_id' => Client::query()->inRandomOrder()->first()->getKey()

So how do we mix the two approaches? Let’s try to use factory states:

So now we may decide if we want to cascade dependencies creating new related models:

factory(Project::class)->state('relations-cascade')->create()

or reuse existing models:

factory(Project::class)->state('relations-reuse')->create()

Fluent API

This is very helpful, but it still doesn’t feel right. I mean, there should exist more readable, more fluent API.

Sometimes you may even need to choose which relations to cascade as new and for which rather reuse existing models. The ideal API could look like this:

// ideal API I dream aboutfactory(Task::class)
->cascade(['project'])
->reuse(['taskCategory', 'assignee'])
->create()

This would create a Task and a Project, but it would attach this Task to existing TaskCategory and existing Assignee. Very clean.

So how can we get closer towards this ideal solution?

If we take a look once again on factory callbacks, they actually cover the reusing scenario, take a look on afterMakingState() method. If we define such callback:

$factory->afterMakingState(Project::class, 'reuse',
function (Project $project, $faker) {
$project->client_id = Client::query()
->inRandomOrder()->first()->getKey();
}
);

then we can easily make and/or create a new project, both work:

>>> factory(App\Project::class)->state('reuse')->make()=> App\Project {#3034
title: "Eos veniam nihil totam omnis debitis.",
client_id: 2,
}
>>> factory(App\Project::class)->state('reuse')->create()=> App\Project {#3038
title: "Molestias omnis asperiores est aut officia.",
client_id: 5,
updated_at: "2020-03-14 13:37:21",
created_at: "2020-03-14 13:37:21",
id: 4,
}

But the cascade option is a bit more tricky. If we use the same afterMakingState()method, creating a project would be possible, but making a project would inevitably lead to creating a new client as well. This may or may not be a problem for you.

Only if we could somehow know, before the operation, if we are about to create or make the project.


Actually, what we really need is the ability to create beforeCreatingState callback, that would do the trick:

$factory->beforeCreatingState(Project::class, 'cascade',
function (Project $project, $faker) {
$project->client_id = factory(Client::class)
->create()->getKey();
}
);

To achieve this we need to extend:

  • Illuminate\Database\Eloquent\Factory to enable the option to define this new callback
  • Illuminate\Database\Eloquent\FactoryBuilder so we can use this callback
  • and register the new binding in App\Providers\AppServiceProvider

Now let’s wrap these two callbacks so the code is a bit easier to read:

Now we can define factory like this:

$factory->cascade(Project::class,
function (Project $project, $faker) {
$project->client_id = factory(Client::class)->create()
->getKey();
}
);

$factory->reuse(Project::class,
function (Project $project, $faker) {
$project->client_id = Client::query()->inRandomOrder()
->first()->getKey();
}
);

And use it like this:

factory(App\Project::class)->reuse()->create()factory(App\Project::class)->reuse()->make()factory(App\Project::class)->cascade()->create()// combination of "cascading" while "making" does not make sense

And of course, in case of a model with more complex relations, you can define factory like this:

Finally we can use this factory to create new tasks like we wanted:

// cascade all relations
factory(Task::class)->cascade()->create()
// cascade only one relation, reuse others
factory(Task::class)
->cascade(['project'])
->reuse(['taskCategory', 'assignee'])
->create()

Conclusion

In this article we’ve tried to introduce the problem of preparing fake data in a more complex project with a lot of relations between models.

We have proposed the solution of extending Laravel Factory feature introducing new methods to define how to feed the relations with the fake data. We’ve discussed two typical scenarios:

  1. creating new instances for related models
  2. reusing existing instances for related models

Pavol Perdík

Written by

Founder of BRACKETS (https://www.brackets.sk/) and Craftable (https://www.getcraftable.com/)

More From Medium

Related reads

Related reads

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade