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:
- My constructor has
$entityManager->getRepository
call - It internally instantiates some services dependent on this one
- 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 - 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.