DEV Community

Victor Lima
Victor Lima

Posted on

Extending PHP Faker Library to define custom data structures using Laravel 11

Introduction

If you work with Laravel at a daily basis (specially if you apply TDD), you might have figured out that during the 'test writing phase', sometimes we face situations that requires us to write custom array structures to fill our models, or some kind of data that cannot be easily faked and had to be written manually inside the Factory class.

Recently I've dealt with EditorJS and had to save the JSON data it provides in my database, so I ended up having to fake some of the data in order to easily make some assertions during testing.

For the following examples, I have a users table with a JSON column called custom_data.

The Problem

Many of us end up doing the obvious approach:

$user = User::factory()
    ->create([
        'custom_data' => [
        'id'   => uniqid(),
        'data' => [
            'first_index'  => 'first_value',
            'second_index' => 'second_value',
            'third_index'  => 'third_value',
        ],
        'type' => 'very_complex',
    ],
]);
Enter fullscreen mode Exit fullscreen mode

The output of $user->custom_data of course is:

array:3 [
  "id" => "666251e06b701"
  "data" => array:3 [
    "first_index" => "first_value"
    "second_index" => "second_value"
    "third_index" => "third_value"
  ]
  "type" => "very_complex"
] // app/Console/Commands/Playground.php:29
Enter fullscreen mode Exit fullscreen mode

Or maybe some of us will take a more elegant, but yet sometimes problematic, solution:

$user = User::factory()
    ->withCustomData()
    ->create();
Enter fullscreen mode Exit fullscreen mode

In UserFactory.php:

public function withCustomData(): self
{
    return $this->state([
       'custom_data' => [
           'id'   => uniqid(),
           'data' => [
               'first_index'  => 'first_value',
               'second_index' => 'second_value',
               'third_index'  => 'third_value',
           ],
           'type' => 'very_complex',
       ],
    ]);
}
Enter fullscreen mode Exit fullscreen mode

And as you may expect, the output is the same as above.

At first glance these examples may seem good enough - at the end of the day, they work - but when we consider that this array can be different sometimes, it can quickly become a cumbersome to maintain. For instance, what if the "type" index has a different value and your application behavior depends on that? Will you pass a different parameter every time? Or even create another factory state, thus making the code larger and larger?

I mean, there is no problem to define factory states, but the thing here is to "separate responsibilities" and make our code smaller and more readable.

The issue with this approach is that the UserFactory has the responsibility of taking care of which types of data (and all data variations) it actually supports, when in reality the users table doesn't care about it. The JSON column accepts anything that is valid, regardless of what it is.

The Solution

This said, we can agree that nothing better than having a place to generate these exact data variations and complex things with a few lines of code.

For this, we are going to extend the Faker library by creating a custom provider, so we will be able to call:

$this->faker->complexArray() or fake()->complexArray()

And that will get the job done.

In order to accomplish that, I have written a class inside Tests/Faker/Providers namespace, named CustomComplexArrayProvider.php

The aim here is to shelter all that logic regarding this specific array and all things that may differ inside it.

In tests/Faker/Providers:

<?php

namespace Tests\Faker\Providers;

use Faker\Provider\Base;

class CustomComplexArrayProvider extends Base
{
    public function complexArray(): array
    {
        return [
            'id'   => uniqid(),
            'data' => [
                'first_index'  => 'first_value',
                'second_index' => 'second_value',
                'third_index'  => 'third_value',
            ],
            'type' => 'complex',
        ];
    }
}
Enter fullscreen mode Exit fullscreen mode

As you can see, we have the complexArray method definition and it returns a multidimensional array with 3 indexes. Of course this is a very simple and straightforward example. You can receive parameters inside this method and make it suit your testing needs.

You can even create a method for each type of data, just to make the code more readable:

In CustomComplexArrayProvider.php:

public function notTooComplexArray(): array
{
    return [
        'id'   => uniqid(),
        'data' => [
            'first_index' => 'first_value',
        ],
        'type' => 'not_too_complex',
    ];
}
Enter fullscreen mode Exit fullscreen mode

Okay, fair enough, but how do we use this?

To access these guys and use them within your tests, we must tell Laravel to add this data Provider in the Faker/Generator class.

For that, I've written a Service Provider called TestServiceProvider.

You can generate Provider classes with php artisan make:provider

<?php

namespace App\Providers;

use Faker\Factory;
use Faker\Generator;
use Illuminate\Support\ServiceProvider;
use Tests\Faker\Providers\CustomComplexArrayProvider;

class TestServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        if (!$this->app->environment(['local', 'testing'])) {
            return;
        }

        $this->app->singleton(
            abstract: Generator::class,
            concrete: function (): Generator {
                $factory = Factory::create();

                $factory->addProvider(new CustomComplexArrayProvider($factory));

                return $factory;
            }
        );

        $this->app->bind(
            abstract: Generator::class . ':' . config('app.faker_locale'),
            concrete: Generator::class
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

Then we have to register the Provider in our application, in bootstrap/providers.php:

return [
    App\Providers\AppServiceProvider::class,
    App\Providers\TestServiceProvider::class,
];
Enter fullscreen mode Exit fullscreen mode

As you can see, we registered a binding in the Service Container telling Laravel that it must add our custom class as a Faker provider, so now Faker is aware of the methods we've just created.

And of course, this is intended to be applied only in local/testing environments, so there's a check at the beginning of the code just to avoid this code to run in the production environment.

The singleton call aims to allow us to use our custom class when calling them from a Faker/Generator object, and the bind does the same thing, but when calling from the fake() helper.

This way, the result is:

dd(fake()->complexArray());
Enter fullscreen mode Exit fullscreen mode

Output:

array:3 [
  "id" => "6662549ca6be9"
  "data" => array:3 [
    "first_index" => "first_value"
    "second_index" => "second_value"
    "third_index" => "third_value"
  ]
  "type" => "complex"
] // app/Console/Commands/Playground.php:37
Enter fullscreen mode Exit fullscreen mode

Now you can use this method inside your factories, or even combine them with factory states. That's up to you now.

I hope this article helps you to make your code even easier to understand in 6 months from now 🙌

Thanks for reading!

Top comments (0)