deligoez's avatar

Laravel 8 Factories: Access overriden attributes from definition()

How we can directly access overriden attributes (array $attributes) in a Laravel 8 factory definition() method?

Let's say for this given L7 factory:

$factory->define(Product::class, function (Faker $faker, $attributes) {
    $taxAmount = $attributes['taxAmount'] ?? $faker->randomElement([0, 1, 8, 18]);
    $exemption_reason_code = $attributes['exemption_reason_code'] ?? ($taxAmount === 0 ? $this->faker->randomElement([0, 325, 326]) : null);

    return [
        'name'                  => ucwords($faker->words(2, true).$faker->lexify('???')),
        'taxAmount'             => $taxAmount,
        'exemption_reason_code' => $exemption_reason_code,
    ];
});

If we call the factory like this:

factory(Product::class)->create(['taxAmount' => 0])

In the factory definition, we can easily check if there is an overridden taxAmount attribute or not.

How we can access attributes array in a L8 factory?

Sample L8 factory:

    public function definition() : array
    {
        $taxAmount = $attributes['taxAmount'] ?? $this->faker->randomElement([0, 1, 8, 18]);
        $exemption_reason_code = $attributes['exemption_reason_code'] ?? ($taxAmount === 0 ? $this->faker->randomElement([0, 325, 326]) : null);

        return [
            'name'                  => ucwords($this->faker->words(2, true).$this->faker->lexify('???')),
            'taxAmount'             => $taxAmount,
            'exemption_reason_code' => $exemption_reason_code,
        ];
    }
0 likes
6 replies
deligoez's avatar

Because if an attribute is overridden by make() or create(), I don't want to generate a random value for this attribute and want to take the overridden value.

And sometimes I do generate another attribute based on that overridden value.

If you look at the example factory: I want to take the overridden value (if given) of the taxAmount attribute so I can use it on the $exemption_reason_code.

How do I access an attribute value from current state? I can easily write to state but can not read. Everything is saved as a Closure on Illuminate\Database\Eloquent\Factories\Factory: $states variable.

1 like
bobbybouwmann's avatar

In the state method you get access to all the current generated attributes, from there you should be able to generate something based on precious set data.

squatto's avatar

I just dealt with this same thing and wanted to share what I ended up doing.

My old Laravel 7 factory:

$factory->define(Forgiveness::class, function (Faker $faker, $attributes = []) {
    $application = $attributes['application'];

    // use $application to determine factory data
    // ...

    return [
        'application_id' => $application->id,
        'field1'         => 'value1',
        'field2'         => 'value2',
        // ...
    ];
}

How I called the Laravel 7 factory:

factory(Forgiveness::class)
    ->make([
        'application' => $application,
    ]);

My new Laravel 8 factory (cleaned up and commented to make it useful to you...):

namespace Database\Factories;

use App\Models\Application;
use App\Models\Forgiveness;
use Illuminate\Database\Eloquent\Factories\Factory;

class ForgivenessFactory extends Factory
{
    protected $model = Forgiveness::class;

    public function application(Application $application): ForgivenessFactory
    {
        // use $application to determine factory data
        // ...

        // build the data array to be merged into the current definition
        // (which is empty in definition(), but you can define values there too of course)
        // doing it in a temp $data variable instead of within the state() callback
        // makes it so that you don't have to add
        // "use ($application, $otherVariable, ...)"
        // to the state() callback to get the variables into the definition array
        $data = [
            'application_id' => $application->id,
            'field1'         => 'value1',
            'field2'         => 'value2',
            // ...
        ];

        return $this->state(fn ($attributes) => $data);
    }

    public function definition(): array
    {
        return [];
    }
}

How I call the Laravel 8 factory:

Forgiveness::factory()
           ->application($application)
           ->make();

It basically delegates the actual creation of the model definition to a state method so that I can provide input to the factory. There may be a better way (and probably is!) but this worked great and was simple to put together/use.

1 like
JayD's avatar

Sorry for bumping up this old thread, but is there any other way than that @scott@payforstay.com mentioned, to keep the old logic? I want to transform my Legacy factories to the new L8 standard, but can't because I am using the old $attributes variable a lot to manipulate my factories.

As the old define method was defined:

    /**
     * Define a class with a given set of attributes.
     *
     * @param  string  $class
     * @param  callable  $attributes
     * @return $this
     */
    public function define($class, callable $attributes)
    {
        $this->definitions[$class] = $attributes;

        return $this;
    }

Why is the callable $attributes removed from Laravel's 8 definition() method? Or is it moved to an other method which I need to use?

deligoez's avatar

Take a look at this gist of mine: https://gist.github.com/deligoez/db7e992bda08585cdce221fbc5fb25d6

It's not a 1-1 replacement for the old way of using attributes but still kind of a solution.

New Laravel factories were designed that way. You should define all your inner factory calls as lazy and all conditional logic for attributes should be handled inside factory state methods.

<?php

namespace Database\Factories;

use App\Models\City;
use App\Models\Country;
use App\Models\Region;
use Illuminate\Database\Eloquent\Factories\Factory;

class CityFactory extends Factory
{
    protected $model = City::class;

    public function definition(): array
    {
        return [
            'name'      => $this->faker->city(),
            'code'      => $this->faker->numerify('###'),
            'latitude'  => $this->faker->latitude(),
            'longitude' => $this->faker->longitude(),
            'country_id' => Country::factory()->lazy(),
            'region_id'  => Region::factory()->lazy(),
        ];
    }

    public function country(null|int|Country $country): self
    {
        return $this->state(function (array $attributes) use ($country) {
            $country = $country === null
                ? Country::factory()->lazy()
                : ($country instanceof Country ? $country->id : $country);

            return ['country_id' => $country];
        });
    }

    public function region(null|int|Region $region): self
    {
        return $this->state(function (array $attributes) use ($region) {
            $country = $attributes['country_id'] ?? Country::factory()->lazy();

            $region = $region === null
                ? Region::factory()->lazy(['country_id' => $country])->id
                : ($region instanceof Region ? $region->id : $region);

            return [
                'region_id' => $region,
                'country_id' => $country,
            ];
        });
    }
}
5 likes

Please or to participate in this conversation.