19
Model Factories in Laravel
Ep#28@Laracasts: Turbo Boost With Factories
This post is a part of the Week X of 100DaysOfCode Laravel Challenge series. We are building the Blog project while following the Laravel 8 from Scratch series on Laracasts with the 100DaysOfCode challenge.
Did you notice in the Seeders episode that there is a lot of repetitive code for generating test data? To add 10 records to our users table we define 10 arrays of those users. If this is real data like pre-defined users or user roles etc., then it is okay. But if it is just fake data for testing, then this repetition doesn't make much sense.
Model factories turbo-boosts the process by eliminating the need for passing multiple arrays for multiple records. You define an array of model attributes once in the factory class with fake value using a faker library. Then you generate the N
number of records by just passing N
as a parameter when calling the factory()
method. Besides that, you can create data for related models using factories that were not possible with the seeders.
Every model created with the Artisan command make:model
command by default uses the HasFactory
trait which provides access to the factory()
method. This method accepts a number as a parameter to generate that many records for the table.
Factories are stored in the database/factories
directory. This directory by default contains a file UserFactory.php
which defines the UserFactory
class. Every factory class has a method definition()
that returns a set of model attributes with default values as fake data. Laravel uses Faker PHP library to generate dummy data.
database/factories/UserFactory.php
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
class UserFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array
*/
public function definition()
{
return [
'name' => $this->faker->name(),
'email' => $this->faker->unique()->safeEmail(),
'email_verified_at' => now(),
'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
'remember_token' => Str::random(10),
];
}
/**
* Indicate that the model's email address should be unverified.
*
* @return \Illuminate\Database\Eloquent\Factories\Factory
*/
public function unverified()
{
return $this->state(function (array $attributes) {
return [
'email_verified_at' => null,
];
});
}
}
So, using the above factory class, can we generate some dummy users? Let's try
Boot up tinker php artisan tinker
>>> \App\Models\User::factory()->create();
=> App\Models\User {#4532
name: "Blanca Feeney",
email: "[email protected]",
email_verified_at: "2021-12-23 19:01:43",
#password: "$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi",
#remember_token: "Q2J7GmXoGH",
updated_at: "2021-12-23 19:01:43",
created_at: "2021-12-23 19:01:43",
id: 2,
}
>>>
Yes, that worked. Without a parameter, the factory()
method will create just one record. But if we supply a number to it, it will create that many records.
>>> \App\Models\User::factory(3)->create();
=> Illuminate\Database\Eloquent\Collection {#4550
all: [
App\Models\User {#4554
name: "Gino Beatty",
email: "[email protected]",
email_verified_at: "2021-12-23 19:03:55",
#password: "$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi",
#remember_token: "5Cnxmjmc9D",
updated_at: "2021-12-23 19:03:55",
created_at: "2021-12-23 19:03:55",
id: 3,
},
App\Models\User {#4555
name: "Piper Schmitt",
email: "[email protected]",
email_verified_at: "2021-12-23 19:03:55",
#password: "$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi",
#remember_token: "6zgiY9uPO6",
updated_at: "2021-12-23 19:03:55",
created_at: "2021-12-23 19:03:55",
id: 4,
},
App\Models\User {#4556
name: "Lance Skiles III",
email: "[email protected]",
email_verified_at: "2021-12-23 19:03:55",
#password: "$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi",
#remember_token: "IWqf0v15Gb",
updated_at: "2021-12-23 19:03:55",
created_at: "2021-12-23 19:03:55",
id: 5,
},
],
}
>>>
Great work, it created three records this time.
Now that we have played with Laravel's default UserFactory
, it's time to create our own brand new factory class using the Artisan command make:factory
.
php artisan make:factory PostFactory
And it is created in the database/factories
directory. One other way of creating a factory class is to create it with the Artisan command make:model
as an option --factory
or just -f
.
php artisan make:model Post -f
Let's work with our PostFactory
class. Before we modify it, see the PostFactory
class as it was generated with the Artisan command make:factory
.
<?php
namespace Database\Factories;
use App\Models\Category;
use Illuminate\Database\Eloquent\Factories\Factory;
class PostFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array
*/
public function definition()
{
//
}
}
So, there is nothing in the definition()
method yet. We need to return
an array of model attributes with fake values from this method. So, let's add it.
<?php
namespace Database\Factories;
use App\Models\Category;
use Illuminate\Database\Eloquent\Factories\Factory;
class PostFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array
*/
public function definition()
{
return [
"user_id" => \App\Models\User::factory(),
"category_id" => Category::factory(),
"title" => $this->faker->sentence(),
"slug" => $this->faker->slug(),
"excerpt" => $this->faker->sentence(),
"body" => $this->faker->sentence()
];
}
}
Notice we are referencing the Faker
library here. If we call this factory now in tinker \App\Models\Post::factory()->create()
it will throw an error about the Category
factory not found. Notice that here we are not only generating data for our Post
model but also for our related models User
and Category
too. It is the power of factories that Seeders don't have.
We already have the UserFactory
out of the box. So, let's create the CategoryFactory
in order for the above command to work.
php artisan make:factory CategoryFactory
After adding the attributes to the CategoryFactory
it looks like this.
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
class CategoryFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array
*/
public function definition()
{
return [
"title" => $this->faker->word(),
"slug" => $this->faker->word()
];
}
}
Boot up tinker php artisan tinker
and call the Post
model factory()
to create records for the Post
model as:
>>> \App\Models\Post::factory()->create()
=> App\Models\Post {#4591
user_id: 1,
category_id: 1,
title: "Qui tenetur architecto nihil omnis.",
slug: "hic-itaque-itaque-unde-ut-ratione-in-tempore",
excerpt: "Non unde voluptatem ipsam reprehenderit.",
body: "Impedit voluptates aut quae sit ut ut consequatur.",
updated_at: "2021-12-24 09:09:34",
created_at: "2021-12-24 09:09:34",
id: 1,
}
>>>
And yes, it worked this time. Notice that it also created the related data User
and Category
models and assigned their id
s to the foreign keys user_id
and category_id
for the Post
model.
The related models User
and Category
data crated with the Post
model above could be Eager Loaded or included with the Post
model as:
>>> \App\Models\Post::with("user", "category")->first()
=> App\Models\Post {#3630
id: 1,
user_id: 1,
category_id: 1,
title: "Dicta id maiores amet officia doloribus quae nesciunt.",
slug: "voluptas-voluptatem-voluptates-aut-et-quia-veritatis-molestias",
excerpt: "Dolores deleniti esse accusantium exercitationem dolorum.",
body: "Reprehenderit magnam sed architecto maxime quis inventore.",
published_at: null,
created_at: "2021-12-23 18:51:53",
updated_at: "2021-12-23 18:51:53",
user: App\Models\User {#4572
id: 1,
name: "John Doe",
email: "[email protected]",
email_verified_at: "2021-12-23 18:51:53",
#password: "$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi",
#remember_token: "rRCNWU09wU",
created_at: "2021-12-23 18:51:53",
updated_at: "2021-12-23 18:51:53",
},
category: App\Models\Category {#4577
id: 1,
title: "porro",
slug: "sint",
created_at: "2021-12-23 18:51:53",
updated_at: "2021-12-23 18:51:53",
},
}
>>>
Notice the User
and Category
models included in the Post
model. It is because we already have defined the relationships between these models.
The factory call \App\Models\User::factory()->create([])
will create a User
record with the fake attribute values as defined in the UserFactory
. But what if we want the name of the user to override the fake name with a pre-defined name like John Doe
? We can do that like so.
\App\Models\User::factory()->create([
'name' => "John Doe"
]);
Similarly, instead of creating a new user every time while a Post
record is created, we pass a specific user to the PostFactory
as:
$user = \App\Models\User::factory()->create([
'name' => "John Doe"
]);
Post::factory(5)->create([
"user_id" => $user->id
]);
Update Blog project. Replace the database/seeders/DatabaseSeeder.php
class run()
method content with the above code and run the following command.
php artisan migrate:fresh --seed
Now you should have 5
fresh records of the Post
model with 5
Category
and 1
User
records. As we passed a pre-defined User
to the Post
factory, so, instead of creating a new user for each post and assign it, the specified user will be assigned to each post.
So, think about it. A little work up front can save a lot of time in the future. We defined the above factories once for our app. Now for the lifetime of this app, we use these factories to quickly generate test data for our app.
19