Factor - Short and Expressive Way to Use Laravel's Factories
If you are writing tests in Laravel you have surely used the model factories to make, and perhaps persist, models for your tests. Using the built-in factory()
method to obtain a Factory
instance can get cumbersome to type every single time we need a model. Let's see how we can make it slightly more expressive and shorter.
Alias
As you have probably seen others do, they will create a helpers.php
file and will add 2 methods there:
// helpers.php
function make($model, $overrides = [])
{
return factory($model)->make($overrides);
}
function create($model, $overrides = [])
{
return factory($model)->create($overrides);
}
While the above will work just perfectly, it has its limitation in terms of expressivity. Say we want to allow the creation of multiple instances in one fell-swoop, you may simply add another argument to the methods:
// helpers.php
function make($model, $overrides = [], $times = 1)
{
return factory($model, $times)->make($overrides);
}
function create($model, $overrides = [], $times = 1)
{
return factory($model, $times)->create($overrides);
}
You may think that everything works like before, but in reality, some of your tests will break. How do I know that? Keep reading to find out.
When we pass the second argument to the factory()
method that is an integer
, the factory()
method will correctly assume it is the number of models we want back and will call the times()
methods on the Factory
.
When the Factory
attempts to make/create a model, it will check to see if the internal $amount
property is different than null
. If it is, it will create a Collection
of object even if we only wanted a single model. So by calling factory(User::class, 1)->create()
you may expect to get a user model back, but you actually get a collection with one user model in it.
The easiest solution for this problem is to add a small conditional:
function make($model, $overrides = [], $times = 1)
{
return factory($model, $times > 1 ? $times : null)->make($overrides);
}
Great! Now everything works just as expected!
States
One of the best features that the factories provide, is the ability to define different states for each model factory. Trying to add that to our current shortcut method implementation is kind of a shame. Let's review the method's signature to see why:
function make($model, $overrides = [], $times = 1)
If we add a $state
parameter right after the $model
parameter, we lose the flexibility to easily whip-up a model with few overrides for any case where we don't need any special case. If we add the $state
parameter after the $overrides
we lose the flexibility of being able to easily and expressively declare what state we want for that model. The same considerations are valid for the adding the $state
parameter after the $times
parameter. So what can we do?
Factor
That's exactly where Factor comes into play. By using Factor, we can expressively chain the states()
after our make()
or create()
methods call and it will just work! Given we have the following model factory definition:
$factory->define(User::class, function (Faker $faker) {
return [
'name' => $faker->name,
'email' => $faker->email,
'password' => bcrypt('secret'),
]
});
$factory->state(User::class, 'admin', function (Faker $faker) {
return [
'name' => 'Admin',
'email' => '[email protected]',
]
});
By using Factor, we can expressively use the factory:
$user = make(User::class);
echo $user->name;
// -> 'Lucy Cechtelar'
echo $user->getKeyName();
// -> 'id'
$admin = make(User::class)->states('admin');
echo $admin->name;
// -> Admin
echo $admin->getKeyName();
// -> 'id'
Doesn't seem like much but think about it! we were able to chain the states()
method our shortcut factory method and use the returned result as if it was the model itself. Kinda neat, right?
I Want to Decide!
It may sometimes feel "right" to pass the number of objects we want to build as the second argument instead of the overrides. Factor lets you do that. If you pass an integer as the second argument, it will assume you are trying to set the number of objects to create:
make(User::class, 3);
// Equivalent to:
make(User::class, [], 3);
make(User::class, 3, ['name' => 'John']);
// Equivalent to:
make(User::class, ['name' => 'John'], 3);
A Peek Behind the Curtain
Behind the scenes, Factor simply uses a "proxy object" (called PendingModel
) that will differ any method call or attribute access to the underlying model instance or collection (remember the $times
parameter?). The "proxy object" determines in real-time when it should actually build the models and will proxy everything to them.
small Caveat
Sometimes we want to create models just as a part of the test's setup but we never really need to directly interact with them. As we mentioned before, Factor will not immediately create the models but will decide in real-time when they need to be created. This behavior will break your tests if you rely on the models to exist in the database only. To overcome this shortcoming, Factor also provides a now()
method which will let you use it's elegant and expressive syntax but tell to immediately create the models:
create(User::class);
// -> 'Kfirba\Factor\PendingModel'
create(User::class)->now();
// -> 'App\User'
create(User::class)->states('admin');
// -> 'Kfirba\Factor\PendingModel'
create(User::class)->states('admin')->now();
// -> 'App\User'
I hope you find this package useful! Happy coding 🙂!