zenstruck / foundry

A model factory library for creating expressive, auto-completable, on-demand dev/test fixtures with Symfony and Doctrine.
https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html
MIT License
607 stars 62 forks source link

Force set attribute #601

Closed orangevinz closed 1 month ago

orangevinz commented 1 month ago

I use Froundy for few days and I experienced a weird behaviour If I first create my "static" Profile with a generated uuid, it works.

// OK
final class ProfileStory extends Story
{
    public const PROFILE_JOHN_DOE = '18096a48-a2ae-4eec-ae36-3e352cc3560e';

    public function build(): void
    {
        // Static profile
        ProfileFactory::createOne([
            'name' => 'John Doe',
        ])->forceSet('id', Ulid::fromString(self::PROFILE_JOHN_DOE));

        // Dynamic profiles
        ProfileFactory::createMany(20);
    }
}

but if I do the opposite my id will be ignore...

// KO
final class ProfileStory extends Story
{
    public const PROFILE_JOHN_DOE = '18096a48-a2ae-4eec-ae36-3e352cc3560e';

    public function build(): void
    {
        // Dynamic profiles
        ProfileFactory::createMany(20);

       // Static profile
        ProfileFactory::createOne([
            'name' => 'John Doe',
        ])->forceSet('id', Ulid::fromString(self::PROFILE_JOHN_DOE));
    }
}

And if I do save() my static object, my id is back.

->forceSet('id', Ulid::fromString(self::PROFILE_JOHN_DOE))->save();

Am I doing it the right way ?

nikophil commented 1 month ago

Hello @orangevinz

seems legit to me:

$profile = ProfileFactory::createOne(['name' => 'John Doe']); // $profile is a `Proxy<Profile>` and is already stored in database
$profile->forceSet('id', Ulid::fromString(self::PROFILE_JOHN_DOE)); // id is updated in the variable, but not in db
$profile->save(); // update id in db

The weird behavior comes from the fact that createMany() triggers a flush in doctrine, so if it occurs after the "static profile" creation, the id gets finally updated.

by the way, you can enable "always force properties" in order to not bother with this kind of things, and directly write:

ProfileFactory::createOne(['name' => 'John Doe', 'id' => Ulid::fromString(self::PROFILE_JOHN_DOE));
orangevinz commented 1 month ago

Hey @nikophil Thanks a lot for the clarification. Unfortunately I still can't force the id this way. I can't figure out what I missed

final class ProfileFactory extends ModelFactory
{
    protected function initialize(): self
    {
        return $this->instantiateWith(
            (new Instantiator())
                ->withoutConstructor()
                ->alwaysForceProperties(['id'])
        );
    }
final class ProfileStory extends Story
{
    public const PROFILE_JOHN_DOE = '018f4de7-511a-6249-4064-e2950b4f6f14';

    public function build(): void
    {
        // And a static profile
        ProfileFactory::createOne([
            'id' => Ulid::fromString(self::PROFILE_JOHN_DOE),
            'name' => 'John Doe',
        ]);

        // Create x dynamic profiles
        ProfileFactory::createMany(20);
    }
}
namespace App\Entity\Traits;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\IdGenerator\UlidGenerator;
use Symfony\Bridge\Doctrine\Types\UlidType;
use Symfony\Component\Uid\Ulid;

trait UlidIdentifierEntity
{
    #[ORM\Id]
    #[ORM\Column(type: UlidType::NAME, unique: true)]
    #[ORM\GeneratedValue(strategy: 'CUSTOM')]
    #[ORM\CustomIdGenerator(class: UlidGenerator::class)]
    private ?Ulid $id;

    public function getId(): ?Ulid
    {
        return $this->id;
    }
}
nikophil commented 1 month ago

this is due to the GeneratedValue, when the object is created, doctrine will update the value of your $id, disregarding whether it was filled-in or not.

A solution would be to not use an auto-generated value, and set the id in the constructor;

// Profile.php

    #[ORM\Id]
    #[ORM\Column(type: UlidType::NAME, unique: true)]
    private Ulid $id;

    public function __construct(?Ulid $id = null) 
    {
        $this->id = $id ?? new Ulid::generate(now());
    }

I think this solution is always preferable to the auto-generated one, because you always have a valid object (ie: Profile::$id should never be null, and is never null with this solution). Another good point for this solution is that you can access the id before the object is stored in db which could be useful in some rare occasions.

But because you're using a trait for your ids, you may won't like this solution, so you'll have to use forceSet() + save() I guess

orangevinz commented 1 month ago

Ok it's very clear, I'll stick with forceSet and save then, unless I manage to stop needing a static id 😉 Thanks again for your help and for this library.