Published: Jul 20, 2021 by C.S. Rhymes
Laravel 8 introduced new class based model factories and if you have an existing project you can use the legacy factories package to keep using the old factories. I have tended to keep the factories as they were and continue development, but after working on a fresh Laravel 8 project and using the new class based syntax I decided to go back and update the factories in the older Laravel apps. This article explains how I went about refactoring the factories to classes.
In this scenario we have a Laravel app that allows you to create a post. The post has a title, content, author and is either published or not published.
Table of Contents
Original Factory
Here is the PostFactory in the previous format. It defines the default state, with the publish value set to false. It has a state called published where the publish value is set to true.
// database/factories/PostFactory.php
<?php
/** @var IlluminateDatabaseEloquentFactory $factory */
use AppPost;
use FakerGenerator as Faker;
$factory->define(Post::class, function (Faker $faker) {
return [
'title' => $faker->words(3, true),
'content' => $faker->text,
'author_id' => factory(User::class),
'publish' => false,
];
});
$factory->state(Post::class, 'published', function (Faker $faker) {
return [
'publish' => true,
]
});
Here is a very basic example showing the factory being used in a test, using the factory() helper method. The second test in the example has the published state applied to it.
// tests/Unit/PostTest.php
<?php
namespace TestsUnit;
use AppPost;
use TestsTestCase;
class PostTest extends TestCase
{
public function test_post_is_not_published()
{
$post = factory(Post::class)->make();
$this->assertFalse($post->published);
}
public function test_published_post_is_published()
{
$post = factory(Post::class)->state('published')->make();
$this->assertTrue($post->published);
}
}
Refactoring the Factory
There are a few changes that are needed to refactor to the new factory class, so let’s go through them one by one.
First we can remove the @var
declaration and replace it with a namespace.
// Remove
/** @var IlluminateDatabaseEloquentFactory $factory */
// Add
namespace DatabaseFactories;
The new factories are classes so we need to define the class and make it extend the factory class.
Ensure you extend IlluminateDatabaseEloquentFactoriesFactory;
and not the old factory IlluminateDatabaseEloquentFactory
. This caught me out more than once.
To define that this factory is to be used with the Post model we need to add the protected model property, setting the value as the Post::class.
<?php
namespace DatabaseFactories;
use AppPost;
use FakerGenerator as Faker;
use IlluminateDatabaseEloquentFactoriesFactory;
class PostFactory extends Factory
{
protected $model = Post::class;
}
If you have an older Laravel app you may be using fzaninotto/Faker
in your composer.json file, which is now archived. Take this opportunity to update your composer.json to use fakerphp/faker
and run composer update
.
In the old factories we passed in Faker into the functions, but now faker is available using $this->faker
from the parent Factory class. This means we can also remove the following line.
// Remove
use FakerGenerator as Faker;
Factory definition
Now we are ready to provide our factory definition. This is done via a definition
method on the class.
We still return an array, like the previous factory did, but we need to update $faker
to $this->faker
.
We also need to update the relationship for the author_id so it no longer uses the factory()
helper, from factory(User::class)
to User::factory()
. This means that we will also have to update the UserFactory.php to use the new class based approach.
public function definition()
{
return [
'title' => $this->faker->words(3, true),
'content' => $this->faker->text,
'author_id' => User::factory()),
'publish' => false,
];
}
Factory State
Previously we defined our published state using the $factory->state()
syntax. Now we can create a new method on the class which returns $this->state()
.
public function published()
{
return $this->state(function (array $attributes) {
return [
'publish' => true,
]
});
}
Updating the Model
Before we can use the factory in our test, we need to update our Post model to tell it to use the HasFactory trait. This helps connect the model to the new factory class.
<?php
namespace AppPost;
use IlluminateDatabaseEloquentFactoriesHasFactory;
use IlluminateDatabaseEloquentModel;
class Post extends Model
{
use HasFactory;
}
Composer.json Changes
To use the new factories you need to add the namespace for the database factories to the autoload section of the composer.json.
"autoload": {
"psr-4": {
"App\: ""app/""