foundry icon indicating copy to clipboard operation
foundry copied to clipboard

Attribute "Lazy Values" instead of callables

Open kbond opened this issue 2 years ago • 3 comments

Related: #244, #373.

Currently, if you want an attribute calculated for every object created by the factory you need to wrap the attribute array in a closure:

UserFactory::new(fn() => ['name' => $this->generateRandomName()]);

// instead of

UserFactory::new(['name' => $this->generateRandomName()]); // only ever calls generateRandomName once

I propose we change attributes to always be an array. To create lazy values, wrap the value in a new LazyValue object (with a helper function):

UserFactory::new(['name' => lazy(fn() => $this->generateRandomName())]);

The primary use-case for having attributes as callables is for faker. We'll need faker to use this lazy system. I'm thinking a new Fake object that's a mixin for Faker\Generator. All method calls wrap the actual method in a LazyValue:

UserFactory::new(['name' => $this->fake()->name())]); // equivalent to lazy(fn() => self::faker()->name())

// or can use a helper function
UserFactory::new(['name' => fake()->name())]); // equivalent to lazy(fn() => faker()->name())

The secondary use-case is to pass the index to the attribute callable during a createMany():

PostFactory::createMany(
    5,
    fn(int $i) => ['title' => "Title $i"]; // "Title 1", "Title 2", ... "Title 5"
);

This has the following benefits:

  1. Allows ModelFactory::getDefaults() to have lazy values (#244)
  2. Makes your code more explicit (lazy keyword).
  3. Help make using faker with data providers easier
  4. (long term) Helps with maintenance - attributes will only be arrays.

Todo:

  1. Add/implement LazyValue concept #427
  2. Add Fake faker mixin
  3. Deprecate using a callable as attributes
  4. Deprecate ModelFactory::faker() and faker() function

kbond avatar Feb 27 '23 21:02 kbond

UserFactory::new(['name' => $this->generateRandomName()]); // only ever calls generateRandomName once

I have been caught out by this more than once. When I forget that it only calls generateRandomName() a single time and I wonder why all my created users have the same name :sweat_smile:

One question I have is how this:

UserFactory::new(['name' => lazy(fn() => $this->generateRandomName())]);

differ to this:

UserFactory::new(['name' => fn() => $this->generateRandomName()]);

ndench avatar Jun 28 '23 01:06 ndench

It's valid that a property could be/accept a closure. The lazy() wrapper ensures this is still possible.

kbond avatar Jun 28 '23 02:06 kbond

Ah yes, that makes a lot of sense. Thanks for the explanation!

ndench avatar Jun 28 '23 22:06 ndench

lazy value has been implemented since then!

nikophil avatar Jun 21 '24 15:06 nikophil