Defining Model Factories and Model Factory States

Laravel 5.1 came with Model Factories which allow you to generate models with "fake" data using the Faker library. This quickly became a powerful and essential feature
which greatly assists in Test Driven Development and Database Seeding.

Model Factory States were introduced in Laravel 5.3 and it allow us to utilize model factories in a more efficient manner. Factory States allow us to define subtle modifications that can be
applied to our model factories. For instance, perhaps by default we create Ticket with sample data that have all of the necessary non-nullable attributes.
However we then also want to distinguish tickets by availability but not all tickets will necessarily need to have an availability state. We can address this concern using Factory States.

With the definitions covered, we will look at using Factory States in a simple demo application below.

Creating our Demo Application

Let's assume we are creating a small ticket manager which will allow us to manage tickets for concerts called LaraTicket.

On your terminal, Create a new laravel application called LaraTicket:

laravel new LaraTicket

Once the application is installed. Create your .env file and fill out your database details if you don't want to use Homestead.

cp .env.example .env

Let's begin with our Ticket Model.

Creating our Models

php artisan make:model Tickets -m

We pass the -m flag to create a migration associated with our Model. (Note you must be in the LaraTicket directory to use Artisan)

If you open the app/ folder you will now see Tickets.php and a migration in /database/migrations called DateTime_create_tickets_table where DateTime is the current Date and Time of when you ran the artisan make model command.

Creating Migrations

Open the create_tickets_table migration and edit the file to contain the following:

{% highlight php %}

increments('id'); $table->string('title'); $table->integer('price'); $table->string('venue'); $table->string('venue_address'); $table->boolean('availability')->nullable(); $table->timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('tickets'); } } {% endhighlight %} Let's break down what's happening here... the ``up`` function is used to add the necessary database fields that we will need when storing Ticket information. Note we have added the nullable field for availability as in our scenario, not all tickets will immediately have an availability field. The ``down`` function is simply used to drop the table if it exists when we are rolling back our migrations or refreshing them. Once we have created our migrations, let's run our migrations: ``` php artisan migrate ``` Now if you go and look at your database, you should see the tickets table with the fields that we defined. Screen Shot 2017 10 06 at 9 31 46 PM ## Creating our Database Seeds Let's now create our database seeds so that we have sample data to work with. Database Seeds are a way of inserting dummy data into the database. Let's start by creating our Database Seeder for the Tickets Table. ``` php artisan make:seeder TicketsTableSeeder ``` You should now see a new class in ``/database/seeds`` called TicketsTableSeeder.php, before we use our seeder, let's quickly configure our DatabaseSeeder class to run the seeder. Open ``/database/seeds/DatabaseSeeder`` and let's adjust it to the following: {% highlight php %} call(TicketsTableSeeder::class); Model::reguard(); } } {% endhighlight %} This is quite a simple class. We simply use Model::unguard() which is a static function that disables the protection of mass assignment on Models. This allows us to seed all of our model properties as required. We then call all our seeders which in this case is just the one TicketsTableSeeder class and then we use Model::reguard() to enable mass assignment protection again. Mass assignment protection is utilized to prevent your app from various security risks such as when a user passes an unexpected HTTP parameter through a request and that parameter then changes a column in your database. Traditionally you have to use the fillable or guarded attributes on your model to protect against this however to make database seeding easier and more efficent, we use unguard to disable this checking as we know that the dummmy data is secure. Before we create our TicketsTableSeeder. Let's create our first model factory to generate the seed data. ## Creating Model Factories Let's begin by opening ``/database/factories/ModelFactory.php``. You should see a default ModelFactory for the User, you can leave this for now and let's create our own! {% highlight php %} define(App\Tickets::class, function (Faker $faker) { return [ 'title' => $faker->title, 'price' => 500, 'venue' => 'Awesome Venue', 'venue_address' => '82 Awesome Street, Cape Town' ]; }); {% endhighlight %} What we are accomplishing in the above ode is to use the Faker library to generate a fake title and then we use default dummy data for the price, venue and venue_address attributes. Note that we didn't define the availability field as it is nullable and we will deal with this later... ## Seeding data using the Model Factories Now that we have our Model Factory for the Tickets class defined, we can use it in our TicketsTableSeeder class. Open the TicketsTableSeeder class in ``/database/seeders/`` and let's create some sample data for our database! {% highlight php %} create(); } } } {% endhighlight %} The run function will simply call our Model Factory class for Tickets five times and create five dummy models and then persist them to the Tickets table. We can now use our seeder by using the seed artisan command: ``` php artisan db:seed ``` This command will call the DatabaseSeeder class that we adjusted to call our TicketsTableSeeder. Upon running the command in your terminal, you should see the following output: ``` Seeding: TicketsTableSeeder ``` If you then select all the records from the tickets table in your database. You should see the following output: Screen Shot 2017 10 06 at 10 23 04 PM ## Creating our Tests Now that we have our Model Factory setup, we have a way to generate fake data for testing purposes. Let's add a small test to test when a Ticket is created in the database using our Model Factory. Firstly generate the test using: ``` php artisan make:test TicketsTest ``` You should now see a Feature test in ``tests/Feature`` called TicketsTest.php. Let's create a simple test to test that our model factory persists our model to the database. Do note that we are using PHPUnit so ensure you have this setup and configured as per you wish however it comes with Laravel by default and we will be using the default configuration in the ``phpunit.xml`` config. Edit the TicketsTest.php file as follows: {% highlight php %} create([ 'title' => 'My Amazing Ticket' ]); $this->assertDatabaseHas('tickets', ['title' => 'My Amazing Ticket']); } } {% endhighlight %} There are a number of things happening above so let's break them down:
  1. We are using the DatabaseMigrations trait to ensure that our database tables are created on each test. This is because we are testing database functionality. You would omit this in a Feature or Unit test if it's not needed as it can be a performance hit.
  2. We are calling our Tickets Factory in order to create a model and persist it to the database however note that we are passing in an array which allows you to set properties that were defined in your Model Factory so in this case we are setting the title so that we know for sure what our title is. This is important to assert that the model was persisted correctly.
  3. We are using the assertDatabaseHas() function to check that the tickets table has a record with a title of ``"My Amazing Concert"``.
There is obviously a lot more testing that can be done however this is sufficient to ensure that our Model Factory for Tickets is working correctly. To run the tests, in your terminal type: ``` /vendor/bin/phpunit ``` Or setup your Testing suite (PHPUnit) if you are using PHPStorm. Once run, you should see the following output: Screen Shot 2017 10 06 at 10 36 16 PM ## Working with Model Factory States Now that we have our Model Factory, Database Seeders and tests setup, we are good to go right! Wrong. In this case, we have missed out the availabilty attribute which we defined earlier. Remember that the purpose of the availability attribute was to check if certain Tickets were available or not. This then creates a split in our Model Factories as not all models will need the availability attribute populated and hence it is optional. So how do we solve this problem? Well this is where the awesomeness of Model Factory States comes in. Go to your ModelFactory.php class and let's define a state for the availability field for when tickets are available: {% highlight php %} state(App\Tickets::class, 'available', function (Faker $faker) { return [ 'availability' => true ]; }); {% endhighlight %} So what we have accomplished above is defining a "available" state for our Tickets Model Factory. If we create a model and use this state (as described shortly) then we will still use the dummy data that we defined for the Ticket Model Factory however by using a Model Factory State, we will also append the 'availability' field and set it to true. Let's create another test in our TicketsTest class to see how this works: {% highlight php %} states('available')->create([ 'title' => 'My Available Ticket' ]); $this->assertDatabaseHas('tickets', [ 'title' => 'My Available Ticket', 'availability' => true, ]); } {% endhighlight %} In this test, we do the same as above using the created method however we call the states function and pass in a value of 'available' which in turn calls the available state that we just defined in our ModelFactory.php class. This is useful as now we get control over the optional dummy data that we need. If we then run our tests, you should see the following output: Screen Shot 2017 10 06 at 10 48 52 PM ## Wrapping Up You have now created a simple demo app for handling the very basics of Tickets and in turn utilized Model Factories and Model Factory States for your tests and database seeding. Let me know if you have any questions or feedback in the comments below.