Refactoring series: Model Factory Helpers

Hot off the press. I was working on an application that has around ~20 models. It makes a huge use of Model Factories since they are so amazing!

For example, OrdersFactory looks like this (simplified):

<?php

$factory->define(App\Order::class, function (Faker\Generator $faker) {
    return [
        'date_from'   => Carbon::now()->subDays(5),
        'date_to'     => Carbon::now()->addDays(5),

        'note'        => $faker->sentence,

        'agent_id'    => function () {
            return App\Agent::first()->id ?? factory(App\Agent::class)->create()->id;
        },
        'customer_id' => function () {
            return App\Customer::first()->id ?? factory(App\Customer::class)->create()->id;
        },
    ];
});

Take a look at 'agent_id' and 'customer_id' bits. They are almost identical. It first tries to fetch an existing model. If it cannot find one, it uses factory() helper to create a new instance. While this solution is alright, in the app, there are 20-30 places where I had to write out this closure.

Idea

I like the idea of Programming by Wishful Thinking. So I tried to design an API I would like. Here is what I came up with:

<?php

// Instead of writing out the closure every time,
'customer_id' => function () {
    return App\Customer::first()->id ?? factory(App\Customer::class)->create()->id;
},

// I would love to write this:
'customer_id' => factory(App\Customer::class)->firstOrCreate(),

Solution

Since extending Illuminate\Database\Eloquent\FactoryBuilder is not an option, I realized I could make use of Laravel’s Macroable trait. However, this class did not import the trait at the time.

Out of luck? Not really.

I went ahead and submitted this PR. It was accepted the same day and I was just waiting for the next release.

Result

The implementation of this “feature” is super simple. In a service provider, I now register this macro:

<?php

public function register()
{
    FactoryBuilder::macro('firstOrCreate', function () {
        return function () {
            // $this refers to Illuminate\Database\Eloquent\FactoryBuilder instance.

            return $this->class::first()->id ?? factory($this->class)->create()->id;
        };
    });
}

And the new, nice and shiny OrdersFactory is born:

<?php

$factory->define(App\Order::class, function (Faker\Generator $faker) {
    return [
        'date_from'   => Carbon::now()->subDays(5),
        'date_to'     => Carbon::now()->addDays(5),

        'note'        => $faker->sentence,

        'agent_id'    => factory(App\Agent::class)->firstOrCreate(),
        'customer_id' => factory(App\Customer::class)->firstOrCreate(),
    ];
});

Outcome? Super small change. Super nice result.

Feel free to reach out to me on twitter.

© 2020 Jerguš Lejko