Using Proxies obtained through EntityManager::getReference() in associations? #6934

Open
opened 2026-01-22 15:41:40 +01:00 by admin · 1 comment
Owner

Originally created by @mpdude on GitHub (Feb 21, 2022).

I have two entity classes, Cart and Item, where Cart has a many-to-many association with Item.

Inside a functional test, I'd like to shortcut things a bit, and thus I obtain a reference (proxy) for Item through EntityManager::getReference(Item::class, 42).

I then create a new Cart instance, and the constructor will initialize the Cart::$containedItems collection as a new ArrayCollection. To this collection, I add the Cart reference.

When I try to persist the Cart, I get an exception stating that Item was found as a new Entity through the relationship Cart::$containedItems that was not configured to cascade persist operations.

I know I am on thin ice here, but the documentation on Reference Proxies states:

This is useful, for example, as a performance enhancement, when you want to establish an association to an entity for which you have the identifier. You could simply do this:
$item = $em->getReference('MyProject\Model\Item', $itemId);
$cart->addItem($item);

Details of what's happening

When the data is flushed, UnitOfWork::computeAssociationChanges() will look at the containedItems association. It iterates over the contained entities and tries to determine their state:

193c3abf0e/lib/Doctrine/ORM/UnitOfWork.php (L914-L919)

UnitOfWork does not have a state for the reference/proxy object in its ::$entityStates array, and thus returns STATE_NEW. This leads to taking note of the allegedly new entity a few lines later:

193c3abf0e/lib/Doctrine/ORM/UnitOfWork.php (L930-L939)

Nothing around that area tries to recognize $entry as a proxy. When I try to skip instanceof Proxy values in that place, we will later get into UnitOfWork::getEntityIdentifier() with the proxy as $entity.

This method, again, is not prepared to deal with a proxy:

193c3abf0e/lib/Doctrine/ORM/UnitOfWork.php (L3079-L3083)

The proxy is not in the ::$entityIdentifiers array. It carries the values we'd expect to find in that array in its fields, but my guess would be we'd need a suitable ProxyDefinition to obtain them.

Applicable test cases

I have found \Doctrine\Tests\ORM\Functional\BasicFunctionalTest::testSetSetAssociationWithGetReference which suggests to test the use case I described initially. This test, however, notes:

193c3abf0e/tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php (L547-L551)

That makes me believe the test never really worked with an uninitialized proxy. When the proxy is initialized by accessing properties, we probably have a fully loaded, state-tracked entity with known identifier values in the UnitOfWork.


So, first question probably: Is this accepted as a bug and something that would get support being fixed?

Originally created by @mpdude on GitHub (Feb 21, 2022). I have two entity classes, `Cart` and `Item`, where `Cart` has a many-to-many association with `Item`. Inside a functional test, I'd like to shortcut things a bit, and thus I obtain a reference (proxy) for `Item` through `EntityManager::getReference(Item::class, 42)`. I then create a new `Cart` instance, and the constructor will initialize the `Cart::$containedItems` collection as a new `ArrayCollection`. To this collection, I add the `Cart` reference. When I try to persist the `Cart`, I get an exception stating that `Item` was found as a new Entity through the relationship `Cart::$containedItems` that was not configured to cascade persist operations. I know I am on thin ice here, but the documentation on [Reference Proxies](https://www.doctrine-project.org/projects/doctrine-orm/en/2.11/reference/advanced-configuration.html#reference-proxies) states: > This is useful, for example, as a performance enhancement, when you want to establish an association to an entity for which you have the identifier. You could simply do this: > `$item = $em->getReference('MyProject\Model\Item', $itemId);` > `$cart->addItem($item);` #### Details of what's happening When the data is flushed, `UnitOfWork::computeAssociationChanges()` will look at the `containedItems` association. It iterates over the contained entities and tries to determine their state: https://github.com/doctrine/orm/blob/193c3abf0e5f7a7e2699e7c7ac37c1e713d79ef8/lib/Doctrine/ORM/UnitOfWork.php#L914-L919 `UnitOfWork` does not have a state for the reference/proxy object in its `::$entityStates` array, and thus returns `STATE_NEW`. This leads to taking note of the allegedly new entity a few lines later: https://github.com/doctrine/orm/blob/193c3abf0e5f7a7e2699e7c7ac37c1e713d79ef8/lib/Doctrine/ORM/UnitOfWork.php#L930-L939 Nothing around that area tries to recognize `$entry` as a proxy. When I try to skip `instanceof Proxy` values in that place, we will later get into `UnitOfWork::getEntityIdentifier()` with the proxy as `$entity`. This method, again, is not prepared to deal with a proxy: https://github.com/doctrine/orm/blob/193c3abf0e5f7a7e2699e7c7ac37c1e713d79ef8/lib/Doctrine/ORM/UnitOfWork.php#L3079-L3083 The proxy is not in the `::$entityIdentifiers` array. It carries the values we'd expect to find in that array in its fields, but my guess would be we'd need a suitable `ProxyDefinition` to obtain them. #### Applicable test cases I have found `\Doctrine\Tests\ORM\Functional\BasicFunctionalTest::testSetSetAssociationWithGetReference` which suggests to test the use case I described initially. This test, however, notes: https://github.com/doctrine/orm/blob/193c3abf0e5f7a7e2699e7c7ac37c1e713d79ef8/tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php#L547-L551 That makes me believe the test never really worked with an uninitialized proxy. When the proxy is initialized by accessing properties, we probably have a fully loaded, state-tracked entity with known identifier values in the `UnitOfWork`. <hr/> So, first question probably: Is this accepted as a bug and something that would get support being fixed?
Author
Owner

@mpdude commented on GitHub (Jun 25, 2023):

I seem to be unable to reproduce this as of today.

Test code:

<?php

declare(strict_types=1);

namespace Doctrine\Tests\ORM\Functional\Ticket;

use Doctrine\ORM\UnitOfWork;
use Doctrine\Persistence\Proxy;
use Doctrine\Tests\Models\CMS\CmsArticle;
use Doctrine\Tests\Models\CMS\CmsGroup;
use Doctrine\Tests\Models\CMS\CmsUser;
use Doctrine\Tests\OrmFunctionalTestCase;

final class GH9534Test extends OrmFunctionalTestCase
{
    protected function setUp(): void
    {
        $this->useModelSet('cms');

        parent::setUp();
    }

    public function testUseReferenceInToManyAssociation(): void
    {
        $user           = new CmsUser();
        $user->username = 'joedoe';
        $user->name     = 'Joe Doe';
        $this->_em->persist($user);
        $this->_em->flush();
        $this->_em->clear();

        $userRef = $this->_em->getReference(CmsUser::class, $user->id);
        self::assertInstanceOf(Proxy::class, $userRef);
        self::assertFalse($userRef->__isInitialized());

        self::assertSame(UnitOfWork::STATE_MANAGED, $this->_em->getUnitOfWork()->getEntityState($userRef, UnitOfWork::STATE_NEW));

        $article        = new CmsArticle();
        $article->text  = 'foo';
        $article->topic = 'bar';
        $article->user = $userRef;

        $this->_em->persist($article);
        $this->_em->flush();
        $this->_em->clear();

        $articleFresh = $this->_em->find(CmsArticle::class, $article->id);

        self::assertSame($user->id, $articleFresh->user->getId());
    }

    public function testUseReferenceInOwningSideOfManyToMany(): void
    {
        $group = new CmsGroup();
        $group->name = 'admins';
        $this->_em->persist($group);
        $this->_em->flush();
        $this->_em->clear();

        $groupRef = $this->_em->getReference(CmsGroup::class, $group->id);
        self::assertInstanceOf(Proxy::class, $groupRef);
        self::assertFalse($groupRef->__isInitialized());

        self::assertSame(UnitOfWork::STATE_MANAGED, $this->_em->getUnitOfWork()->getEntityState($groupRef, UnitOfWork::STATE_NEW));

        $user           = new CmsUser();
        $user->username = 'joedoe';
        $user->name     = 'Joe Doe';
        $user->addGroup($groupRef);

        $this->_em->persist($user);
        $this->_em->flush();
        $this->_em->clear();

        $userFresh = $this->_em->find(CmsUser::class, $user->id);

        self::assertCount(1, $userFresh->getGroups());
    }
}

@mpdude commented on GitHub (Jun 25, 2023): I seem to be unable to reproduce this as of today. Test code: ```php <?php declare(strict_types=1); namespace Doctrine\Tests\ORM\Functional\Ticket; use Doctrine\ORM\UnitOfWork; use Doctrine\Persistence\Proxy; use Doctrine\Tests\Models\CMS\CmsArticle; use Doctrine\Tests\Models\CMS\CmsGroup; use Doctrine\Tests\Models\CMS\CmsUser; use Doctrine\Tests\OrmFunctionalTestCase; final class GH9534Test extends OrmFunctionalTestCase { protected function setUp(): void { $this->useModelSet('cms'); parent::setUp(); } public function testUseReferenceInToManyAssociation(): void { $user = new CmsUser(); $user->username = 'joedoe'; $user->name = 'Joe Doe'; $this->_em->persist($user); $this->_em->flush(); $this->_em->clear(); $userRef = $this->_em->getReference(CmsUser::class, $user->id); self::assertInstanceOf(Proxy::class, $userRef); self::assertFalse($userRef->__isInitialized()); self::assertSame(UnitOfWork::STATE_MANAGED, $this->_em->getUnitOfWork()->getEntityState($userRef, UnitOfWork::STATE_NEW)); $article = new CmsArticle(); $article->text = 'foo'; $article->topic = 'bar'; $article->user = $userRef; $this->_em->persist($article); $this->_em->flush(); $this->_em->clear(); $articleFresh = $this->_em->find(CmsArticle::class, $article->id); self::assertSame($user->id, $articleFresh->user->getId()); } public function testUseReferenceInOwningSideOfManyToMany(): void { $group = new CmsGroup(); $group->name = 'admins'; $this->_em->persist($group); $this->_em->flush(); $this->_em->clear(); $groupRef = $this->_em->getReference(CmsGroup::class, $group->id); self::assertInstanceOf(Proxy::class, $groupRef); self::assertFalse($groupRef->__isInitialized()); self::assertSame(UnitOfWork::STATE_MANAGED, $this->_em->getUnitOfWork()->getEntityState($groupRef, UnitOfWork::STATE_NEW)); $user = new CmsUser(); $user->username = 'joedoe'; $user->name = 'Joe Doe'; $user->addGroup($groupRef); $this->_em->persist($user); $this->_em->flush(); $this->_em->clear(); $userFresh = $this->_em->find(CmsUser::class, $user->id); self::assertCount(1, $userFresh->getGroups()); } } ```
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: doctrine/archived-orm#6934