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
600 stars 60 forks source link

Issue with Proxy Entities in Version 2 #632

Open fouteox opened 6 days ago

fouteox commented 6 days ago

Hello,

I recently migrated to version 2.

In the following code, I encountered the error below. This seems to be due to V2 now using proxies.

$post = PostFactory::createOne();

// logic with post...

$comment = CommentFactory::createOne(['post' => $post]);

// logic for test...
// I use browser : 

$this->browser()
            ->get(sprintf('/api/post/%s', $post->getId()))
            ->assertStatus(200)
        ; // Without this test using browser, I don't get the error

$comment->setContent('content');
$comment->_save(); // Error here

Error:


 * A new entity was found through the relationship 'App\Entity\Comment#post' that was not configured to cascade persist operations for entity: App\Entity\Post@5238. To solve this issue: Either explicitly call EntityManager#persist() on this unknown entity or configure cascade persist this association in the mapping for example @ManyToOne(..,cascade={"persist"}). If you cannot find out which entity causes the problem implement 'App\Entity\Post#__toString()' to get a clue.

How can this be modified in this case? I didn't have this error in V1.

Thank you.

fouteox commented 6 days ago

More details :

Work :

$post = PostFactory::createOne();

CommentFactory::createOne([
    'post' => $post,
]);

$this->browser()
            ->get(sprintf('/api/post/%s', $post->getId()))
            ->assertStatus(200)
        ;

Work :

$post = PostFactory::createOne();

CommentFactory::createOne([
    'post' => $post,
]);

$comment->setContent('content');
$comment->_save();

The error seems to come from calling via browser by sending $post THEN wanting to modify $comment, but why?

nikophil commented 6 days ago

Hi,

do you modify the post in your functional test with browser?

nikophil commented 6 days ago

what happens if you do this?

$comment->_withoutAutoRefresh(function (Post $post) {
    $comment->setContent('content');
});
$comment->_save();
fouteox commented 6 days ago

No, and this work too :

$comment = CommentFactory::createOne(['post' => PostFactory::createOne()]);

$this->browser()
            ->get(sprintf('/api/post/%s', $comment->getPost()->getId()))
            ->assertStatus(200)
        ;

$comment->setContent('content');
$comment->_save();
nikophil commented 6 days ago

I don't get the difference between the last working version and the failing one?

fouteox commented 6 days ago

what happens if you do this?

$comment->_withoutAutoRefresh(function (Post $post) {
    $comment->setContent('content');
});
$comment->_save();

I have this error :

TypeError: Value of type null returned from AppEntityCommentProxy::__get() must be compatible with unset property AppEntityCommentProxy::$_autoRefresh of type bool

I don't get the difference between the last working version and the failing one?

The only difference between the code that works and the code that doesn't is that I declare post directly in the comment.

If I instantiate it in a variable it fails.

Don't work :

$post = PostFactory::createOne();

$comment = CommentFactory::createOne(['post' => $post]);

$this->browser()
            ->get(sprintf('/api/post/%s', $post->getId()))
            ->assertStatus(200)
        ;

$comment->setContent('content');
$comment->_save();

Work :

$comment = CommentFactory::createOne(['post' => PostFactory::createOne()]);

$this->browser()
            ->get(sprintf('/api/post/%s', $comment->getPost()->getId()))
            ->assertStatus(200)
        ;

$comment->setContent('content');
$comment->_save();

Je viens de voir que tu étais français donc je décris mon problème dans cette langue, je pense que je serai mieux compris.

En fait j'ai l'impression que d'extraire "comment" dans une variable, ce qui en fait donc un proxy, pose problème avec browser pour une raison que j'ignore.

Ce qui est étrange, c'est qu'il y a plusieurs scénarios qui fonctionnent, mais un en particulier pose soucis. C'est quand je créé un "comment" dans une variable, l'affecte ensuite à "post", puis execute un appel à browser. Dans ce cas là, si j'essaie de modifier "comment", l'erreur se produit.

Encore une fois, si je n'extrais pas "comment" dans une variable, je n'ai aucune erreur.

fouteox commented 6 days ago

Also works when we access $comment in browser via $post->getComment()->getId() :

$post = PostFactory::createOne();

$comment = CommentFactory::createOne(['post' => $post]);

$this->browser()
            ->get(sprintf('/api/post/%s', $comment->getPost()->getId()))
            ->assertStatus(200)
        ;

$comment->setContent('content');
$comment->_save();
mmarton commented 5 days ago

I'm having the same issue, with this (simplified):

public function testIssue1(): void
{
    $event = EventFactory::createOne();
    $event->setLocation(LocationFactory::createOne());
    $event->_save();
}
// Doctrine\ORM\ORMInvalidArgumentException: A new entity was found through the relationship 'App\Entity\Event#location'

public function testIssue2(): void
{
    $event = EventFactory::createOne();
    $event->_withoutAutoRefresh(function (Event $event): void {
        $event->setLocation(LocationFactory::createOne());
    });
    $event->_save();
}
// TypeError: Cannot assign null to property AppEntityEventProxy::$_autoRefresh of type bool
nikophil commented 5 days ago

Hi @mmarton

I'm wondering if both problems are the same... Could you give me the whole error message, please? (from your first example) I think your problem will be solved by passing LocationFactory::createOne()->_real()

We're gonna advertise on the docs for this problem

mmarton commented 5 days ago

Sure, here is the full stack:

Foundry (Tests\Integration\Foundry)
 ✘ Orm issue
   ┐
   ├ Doctrine\ORM\ORMInvalidArgumentException: A new entity was found through the relationship 'App\Entity\Event#location' that was not configured to cascade persist operations for entity: Rutherford Group. To solve this issue: Either explicitly call EntityManager#persist() on this unknown entity or configure cascade persist this association in the mapping for example @ManyToOne(..,cascade={"persist"}).
   │
   │ /var/www/html/vendor/doctrine/orm/src/ORMInvalidArgumentException.php:103
   │ /var/www/html/vendor/doctrine/orm/src/UnitOfWork.php:3859
   │ /var/www/html/vendor/doctrine/orm/src/UnitOfWork.php:417
   │ /var/www/html/vendor/doctrine/orm/src/EntityManager.php:403
   │ /var/www/html/vendor/zenstruck/foundry/src/Persistence/PersistenceManager.php:197
   │ /var/www/html/vendor/zenstruck/foundry/src/Persistence/PersistenceManager.php:171
   │ /var/www/html/vendor/zenstruck/foundry/src/Persistence/IsProxy.php:60
   │ /var/www/html/tests/Integration/FoundryTest.php:22
   ┴
NeuralClone commented 5 days ago

While don't know for sure if I've had the same issue as @fouteox, I have had the issue brought up by @mmarton. I fixed it by using _real(). For example:

$contact = ContactFactory::createOne([
    'firstName' => 'Alyson',
    'lastName' => 'Perkins',
    'mainEmail' => null,
    'mainPhone' => null,
])

$accountContact = AccountContactFactory::createOne([
    'contact' => $contact,
    'account' => AccountFactory::new([
        'name' => 'Rhode Island',
        'uid' => 'RI',
    ]),
    'main' => true,
]);

$contact->addAccountContact($accountContact->_real());

// this blows up with the Doctrine error without calling _real() above
$contact->_save();
nikophil commented 4 days ago

I've just created a PR to mitigate this problem (note: it won't work if the setter / adder uses fluent api, due to some restrictions in symfony/var-exporter). It still might occur in some other edge cases, I've also added a not in the docs about it

https://github.com/zenstruck/foundry/pull/635

fouteox commented 4 days ago

I've just created a PR to mitigate this problem (note: it won't work if the setter / adder uses fluent api, due to some restrictions in symfony/var-exporter). It still might occur in some other edge cases, I've also added a not in the docs about it

635

Yes I can confirm that the code below works. That is to say by having a setter which returns not static but void. But is it that we need to double setter for the tests to work?

$post = PostFactory::createOne();

$comment = CommentFactory::createOne(['post' => $post]);

$this->browser()
            ->get(sprintf('/api/post/%s', $post->getId()))
            ->assertStatus(200)
        ;

$comment->setContentWithoutStatic('content'); // Here, setter return void instead static
$comment->_save();