DEV Community

Daniel Werner
Daniel Werner

Posted on • Originally published at danielwerner.dev on

Under the hood: How states work in Laravel factories

Since Laravel 8 we can use the new and reworked model factories. They allow us to create multiple interconnected model instances using a simple and easy to read syntax. As usual Laravel has a great documentation about how to create and write these factories. In this article I don’t describe the usage of the factories, if you are interested in that check out the documentation. Instead we will go a little deeper and understand how the states work in factories. Also we will show an interesting example we run into lately, where the states worked differently as we expected.

What are the states?

From the documentation: “State manipulation methods allow you to define discrete modifications that can be applied to your model factories in any combination.” Basically it allows us to define for example a status of the model you create, or basically predefine any of the model attributes.

According to the documentation you should pass a closure to the state method, however you can also pass a simple array, because it will wrap it into a closure:

public function state($state)
{
    return $this->newInstance([
        'states' => $this->states->concat([
            is_callable($state) ? $state : function () use ($state) {
                return $state;
            },
        ]),
    ]);
}
Enter fullscreen mode Exit fullscreen mode

If you define a sequence it basically also add a state on the factory:

public function sequence(...$sequence)
{
    return $this->state(new Sequence(...$sequence));
}
Enter fullscreen mode Exit fullscreen mode

When the factory creates a new model instance it will call the state callback, to apply the changes to the model attributes:

protected function getRawAttributes(?Model $parent)
{
    return $this->states->pipe(function ($states) {
        return $this->for->isEmpty() ? $states : new Collection(array_merge([function () {
            return $this->parentResolvers();
        }], $states->all()));
    })->reduce(function ($carry, $state) use ($parent) {
        if ($state instanceof Closure) {
            $state = $state->bindTo($this);
        }

        return array_merge($carry, **$state($carry, $parent)**);
    }, $this->definition());
}
Enter fullscreen mode Exit fullscreen mode

What has tricked us?

As I mentioned earlier is is also possible to pass a simple array to the state, and it works fine with static values, like:

->state(['status' => 'draft'])
Enter fullscreen mode Exit fullscreen mode

In our case we wanted to create multiple instances of a model, and pass a state which is generated by faker with some special rules like this:

->count(3)->state(['value' => $this->faker->numberBetween(0, 10)])
Enter fullscreen mode Exit fullscreen mode

This is obviously a simplified example in the real case we did multiple instances with the same faker with different conditions. And here comes the trick. It generated all the instances with the same value. Why this has happened? As we saw earlier when you pass a non callable argument to the state it will wrap it into a closure. And basically the faker will be called only once, when you pass the array, and the generated closure for the state will look like this:

function () {
    return ['value' => 4];
}
Enter fullscreen mode Exit fullscreen mode

And this state closure will be executed over and over again so it will return the same value for all models. But when we realised this, and passed a closure our closure looked like this:

function () {
    return ['value' => $this->faker->numberBetween(0, 10)];
}
Enter fullscreen mode Exit fullscreen mode

and in this case the faker is executed for all generated models separately, and generated different random values as expected.

What we could have done differently?

In this case we could use sequences. Those are basically intended for these purposes.

->state(new Sequence(
    fn ($sequence) => ['value' => $this->faker->numberBetween(0, 10)],
))
Enter fullscreen mode Exit fullscreen mode

I wanted to show one interesting thing for the end of this article. As we already saw the sequence instance is passed to the state. The sequence is not wrapped into a closure because it has an __invoke() method and the php built in is_callable($state) method will return true for those invokable classes.

Hope this article helps you understanding the mechanics behind the factory states and to avoid the same mistake we did.

The post Under the hood: How states work in Laravel factories appeared first on Daniel Werner.

Discussion (0)