Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 37 additions & 2 deletions src/Type/Doctrine/GetRepositoryDynamicReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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;
Expand All @@ -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<object> $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;
Expand Down
40 changes: 38 additions & 2 deletions src/Type/Doctrine/ObjectMetadataResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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(
Expand Down
35 changes: 35 additions & 0 deletions tests/Type/Doctrine/NamedManagerRepositoryTypeInferenceTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Doctrine;

use PHPStan\Testing\TypeInferenceTestCase;

class NamedManagerRepositoryTypeInferenceTest extends TypeInferenceTestCase
{

/** @return iterable<mixed> */
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'];
}

}
205 changes: 205 additions & 0 deletions tests/Type/Doctrine/data/named-manager-registry.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
<?php declare(strict_types = 1);

use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\Persistence\ManagerRegistry;
use Doctrine\Persistence\ObjectManager;
use Doctrine\Persistence\Mapping\ClassMetadataFactory;
use NamedManagerRepositoryInference\DefaultSharedRepository;
use NamedManagerRepositoryInference\SharedEntity;
use NamedManagerRepositoryInference\TenantSharedRepository;

require_once __DIR__ . '/namedManagerRepository.php';

$createManager = static function (string $repositoryClass): ObjectManager {
$metadata = new ClassMetadata(SharedEntity::class);
$metadata->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.');
}

};
5 changes: 5 additions & 0 deletions tests/Type/Doctrine/data/named-manager-repository.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
includes:
- ../../../../extension.neon
parameters:
doctrine:
objectManagerLoader: named-manager-registry.php
Loading