2
votes

I have simple question.

How could I write a Factory that let me defines relationships that use make() or create() depending on original call make() or create()?

It is my use case:

I have a simple factory

/** @var $factory Illuminate\Database\Eloquent\Factory */
$factory->define(App\User::class, function (Faker $faker) {
    return [
        'role_id' => factory(Role::class),
        'name' => $faker->name,
        'email' => $faker->unique()->safeEmail,
        'password' => 'secret',
    ];
});

My problem is with that role_id. When using factory(Role::class), it will create a Role always! Writing in Database...

In my tests, when I write factory(User::class)->create(); It will create an User and Role, thats ok!

But if I write factory(User::class)->make(); It will still create a Role... I wont write in Database a Role when using make(), in that case, I just want a simple role_id => 0.

How could I achieve that?

Thanks!

Edit (Workaround)

Well, it was harder than expected, there is no way to know when using make() or create() when your are defining nested relationships... The only way I found is using debug_stacktrace() method, and look for that make method called from Factory.

For simplification and reusability, I created a helper method for Factories called associateTo($class), this method will define relationships in Factory. It will return an existing ID from related model when using create() or 1 when using make().

By this way, Factories can be:

/** @var $factory Illuminate\Database\Eloquent\Factory */
$factory->define(App\User::class, function (Faker $faker) {
    return [
        'role_id' => associateTo(Role::class), // Defining relationship
        'name' => $faker->name,
        'email' => $faker->unique()->safeEmail,
        'password' => 'secret',
        'remember_token' => Str::random(10),
    ];
});

The helper function is:

/**
 * Helper that returns an ID of a specified Model (by class name).
 * If there are any Model, it will grab one randomly, if not, it will create a new one using it's Factory
 * When using make() it wont write in Database, instead, it will return a 1
 *
 * @param string $modelClass
 * @param array $attributes
 * @param bool $forceNew
 * @return int
 */
function associateTo(string $modelClass, array $attributes = [])
{
    $isMaking = collect(debug_backtrace())
        // A function called 'make()'
        ->filter(fn ($item) => isset($item['function']) && $item['function'] === 'make') 
        // file where I have global functions
        ->filter(fn ($item) => isset($item['file']) && Str::endsWith($item['file'], 'functions.php')) 
        ->count();

    if ($isMaking) return 1;

    /** @var Model $model */
    $model = resolve($modelClass);

    return optional($model::inRandomOrder()->first())->id
        ?? create($modelClass, $attributes)->id; // call to Factory
}

With this, I can write Unit test easily (using Factories) without the need of using Database connections, so Unit Tests becomes superfast!

I hope this help someone else!

2

2 Answers

0
votes

You can overwrite attribute role_id:

factory(User::class)->make(['role_id' => 0]);

Another solution - helper method for any attribute with _id ending:

function make($class)
{
    $attributes = \Schema::getColumnListing((new $class)->getTable());
    $exclude_attribures = [];

    foreach ($attributes as $attribute)
    {
        if(ends_with($attribute, '_id'))
        {
            $exclude_attribures[] = [$attribute => 0];
        }
    }

    return factory($class)->make($exclude_attribures);
}

Call example:

make(User:class);
0
votes

You can take advantage of states


/** @var $factory Illuminate\Database\Eloquent\Factory */
$factory->define(App\User::class, function (Faker $faker) {
    return [
        'role_id' => factory(Role::class),
        'name' => $faker->name,
        'email' => $faker->unique()->safeEmail,
        'password' => 'secret',
    ];
});

$factory->state(App\User::class, 'withoutRelationship', [
    'role_id' => null,
    'another_id' => null,
]);

Then

factory(User::class)->state('withoutRelationship')->make();