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
608 stars 63 forks source link

Double call Zenstruck\Foundry\Proxy->object() generates RuntimeException('Cannot auto refresh "Entity" as there are unsaved changes. Be sure to call ->save() or disable auto refreshing') #564

Closed OleksandrConstell closed 5 days ago

OleksandrConstell commented 4 months ago

Hello everyone.

I found an interesting bug with Zenstruck\Foundry\ModelFactory and Zenstruck\Foundry\Proxy. Use:

Entity Quote and QuoteFactor

use Ramsey\Uuid\Doctrine\UuidV7Generator;
class Quote 
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    #[Groups(['event', 'quote'])]
    private ?int $id = null;

    #[ORM\Column(type: 'uuid', unique: true, nullable: false)]
    #[ORM\GeneratedValue(strategy: 'CUSTOM')]
    #[ORM\CustomIdGenerator(class: UuidV7Generator::class)]
    private ?string $uuid = null;

    public function getUuid(): ?string
    {
        return $this->uuid;
    }

    public function setUuid(?string $uuid): void
    {
        $this->uuid = $uuid;
    }
}

use Ramsey\Uuid\Uuid;
class QuoteFactory extends ModelFactory
{
    protected function getDefaults(): array
    {
        return ['uuid' => Uuid::uuid7()->toString()];
    }

    protected static function getClass(): string
    {
        return Quote::class;
    }
}

Unit test

class SomeTest extends BaseTestCase
{
    public function testSomething(): void
    {
        $quote = QuoteFactory::createOne();
        //to be sure that everything is up to date
        $quote->save();

        //First call - no exception
        $quote->object();

        //The second call calls the Exception
        $quote->object();
        /**
         * class Zenstruck\Foundry\Proxy::130
         * RuntimeException(Cannot auto refresh "Quote" as there are unsaved changes. 
         * Be sure to call ->save() or disable auto refreshing
         * (see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#auto-refresh for details)
         */
    }
}

Method $this->objectManager()->getUnitOfWork()->computeChangeSet($om->getClassMetadata($this->class), $this->object) RETURNS:

array(1) {
  ["uuid"]=>
  array(2) {
    [0]=>
    object(Ramsey\Uuid\Lazy\LazyUuidFromString)#12759 (2) {
      ["unwrapped":"Ramsey\Uuid\Lazy\LazyUuidFromString":private]=>
      NULL
      ["uuid":"Ramsey\Uuid\Lazy\LazyUuidFromString":private]=>
      string(36) "018dd661-d095-70f3-8551-7ac7bb17af35"
    }
    [1]=>
    string(36) "018dd661-d095-70f3-8551-7ac7bb17af35"
  }
}

I figured out why. Looks like after these steps the \RuntimeException appears: FILE: https://github.com/zenstruck/foundry/blob/v1.36.1/src/Proxy.php 1) call Proxy->object()
2) Method "->object()" calls "->computeChangeSet()"(LINE 125) -> no changes -> CALL "$this->refresh()" (LINE 134); 3) Method "->refresh()" calls "$this->objectManager()->refresh($this->object);" (LINE 176)

4) Second call Proxy->object() calls "->computeChangeSet()"(LINE 125) -> some changes -> CALL "throw new \RuntimeException()" (LINE 130);

Looks like "refreshing entity" and calling "computeChangeSet" generates unsaved data and as a result exception.

Please take a look.

nikophil commented 5 days ago

hello,

sorry for this late reply :sweat_smile:

I've just tested this with Foundry v2 (using rasmey/uid or symfony/uid as well) and I've never reproduced this.

I'm closing it, feel free to reopen the issue, if this problem still occur in foundry v2