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 ids 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.

Overriding attribute values

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