FriendsOfPHP / proxy-manager-lts

Adding support for a wider range of PHP versions to ocramius/proxy-manager.

Home Page:https://github.com/Ocramius/ProxyManager

Repository from Github https://github.comFriendsOfPHP/proxy-manager-ltsRepository from Github https://github.comFriendsOfPHP/proxy-manager-lts

Restrict circular proxy instantiation

pchapl opened this issue · comments

I ran into a problem that took a lot of time, and perhaps it may be solved at the proxy creation level.

My problem was about EntityManager and EntityRepository behavior in Symfony app:

final class ProductRepository implements ProductRepositoryInterface
{
    /** @var EntityRepository<Product> $repository */
    private EntityRepository $repository;

    public function __construct(EntityManagerInterface $entityManager)
    {
        $this->repository = $entityManager->getRepository(Product::class);
    }

    public function update(string $productId): void
    {
        $product = $this->repository->find($productId);
        $product->update(); // <- change state
        $this->entityManager->flush(); // <- do nothing
    }
}

Of course, it turned out that there were different UnitOfWork instances in the injected entity manager and stored repository:

$unitOfWork1 = $this->repository->getEntityManager()->getUnitOfWork();
$unitOfWork2 = $this->entityManager->getUnitOfWork();
dump($unitOfWork1 === $unitOfWork2); // <- FALSE

After investigation, I found implicit circular dependency in my container (and resolved it with making one of my services lazy).

Anyway, I think that it's interesting how can two (proxied) EntityManager instances (with different UnitOfWork instances) exist in the container.

What I found:

  1. My constructor has $entityManager->getRepository call
  2. It internally instantiates some services dependent on this one
  3. It calls $entityManager->getRepository again
    So now we have two \ContainerDjfkKub\EntityManager_9a5be93::getRepository calls in the stack; Thanks to coalesce $this->services['doctrine.dbal.default_connection'] ?? $this->getDoctrine_Dbal_DefaultConnectionService() or something else we do not go there third time
  4. Initializer assigns $wrappedInstance by ref with newly created EntityManager. Twice. While references to first are already saved in container.

My in-place dirty fix for it looks like callable "setter" param which ensures to not assign twice:

// EntityManager_9a5be93
public function getRepository($entityName)
{
    $this->initializer8b6cd && ($this->initializer8b6cd->__invoke(
        $valueHolder3da10,
        $this,
        'getRepository',
        array('entityName' => $entityName),
        $this->initializer8b6cd,
        setter: fn($v) => $this->valueHolder3da10 ??= $v, // <- instead of passing $valueHolder3da10 by ref, this closure assure to assign once
    ) || 1);
        return $this->valueHolder3da10->getRepository($entityName);
}
// App_KernelDevDebugContainer
return $this->services['doctrine.orm.default_entity_manager'] = $this->createProxy('EntityManager_9a5be93', function () {
    return \EntityManager_9a5be93::staticProxyConstructor(function (
        &$wrappedInstance,
        \ProxyManager\Proxy\LazyLoadingInterface $proxy,
        string $method,
        array $args,
        callable $self,
        ?callable $setter = null
    ) {
        $entityManager = $this->getDoctrine_Orm_DefaultEntityManagerService(false);

        if ($setter) {
            $setter($entityManager);
        } else {
            $wrappedInstance = $entityManager;
        }
        $proxy->setProxyInitializer(null);

        return true;
    });
});

I don't think a fix like that should be used. Instead, some checks to protect against proxy double initialization may be added

Version: "friendsofphp/proxy-manager-lts": "v1.0.12" (via "symfony/proxy-manager-bridge": "v6.1.0")

Maybe the whole issue should be redirected to original ocramius/proxymanager, but I can't find easy way to reproduce Symfony behavior using proxymanager directly

Thanks for the detailed report.

Could you please share a reproducing app that I could run to reproduce the issue? That'd help a lot look for a fix

I don't think the issue is with proxy manager. To me it looks like something the DI containe can end up with on its own with complex enough object graphs. To be confirmed of course. Why do you think proxy manager plays a role here ?

Here it is: https://github.com/pchapl/circular-proxy-instantiation

There is unitOfWork instances comprasion in \App\Service\Repository, called twice from \App\Command\Command::execute with different result.

I think it's related to proxy manager because I failed to reproduce it without EntityManager proxy (i.e. it was working before composer install symfony/proxy-manager-bridge)
And I see an issue in nested initializer8b6cd->__invoke calls which is not supposed to happen

I think this is fixed by doctrine/orm#9915 and doctrine/DoctrineBundle#1551
Please report back if not.