Nested States in Laravel 8 Database Factories
Tags: laravel, php, testing
Published: 2020-12-28 10:12:38

---
title: Nested States in Laravel 8 Database Factories
published: true
date: 2020-09-17 21:00:00 UTC
tags: laravel, php, testing
canonical_url: https://stefanzweifel.dev/posts/2020/09/17/nested-states-in-laravel-8-database-factories
---

While upgrading the new [screeenly.com](https://3.screeenly.com) to Laravel 8, I hit a bit of a wall. At first, it wasn't clear to me how I can get my previous setup of database factories to work with the new and improved Factory Classes.

See, in screeenly I'm using [laravel/cashier-paddle](https://laravel.com/docs/master/cashier-paddle). Cashier comes with 3 models: `Customer`, `Subscription` and `Receipt`. In my Laravel 7 testsuite, I've created factories for all of them with different states. `subscribed`, `onTrial`, `trialExpired`.

To make writing test easier, I've put those states on the `User` Factory. Here's how the code look liked for the `subscribed` state.

```php
$factory->afterCreating(User::class, function ($user, $faker) {
    $user->customer()->create();
});

$factory->afterCreatingState(User::class, 'subscribed', function ($user) {
    $customer = $user->customer()->update([
        'trial_ends_at' => null,
    ]);

    factory(Subscription::class)->states(['status_active'])->create([
        'billable_id' => $user->id,
        'paddle_plan' => app(TestPlan::class)->paddleId()
    ]);

    factory(Receipt::class)->create([
        'billable_id' => $user->id,
    ]);
});

```

When a `User` is created, a new "empty" `Customer` model is attached to it. When the `subscribed` state has been defined on the call to the `User`-factory, the `trial_ends_at` value is reset to `null`, a new `Subscription` is created and attached to the user. We also create a new `Receipt`.

In my tests I could create this whole structure by using the following line.

```php
$user = factory(User::class)->states(['subscribed'])->create();

```

I don't have to remember to create a `Customer` or a `Subscription`.  
Super convenient. 👌

## Make it work for Laravel 8[#](#make-it-work-for-laravel-8 "Permalink")

I've used [Laravel Shift](https://laravelshift.com) to automate the upgrade to Laravel 8. It migrated my existing factories to the new class versions, but couldn't convert my exessive use of `afterCreatingState` to the new format.

Getting the above code to work in Laravel 8 took me a couple of nights, as it wasn't clear to me from the docs, that I could put **anything** in the new [states](https://laravel.com/docs/master/database-testing#factory-states)-methods. (Now, it's so obious to me)

In those methods I can combine `$this->state()` with `$this->afterCreating()`. 🤯

Here's how my `User`-, `Customer`- and `Subscription`-Factories now look like to create the `subscribed`-state:

```php
namespace Database\Factories;

use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;

class UserFactory extends Factory
{
    protected $model = User::class;

    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),
            'finished_onboarding_at' => null,
        ];
    }

    public function subscribed()
    {
        return $this
            ->afterCreating(function (User $user) {
                CustomerFactory::new()->subscribed()->create([
                    'billable_id' => $user->id,
                ]);
            });
    }
}

```

The `subscribed`-method doesn't update the `User` itself with `$this->state`, but calls a `$this->afterCreating` callback where we create a new subscribed `Customer`.

```php
namespace Database\Factories;

use App\Domain\PaddleSubscription\Plans\TestPlan;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
use Laravel\Paddle\Customer;

class CustomerFactory extends Factory
{
    protected $model = Customer::class;

    public function definition()
    {
        return [
            'billable_id' => User::factory(),
            'billable_type' => User::class,
            'trial_ends_at' => now()
        ];
    }

    public function subscribed()
    {
        return $this
            ->state(fn () => ['trial_ends_at' => null])
            ->afterCreating(function (Customer $customer) {
                SubscriptionFactory::new()->statusActive()->create([
                    'billable_id' => $customer->id,
                    'paddle_plan' => app(TestPlan::class)->paddleId()
                ]);
            });
    }
}

```

The `CustomerFactory` is similar to the `UserFactory`. But instead of just using a `afterCreating`-callback again, we combine it with `$this->state` and reset the `trial_ends_at` timestamp.

```php
namespace Database\Factories;

use App\Domain\PaddleSubscription\Plans\TestPlan;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
use Laravel\Paddle\Subscription;

class SubscriptionFactory extends Factory
{
    protected $model = Subscription::class;

    public function definition()
    {
        return [
            'billable_id' => User::factory(),
            'billable_type' => User::class,
            'name' => 'default',
            'paddle_id' => $this->faker->unique()->randomNumber,
            'paddle_status' => Subscription::STATUS_ACTIVE,
            'paddle_plan' => app(TestPlan::class)->paddleId(),
            'quantity' => 1,
            'trial_ends_at' => null,
            'paused_from' => null,
            'ends_at' => null,
        ];
    }

    public function statusActive()
    {
        return $this->state(function () {
            return [
                'paddle_status' => Subscription::STATUS_ACTIVE
            ];
        });
    }
}

```

To finish things of, the `Subscription`-factory creates a new active subscription.

The tests itself have been updated too. To create 5 new subscribed `User` I can use this one-liner.

```php
$users = User::factory()->times(5)->subscribed()->create();

```

Much easier to write and **read** than.

```php
$users = User::factory()
    ->times(5)
    ->has(
        CustomerFactory::new()
            ->has(SubscriptionFactory::new()->statusActive())
    )
    ->create();

```

## Conclusion[#](#conclusion "Permalink")

Maybe you think I write my tests in a wrong way. It's not "professional" enough. Or I hide too much information in the factory and that I should create the `Customer` and the `Subscription` in every – single – test.

In the example of `User`, `Customer` and `Subscription` I think it's acceptable to hide the relationship in those `state`-methods. It's central to how the app works and the majority of the tests rely on that constellation. I don't want to repeat myself in every tests. Maintainable tests are as important as maintainable application code.

I hope this short posts helped you and makes your tests a bit more readable. When I find the time, I will contribute an example to the official docs.