From 846cd93692a4d70b4e6a7ccc4cd6a4a7560ce390 Mon Sep 17 00:00:00 2001 From: MateuszKolankowski Date: Tue, 27 Jan 2026 00:10:13 +0100 Subject: [PATCH 1/7] Remove lazy loading for Doctrine EntityManager --- src/bundle/Core/Resources/config/services.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/src/bundle/Core/Resources/config/services.yml b/src/bundle/Core/Resources/config/services.yml index 166b1940cc..dff63373ad 100644 --- a/src/bundle/Core/Resources/config/services.yml +++ b/src/bundle/Core/Resources/config/services.yml @@ -368,7 +368,6 @@ services: ibexa.doctrine.orm.entity_manager: class: Doctrine\ORM\EntityManager - lazy: true factory: ['@ibexa.doctrine.orm.entity_manager_factory', 'getEntityManager'] ibexa.doctrine.orm.entity_manager_factory: From 4f4131198aee3a27f42200020bd9362de79bf6cb Mon Sep 17 00:00:00 2001 From: MateuszKolankowski Date: Thu, 29 Jan 2026 09:04:15 +0100 Subject: [PATCH 2/7] Refactor entity manager to use SiteAccessAwareEntityManager for PHP 8.4 compatibility --- src/bundle/Core/Resources/config/services.yml | 5 +- .../Doctrine/SiteAccessAwareEntityManager.php | 314 ++++++++++++++++++ 2 files changed, 317 insertions(+), 2 deletions(-) create mode 100644 src/lib/Persistence/Doctrine/SiteAccessAwareEntityManager.php diff --git a/src/bundle/Core/Resources/config/services.yml b/src/bundle/Core/Resources/config/services.yml index dff63373ad..f3205e4fa0 100644 --- a/src/bundle/Core/Resources/config/services.yml +++ b/src/bundle/Core/Resources/config/services.yml @@ -367,8 +367,9 @@ services: - { name: console.command } ibexa.doctrine.orm.entity_manager: - class: Doctrine\ORM\EntityManager - factory: ['@ibexa.doctrine.orm.entity_manager_factory', 'getEntityManager'] + class: Ibexa\Core\Persistence\Doctrine\SiteAccessAwareEntityManager + arguments: + $entityManagerFactory: '@ibexa.doctrine.orm.entity_manager_factory' ibexa.doctrine.orm.entity_manager_factory: class: Ibexa\Bundle\Core\Entity\EntityManagerFactory diff --git a/src/lib/Persistence/Doctrine/SiteAccessAwareEntityManager.php b/src/lib/Persistence/Doctrine/SiteAccessAwareEntityManager.php new file mode 100644 index 0000000000..147f3c6743 --- /dev/null +++ b/src/lib/Persistence/Doctrine/SiteAccessAwareEntityManager.php @@ -0,0 +1,314 @@ +resolvedEntityManager ??= $this->entityManagerFactory->getEntityManager(); + } + + public function getConnection(): Connection + { + return $this->getWrapped()->getConnection(); + } + + public function getExpressionBuilder(): Query\Expr + { + return $this->getWrapped()->getExpressionBuilder(); + } + + public function beginTransaction(): void + { + $this->getWrapped()->beginTransaction(); + } + + /** + * @deprecated Use {@see wrapInTransaction()} instead. + */ + public function transactional($func): mixed + { + return $this->getWrapped()->transactional($func); + } + + public function wrapInTransaction(callable $func): mixed + { + return $this->getWrapped()->wrapInTransaction($func); + } + + public function commit(): void + { + $this->getWrapped()->commit(); + } + + public function rollback(): void + { + $this->getWrapped()->rollback(); + } + + public function createQuery($dql = ''): Query + { + return $this->getWrapped()->createQuery($dql); + } + + /** + * @deprecated + */ + public function createNamedQuery($name): Query + { + return $this->getWrapped()->createNamedQuery($name); + } + + public function createNativeQuery($sql, ResultSetMapping $rsm): NativeQuery + { + return $this->getWrapped()->createNativeQuery($sql, $rsm); + } + + /** + * @deprecated + */ + public function createNamedNativeQuery($name): NativeQuery + { + return $this->getWrapped()->createNamedNativeQuery($name); + } + + public function createQueryBuilder(): QueryBuilder + { + return $this->getWrapped()->createQueryBuilder(); + } + + /** + * @template T of object + * + * @param class-string $entityName + * + * @return T|null + */ + public function getReference($entityName, $id): ?object + { + return $this->getWrapped()->getReference($entityName, $id); + } + + /** + * @deprecated + * + * @template T of object + * + * @param class-string $entityName + * + * @return T|null + */ + public function getPartialReference($entityName, $identifier): ?object + { + return $this->getWrapped()->getPartialReference($entityName, $identifier); + } + + public function close(): void + { + $this->getWrapped()->close(); + } + + /** + * @deprecated + * + * @template T of object + * + * @param T $entity + * + * @return T + */ + public function copy($entity, $deep = false): object + { + return $this->getWrapped()->copy($entity, $deep); + } + + public function lock($entity, $lockMode, $lockVersion = null): void + { + $this->getWrapped()->lock($entity, $lockMode, $lockVersion); + } + + public function getEventManager(): \Doctrine\Common\EventManager + { + return $this->getWrapped()->getEventManager(); + } + + public function getConfiguration(): Configuration + { + return $this->getWrapped()->getConfiguration(); + } + + public function isOpen(): bool + { + return $this->getWrapped()->isOpen(); + } + + public function getUnitOfWork(): UnitOfWork + { + return $this->getWrapped()->getUnitOfWork(); + } + + /** + * @deprecated + */ + public function getHydrator($hydrationMode): AbstractHydrator + { + return $this->getWrapped()->getHydrator($hydrationMode); + } + + public function newHydrator($hydrationMode): AbstractHydrator + { + return $this->getWrapped()->newHydrator($hydrationMode); + } + + public function getProxyFactory(): ProxyFactory + { + return $this->getWrapped()->getProxyFactory(); + } + + public function getFilters(): FilterCollection + { + return $this->getWrapped()->getFilters(); + } + + public function isFiltersStateClean(): bool + { + return $this->getWrapped()->isFiltersStateClean(); + } + + public function hasFilters(): bool + { + return $this->getWrapped()->hasFilters(); + } + + public function getCache(): ?Cache + { + return $this->getWrapped()->getCache(); + } + + /** + * @template T of object + * + * @param class-string $className + * + * @return T|null + */ + public function find($className, $id, $lockMode = null, $lockVersion = null): ?object + { + return $this->getWrapped()->find($className, $id, $lockMode, $lockVersion); + } + + public function persist(object $object): void + { + $this->getWrapped()->persist($object); + } + + public function remove(object $object): void + { + $this->getWrapped()->remove($object); + } + + public function clear(): void + { + $this->getWrapped()->clear(); + } + + public function detach(object $object): void + { + $this->getWrapped()->detach($object); + } + + public function refresh(object $object, ?int $lockMode = null): void + { + $this->getWrapped()->refresh($object, $lockMode); + } + + public function flush(): void + { + $this->getWrapped()->flush(); + } + + /** + * @template T of object + * + * @param class-string $className + * + * @return EntityRepository + */ + public function getRepository($className): EntityRepository + { + return $this->getWrapped()->getRepository($className); + } + + /** + * @template T of object + * + * @param class-string $className + * + * @return ClassMetadata + */ + public function getClassMetadata($className): ClassMetadata + { + return $this->getWrapped()->getClassMetadata($className); + } + + public function getMetadataFactory(): ClassMetadataFactory + { + return $this->getWrapped()->getMetadataFactory(); + } + + public function initializeObject(object $obj): void + { + $this->getWrapped()->initializeObject($obj); + } + + public function isUninitializedObject(mixed $value): bool + { + return $this->getWrapped()->isUninitializedObject($value); + } + + public function contains(object $object): bool + { + return $this->getWrapped()->contains($object); + } +} From a9a7717418f46c76a002da005cdd8e7695e5bc1c Mon Sep 17 00:00:00 2001 From: MateuszKolankowski Date: Thu, 29 Jan 2026 14:44:30 +0100 Subject: [PATCH 3/7] Refactor SiteAccessAwareEntityManager to remove deprecated methods and improve PHP 8.4 compatibility --- phpstan-baseline.neon | 12 +++++++ .../Doctrine/SiteAccessAwareEntityManager.php | 32 +++---------------- 2 files changed, 16 insertions(+), 28 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 9a1d744f69..eb4582b458 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -17766,6 +17766,18 @@ parameters: count: 1 path: src/lib/Persistence/Cache/UserPreferenceHandler.php + - + message: '#^Method Doctrine\\Persistence\\ObjectManager\:\:find\(\) invoked with 4 parameters, 2 required\.$#' + identifier: arguments.count + count: 1 + path: src/lib/Persistence/Doctrine/SiteAccessAwareEntityManager.php + + - + message: '#^Return type \(Doctrine\\ORM\\Mapping\\ClassMetadataFactory\) of method Ibexa\\Core\\Persistence\\Doctrine\\SiteAccessAwareEntityManager\:\:getMetadataFactory\(\) should be compatible with return type \(Doctrine\\Persistence\\Mapping\\ClassMetadataFactory\\>\) of method Doctrine\\Persistence\\ObjectManager\:\:getMetadataFactory\(\)$#' + identifier: method.childReturnType + count: 2 + path: src/lib/Persistence/Doctrine/SiteAccessAwareEntityManager.php + - message: '#^Property Ibexa\\Core\\Persistence\\FieldTypeRegistry\:\:\$coreFieldTypes \(array\\) does not accept array\\.$#' identifier: assign.propertyType diff --git a/src/lib/Persistence/Doctrine/SiteAccessAwareEntityManager.php b/src/lib/Persistence/Doctrine/SiteAccessAwareEntityManager.php index 147f3c6743..3eaadff938 100644 --- a/src/lib/Persistence/Doctrine/SiteAccessAwareEntityManager.php +++ b/src/lib/Persistence/Doctrine/SiteAccessAwareEntityManager.php @@ -26,13 +26,6 @@ use Ibexa\Bundle\Core\Entity\EntityManagerFactory; /** - * A non-lazy decorator for EntityManager that resolves the actual EntityManager - * lazily on first use via EntityManagerFactory. - * - * This decorator solves the PHP 8.4 lazy proxy compatibility issue where Symfony's - * lazy proxy mechanism cannot handle nested lazy services. By implementing explicit - * lazy resolution, we avoid the "Lazy proxy factory must return a non-lazy object" error. - * * @internal */ final class SiteAccessAwareEntityManager implements EntityManagerInterface @@ -64,9 +57,6 @@ public function beginTransaction(): void $this->getWrapped()->beginTransaction(); } - /** - * @deprecated Use {@see wrapInTransaction()} instead. - */ public function transactional($func): mixed { return $this->getWrapped()->transactional($func); @@ -92,9 +82,6 @@ public function createQuery($dql = ''): Query return $this->getWrapped()->createQuery($dql); } - /** - * @deprecated - */ public function createNamedQuery($name): Query { return $this->getWrapped()->createNamedQuery($name); @@ -105,9 +92,6 @@ public function createNativeQuery($sql, ResultSetMapping $rsm): NativeQuery return $this->getWrapped()->createNativeQuery($sql, $rsm); } - /** - * @deprecated - */ public function createNamedNativeQuery($name): NativeQuery { return $this->getWrapped()->createNamedNativeQuery($name); @@ -131,8 +115,6 @@ public function getReference($entityName, $id): ?object } /** - * @deprecated - * * @template T of object * * @param class-string $entityName @@ -150,16 +132,16 @@ public function close(): void } /** - * @deprecated - * * @template T of object * * @param T $entity + * @param bool $deep * * @return T */ public function copy($entity, $deep = false): object { + /** @var T */ return $this->getWrapped()->copy($entity, $deep); } @@ -188,9 +170,6 @@ public function getUnitOfWork(): UnitOfWork return $this->getWrapped()->getUnitOfWork(); } - /** - * @deprecated - */ public function getHydrator($hydrationMode): AbstractHydrator { return $this->getWrapped()->getHydrator($hydrationMode); @@ -227,11 +206,8 @@ public function getCache(): ?Cache } /** - * @template T of object - * - * @param class-string $className - * - * @return T|null + * @param int|string|null $lockMode + * @param int|null $lockVersion */ public function find($className, $id, $lockMode = null, $lockVersion = null): ?object { From 2b2748cf68319c0841f4e86fcab833e0dd8b35b5 Mon Sep 17 00:00:00 2001 From: MateuszKolankowski Date: Thu, 29 Jan 2026 14:55:15 +0100 Subject: [PATCH 4/7] Fixed type hint for getExpressionBuilder method in SiteAccessAwareEntityManager --- src/lib/Persistence/Doctrine/SiteAccessAwareEntityManager.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib/Persistence/Doctrine/SiteAccessAwareEntityManager.php b/src/lib/Persistence/Doctrine/SiteAccessAwareEntityManager.php index 3eaadff938..082f746759 100644 --- a/src/lib/Persistence/Doctrine/SiteAccessAwareEntityManager.php +++ b/src/lib/Persistence/Doctrine/SiteAccessAwareEntityManager.php @@ -19,6 +19,7 @@ use Doctrine\ORM\NativeQuery; use Doctrine\ORM\Proxy\ProxyFactory; use Doctrine\ORM\Query; +use Doctrine\ORM\Query\Expr; use Doctrine\ORM\Query\FilterCollection; use Doctrine\ORM\Query\ResultSetMapping; use Doctrine\ORM\QueryBuilder; @@ -47,7 +48,7 @@ public function getConnection(): Connection return $this->getWrapped()->getConnection(); } - public function getExpressionBuilder(): Query\Expr + public function getExpressionBuilder(): Expr { return $this->getWrapped()->getExpressionBuilder(); } From 8a12b613566ab0d18951898cb467af34e900a0ab Mon Sep 17 00:00:00 2001 From: MateuszKolankowski Date: Thu, 29 Jan 2026 16:05:16 +0100 Subject: [PATCH 5/7] Refactor find method in SiteAccessAwareEntityManager to remove unused parameters --- phpstan-baseline.neon | 6 ------ .../Persistence/Doctrine/SiteAccessAwareEntityManager.php | 8 ++------ 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index eb4582b458..0dbee62412 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -17766,12 +17766,6 @@ parameters: count: 1 path: src/lib/Persistence/Cache/UserPreferenceHandler.php - - - message: '#^Method Doctrine\\Persistence\\ObjectManager\:\:find\(\) invoked with 4 parameters, 2 required\.$#' - identifier: arguments.count - count: 1 - path: src/lib/Persistence/Doctrine/SiteAccessAwareEntityManager.php - - message: '#^Return type \(Doctrine\\ORM\\Mapping\\ClassMetadataFactory\) of method Ibexa\\Core\\Persistence\\Doctrine\\SiteAccessAwareEntityManager\:\:getMetadataFactory\(\) should be compatible with return type \(Doctrine\\Persistence\\Mapping\\ClassMetadataFactory\\>\) of method Doctrine\\Persistence\\ObjectManager\:\:getMetadataFactory\(\)$#' identifier: method.childReturnType diff --git a/src/lib/Persistence/Doctrine/SiteAccessAwareEntityManager.php b/src/lib/Persistence/Doctrine/SiteAccessAwareEntityManager.php index 082f746759..71a48cad39 100644 --- a/src/lib/Persistence/Doctrine/SiteAccessAwareEntityManager.php +++ b/src/lib/Persistence/Doctrine/SiteAccessAwareEntityManager.php @@ -206,13 +206,9 @@ public function getCache(): ?Cache return $this->getWrapped()->getCache(); } - /** - * @param int|string|null $lockMode - * @param int|null $lockVersion - */ - public function find($className, $id, $lockMode = null, $lockVersion = null): ?object + public function find($className, $id): ?object { - return $this->getWrapped()->find($className, $id, $lockMode, $lockVersion); + return $this->getWrapped()->find($className, $id); } public function persist(object $object): void From 2e4a78678e6f7d01fa7af2a21dc11b33f0d23bd4 Mon Sep 17 00:00:00 2001 From: MateuszKolankowski Date: Wed, 4 Feb 2026 14:51:08 +0100 Subject: [PATCH 6/7] Implement reset functionality in SiteAccessAwareEntityManager and update service configuration --- src/bundle/Core/Resources/config/services.yml | 2 ++ .../Doctrine/SiteAccessAwareEntityManager.php | 17 +++++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/bundle/Core/Resources/config/services.yml b/src/bundle/Core/Resources/config/services.yml index f3205e4fa0..f8db70e326 100644 --- a/src/bundle/Core/Resources/config/services.yml +++ b/src/bundle/Core/Resources/config/services.yml @@ -370,6 +370,8 @@ services: class: Ibexa\Core\Persistence\Doctrine\SiteAccessAwareEntityManager arguments: $entityManagerFactory: '@ibexa.doctrine.orm.entity_manager_factory' + tags: + - { name: 'kernel.reset', method: 'reset' } ibexa.doctrine.orm.entity_manager_factory: class: Ibexa\Bundle\Core\Entity\EntityManagerFactory diff --git a/src/lib/Persistence/Doctrine/SiteAccessAwareEntityManager.php b/src/lib/Persistence/Doctrine/SiteAccessAwareEntityManager.php index 71a48cad39..6f2a2dc343 100644 --- a/src/lib/Persistence/Doctrine/SiteAccessAwareEntityManager.php +++ b/src/lib/Persistence/Doctrine/SiteAccessAwareEntityManager.php @@ -25,19 +25,32 @@ use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\UnitOfWork; use Ibexa\Bundle\Core\Entity\EntityManagerFactory; +use Ibexa\Contracts\Core\MVC\EventSubscriber\ConfigScopeChangeSubscriber; +use Ibexa\Core\MVC\Symfony\Event\ScopeChangeEvent; +use Symfony\Contracts\Service\ResetInterface; /** * @internal */ -final class SiteAccessAwareEntityManager implements EntityManagerInterface +final class SiteAccessAwareEntityManager implements EntityManagerInterface, ConfigScopeChangeSubscriber, ResetInterface { private ?EntityManagerInterface $resolvedEntityManager = null; public function __construct( - private readonly EntityManagerFactory $entityManagerFactory, + private readonly EntityManagerFactory $entityManagerFactory ) { } + public function onConfigScopeChange(ScopeChangeEvent $event): void + { + $this->resolvedEntityManager = null; + } + + public function reset(): void + { + $this->resolvedEntityManager = null; + } + private function getWrapped(): EntityManagerInterface { return $this->resolvedEntityManager ??= $this->entityManagerFactory->getEntityManager(); From 70acf0b8db8bc403b3d7423489f686343e50596c Mon Sep 17 00:00:00 2001 From: MateuszKolankowski Date: Wed, 4 Feb 2026 15:13:45 +0100 Subject: [PATCH 7/7] Made code compatibile with older php versions --- .../Doctrine/SiteAccessAwareEntityManager.php | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/lib/Persistence/Doctrine/SiteAccessAwareEntityManager.php b/src/lib/Persistence/Doctrine/SiteAccessAwareEntityManager.php index 6f2a2dc343..88a2d51a8c 100644 --- a/src/lib/Persistence/Doctrine/SiteAccessAwareEntityManager.php +++ b/src/lib/Persistence/Doctrine/SiteAccessAwareEntityManager.php @@ -34,11 +34,13 @@ */ final class SiteAccessAwareEntityManager implements EntityManagerInterface, ConfigScopeChangeSubscriber, ResetInterface { + private EntityManagerFactory $entityManagerFactory; + private ?EntityManagerInterface $resolvedEntityManager = null; - public function __construct( - private readonly EntityManagerFactory $entityManagerFactory - ) { + public function __construct(EntityManagerFactory $entityManagerFactory) + { + $this->entityManagerFactory = $entityManagerFactory; } public function onConfigScopeChange(ScopeChangeEvent $event): void @@ -71,12 +73,15 @@ public function beginTransaction(): void $this->getWrapped()->beginTransaction(); } - public function transactional($func): mixed + public function transactional($func) { return $this->getWrapped()->transactional($func); } - public function wrapInTransaction(callable $func): mixed + /** + * @return mixed + */ + public function wrapInTransaction(callable $func) { return $this->getWrapped()->wrapInTransaction($func); } @@ -288,7 +293,10 @@ public function initializeObject(object $obj): void $this->getWrapped()->initializeObject($obj); } - public function isUninitializedObject(mixed $value): bool + /** + * @param mixed $value + */ + public function isUninitializedObject($value): bool { return $this->getWrapped()->isUninitializedObject($value); }