From 77d2642f6fc1be58c8b3c7ee85248606a9aa274c Mon Sep 17 00:00:00 2001 From: ItsOtherMauridian <165866613+ItsOtherMauridian@users.noreply.github.com> Date: Fri, 22 May 2026 19:28:01 +0000 Subject: [PATCH] fix: infer repositories from named managers --- ...etRepositoryDynamicReturnTypeExtension.php | 39 +++- src/Type/Doctrine/ObjectMetadataResolver.php | 40 +++- ...amedManagerRepositoryTypeInferenceTest.php | 35 +++ .../Doctrine/data/named-manager-registry.php | 205 ++++++++++++++++++ .../data/named-manager-repository.neon | 5 + .../Doctrine/data/namedManagerRepository.php | 41 ++++ 6 files changed, 361 insertions(+), 4 deletions(-) create mode 100644 tests/Type/Doctrine/NamedManagerRepositoryTypeInferenceTest.php create mode 100644 tests/Type/Doctrine/data/named-manager-registry.php create mode 100644 tests/Type/Doctrine/data/named-manager-repository.neon create mode 100644 tests/Type/Doctrine/data/namedManagerRepository.php diff --git a/src/Type/Doctrine/GetRepositoryDynamicReturnTypeExtension.php b/src/Type/Doctrine/GetRepositoryDynamicReturnTypeExtension.php index 5a4f3e26..0ed33eb8 100644 --- a/src/Type/Doctrine/GetRepositoryDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/GetRepositoryDynamicReturnTypeExtension.php @@ -104,9 +104,10 @@ public function getTypeFromMethodCall( } $repositoryTypes = []; + $managerName = $this->getManagerName($scope, $methodCall->getArgs()); foreach ($objectNames as $objectName) { try { - $repositoryClass = $this->getRepositoryClass($objectName, $defaultRepositoryClass); + $repositoryClass = $this->getRepositoryClass($objectName, $defaultRepositoryClass, $managerName); } catch (\Doctrine\Persistence\Mapping\MappingException | MappingException | AnnotationException $e) { return $this->getDefaultReturnType($scope, $methodCall->getArgs(), $methodReflection, $defaultRepositoryClass); } @@ -138,7 +139,24 @@ private function getDefaultReturnType(Scope $scope, array $args, MethodReflectio return $defaultType; } - private function getRepositoryClass(string $className, string $defaultRepositoryClass): string + /** + * @param Arg[] $args + */ + private function getManagerName(Scope $scope, array $args): ?string + { + if (count($args) < 2) { + return null; + } + + $constantStrings = $scope->getType($args[1]->value)->getConstantStrings(); + if (count($constantStrings) !== 1) { + return null; + } + + return $constantStrings[0]->getValue(); + } + + private function getRepositoryClass(string $className, string $defaultRepositoryClass, ?string $managerName): string { if (!$this->reflectionProvider->hasClass($className)) { return $defaultRepositoryClass; @@ -149,6 +167,23 @@ private function getRepositoryClass(string $className, string $defaultRepository return $defaultRepositoryClass; } + if ($managerName !== null) { + $objectManager = $this->metadataResolver->getObjectManagerByName($managerName); + if ($objectManager !== null) { + $metadata = $objectManager->getClassMetadata($classReflection->getName()); + $odmMetadataClass = 'Doctrine\ODM\MongoDB\Mapping\ClassMetadata'; + if ($metadata instanceof $odmMetadataClass) { + /** @var ClassMetadata $odmMetadata */ + $odmMetadata = $metadata; + return $odmMetadata->customRepositoryClassName ?? $defaultRepositoryClass; + } + + if ($metadata instanceof \Doctrine\ORM\Mapping\ClassMetadata) { + return $metadata->customRepositoryClassName ?? $defaultRepositoryClass; + } + } + } + $metadata = $this->metadataResolver->getClassMetadata($classReflection->getName()); if ($metadata !== null) { return $metadata->customRepositoryClassName ?? $defaultRepositoryClass; diff --git a/src/Type/Doctrine/ObjectMetadataResolver.php b/src/Type/Doctrine/ObjectMetadataResolver.php index bfcafd42..e53786a0 100644 --- a/src/Type/Doctrine/ObjectMetadataResolver.php +++ b/src/Type/Doctrine/ObjectMetadataResolver.php @@ -7,6 +7,7 @@ use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Mapping\MappingException; +use Doctrine\Persistence\ManagerRegistry; use Doctrine\Persistence\ObjectManager; use PHPStan\Doctrine\Mapping\ClassMetadataFactory; use PHPStan\ShouldNotHappenException; @@ -23,7 +24,7 @@ final class ObjectMetadataResolver private ?string $objectManagerLoader = null; - /** @var ObjectManager|false|null */ + /** @var ObjectManager|ManagerRegistry|false|null */ private $objectManager; private ?ClassMetadataFactory $metadataFactory = null; @@ -46,6 +47,38 @@ public function hasObjectManagerLoader(): bool /** @api */ public function getObjectManager(): ?ObjectManager + { + $objectManager = $this->getObjectManagerLoaderResult(); + if ($objectManager instanceof ManagerRegistry) { + return $objectManager->getManager(); + } + + if ($objectManager instanceof ObjectManager) { + return $objectManager; + } + + return null; + } + + public function getObjectManagerByName(string $name): ?ObjectManager + { + $objectManager = $this->getObjectManagerLoaderResult(); + if (!$objectManager instanceof ManagerRegistry) { + return null; + } + + $namedObjectManager = $objectManager->getManager($name); + if (!$namedObjectManager instanceof ObjectManager) { + return null; + } + + return $namedObjectManager; + } + + /** + * @return ObjectManager|ManagerRegistry|null + */ + private function getObjectManagerLoaderResult() { if ($this->objectManager === false) { return null; @@ -172,7 +205,10 @@ public function getClassMetadata(string $className): ?ClassMetadata return $ormMetadata; } - private function loadObjectManager(string $objectManagerLoader): ?ObjectManager + /** + * @return ObjectManager|ManagerRegistry|null + */ + private function loadObjectManager(string $objectManagerLoader) { if (!is_file($objectManagerLoader)) { throw new ShouldNotHappenException(sprintf( diff --git a/tests/Type/Doctrine/NamedManagerRepositoryTypeInferenceTest.php b/tests/Type/Doctrine/NamedManagerRepositoryTypeInferenceTest.php new file mode 100644 index 00000000..e9d98120 --- /dev/null +++ b/tests/Type/Doctrine/NamedManagerRepositoryTypeInferenceTest.php @@ -0,0 +1,35 @@ + */ + public function dataFileAsserts(): iterable + { + yield from $this->gatherAssertTypes(__DIR__ . '/data/namedManagerRepository.php'); + } + + /** + * @dataProvider dataFileAsserts + * @param mixed ...$args + */ + public function testFileAsserts( + string $assertType, + string $file, + ...$args + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + /** @return string[] */ + public static function getAdditionalConfigFiles(): array + { + return [__DIR__ . '/data/named-manager-repository.neon']; + } + +} diff --git a/tests/Type/Doctrine/data/named-manager-registry.php b/tests/Type/Doctrine/data/named-manager-registry.php new file mode 100644 index 00000000..0d797e4d --- /dev/null +++ b/tests/Type/Doctrine/data/named-manager-registry.php @@ -0,0 +1,205 @@ +customRepositoryClassName = $repositoryClass; + + $metadataFactory = new class ($metadata) implements ClassMetadataFactory { + + private ClassMetadata $metadata; + + public function __construct(ClassMetadata $metadata) + { + $this->metadata = $metadata; + } + + public function getAllMetadata() + { + return [$this->metadata]; + } + + public function getMetadataFor($className) + { + return $this->metadata; + } + + public function hasMetadataFor($className) + { + return $className === SharedEntity::class; + } + + public function setMetadataFor($className, $class) + { + } + + public function isTransient($className) + { + return $className !== SharedEntity::class; + } + + }; + + return new class ($metadata, $metadataFactory) implements ObjectManager { + + private ClassMetadata $metadata; + + private ClassMetadataFactory $metadataFactory; + + public function __construct(ClassMetadata $metadata, ClassMetadataFactory $metadataFactory) + { + $this->metadata = $metadata; + $this->metadataFactory = $metadataFactory; + } + + public function find($className, $id) + { + return null; + } + + public function persist($object) + { + } + + public function remove($object) + { + } + + public function clear($objectName = null) + { + } + + public function detach($object) + { + } + + public function refresh($object) + { + } + + public function flush() + { + } + + public function getRepository($className) + { + throw new LogicException('Repository instances are not needed by this type inference fixture.'); + } + + public function getClassMetadata($className) + { + return $this->metadata; + } + + public function getMetadataFactory() + { + return $this->metadataFactory; + } + + public function initializeObject($obj) + { + } + + public function contains($object) + { + return false; + } + + }; +}; + +$defaultManager = $createManager(DefaultSharedRepository::class); +$tenantManager = $createManager(TenantSharedRepository::class); + +return new class ($defaultManager, $tenantManager) implements ManagerRegistry { + + private ObjectManager $defaultManager; + + private ObjectManager $tenantManager; + + public function __construct(ObjectManager $defaultManager, ObjectManager $tenantManager) + { + $this->defaultManager = $defaultManager; + $this->tenantManager = $tenantManager; + } + + public function getDefaultConnectionName() + { + return 'default'; + } + + public function getConnection($name = null) + { + throw new LogicException('Connections are not used in this type inference fixture.'); + } + + public function getConnections() + { + return []; + } + + public function getConnectionNames() + { + return []; + } + + public function getDefaultManagerName() + { + return 'default'; + } + + public function getManager($name = null) + { + if ($name === 'tenant') { + return $this->tenantManager; + } + + return $this->defaultManager; + } + + public function getManagers() + { + return [ + 'default' => $this->defaultManager, + 'tenant' => $this->tenantManager, + ]; + } + + public function resetManager($name = null) + { + return $this->getManager($name); + } + + public function getManagerNames() + { + return [ + 'default' => 'default', + 'tenant' => 'tenant', + ]; + } + + public function getRepository($persistentObject, $persistentManagerName = null) + { + return $this->getManager($persistentManagerName)->getRepository($persistentObject); + } + + public function getManagerForClass($class) + { + return $class === SharedEntity::class ? $this->defaultManager : null; + } + + public function getAliasNamespace($alias) + { + throw new LogicException('Alias namespaces are not used in this type inference fixture.'); + } + +}; diff --git a/tests/Type/Doctrine/data/named-manager-repository.neon b/tests/Type/Doctrine/data/named-manager-repository.neon new file mode 100644 index 00000000..3dbf3eea --- /dev/null +++ b/tests/Type/Doctrine/data/named-manager-repository.neon @@ -0,0 +1,5 @@ +includes: + - ../../../../extension.neon +parameters: + doctrine: + objectManagerLoader: named-manager-registry.php diff --git a/tests/Type/Doctrine/data/namedManagerRepository.php b/tests/Type/Doctrine/data/namedManagerRepository.php new file mode 100644 index 00000000..3c8d223c --- /dev/null +++ b/tests/Type/Doctrine/data/namedManagerRepository.php @@ -0,0 +1,41 @@ +', $registry->getRepository(SharedEntity::class, 'default')); + assertType(TenantSharedRepository::class . '<' . SharedEntity::class . '>', $registry->getRepository(SharedEntity::class, 'tenant')); + } + +} + +class SharedEntity +{ + +} + +/** + * @template T of object + * @extends EntityRepository + */ +class DefaultSharedRepository extends EntityRepository +{ + +} + +/** + * @template T of object + * @extends EntityRepository + */ +class TenantSharedRepository extends EntityRepository +{ + +}