I use Laravel model factories quite extensively. Here's an older related post, in case you are interested. I tend to create a lot of methods inside them to simplify tests, utilizing Factory States. The current docs show the following example for using Factory States:
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
use App\Models\User;
class UserFactory extends Factory {
protected $model = User::class;
public function suspended(): static
{
return $this->state(function (array $attributes) {
return [
'account_status' => 'suspended',
];
});
}
}
Now enable factories using HasFactory
trait on the User
model:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class User extends Model {
use HasFactory;
// ...
}
Combination of the above makes the following possible:
<?php
$user = \App\Models\User::factory()->suspended()->create();
Pretty useful and pretty self-explanatory. For the completeness, the above
creates a suspended user account. Now take a look at the factory()
method:
<?php
namespace Illuminate\Database\Eloquent\Factories;
trait HasFactory
{
/**
* Get a new factory instance for the model.
*
* @param callable|array|int|null $count
* @param callable|array $state
* @return \Illuminate\Database\Eloquent\Factories\Factory<static>
*/
public static function factory($count = null, $state = [])
{
$factory = static::newFactory() ?: Factory::factoryForModel(get_called_class());
return $factory
->count(is_numeric($count) ? $count : null)
->state(is_callable($count) || is_array($count) ? $count : $state);
}
// ...
}
The problematic bit is the @return
statement which specifically says that
the parent class of Factory
is returned, instead of our UserFactory
,
which contains the suspended()
method. Trying to get IDE hinting for any
such custom methods (states) simply does not work this way, because we need
to somehow tell the language server that calling User::factory()
really
returns UserFactory
instead of just Factory
. One of the ways to do just
that is doing so explicitly:
<?php
/** @var \Database\Factories\UserFactory $userFactory */
$userFactory = User::factory();
$userFactory->suspended()->create(); // now the autocompletion works
Works but it is quite ugly, eh. One downside is that this cannot be chained easily as one might be used to, because the variable has to be on it's own line:
<?php
/** @var \Database\Factories\UserFactory $userFactory */
$userFactory = User::factory()->suspended()->create(); // autocompletion wont work
Another, much worse downside is that we have to do this everywhere we want to have the autocomplete and it is simply not worth. If there just was and easy way to fix this... Wait, there is one:
<?php
namespace App\Models;
use Database\Factories\UserFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class User extends Model {
use HasFactory {
factory as traitFactory;
}
/**
* @param callable|array|int|null $count
* @param callable|array $state
* @return UserFactory
*/
public static function factory($count = null, $state = []) {
return static::traitFactory($count, $state);
}
// ...
}
Nice and easy! But how it works? There are two factors in play now. First,
we override the factory()
method received in the User
model from
HasFactory
trait and typehint the new return type to
@return UserFactory
. But since we want to call the original trait
factory()
method inside it, we need to use php trait
conflict resolution
and as
operator to rename the method locally as traitFactory()
like
this:
<?php
// ...
use HasFactory {
factory as traitFactory;
}
Enjoy!