diff --git a/composer.json b/composer.json index 5148ff56..280f7145 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,13 @@ "require": { "php": "~7.1", "phpstan/phpstan": "^0.11", - "nikic/php-parser": "^4.0" + "nikic/php-parser": "^4.0", + "doctrine/orm": "^2.5", + "doctrine/common": "^2.7", + "doctrine/annotations": "^1.5", + "doctrine/persistence": "^1.1", + "doctrine/dbal": "^2.5", + "doctrine/event-manager": "^1.0" }, "require-dev": { "consistence/coding-standard": "^3.0.1", @@ -23,15 +29,8 @@ "phpstan/phpstan-strict-rules": "^0.11", "phpunit/phpunit": "^7.0", "slevomat/coding-standard": "^4.5.2", - "doctrine/common": "^2.7", - "doctrine/orm": "^2.5", "doctrine/collections": "^1.0" }, - "conflict": { - "doctrine/common": "<2.7", - "doctrine/orm": "<2.5", - "doctrine/collections": "<1.0" - }, "autoload": { "psr-4": { "PHPStan\\": "src/" diff --git a/extension.neon b/extension.neon index 87b2841a..1b5b841b 100644 --- a/extension.neon +++ b/extension.neon @@ -1,6 +1,7 @@ parameters: doctrine: repositoryClass: Doctrine\ORM\EntityRepository + metadata: [] allCollectionsSelectable: true conditionalTags: @@ -21,7 +22,7 @@ services: - class: PHPStan\Type\Doctrine\ObjectManagerGetRepositoryDynamicReturnTypeExtension arguments: - repositoryClass: %doctrine.repositoryClass% + metadataProvider: @PHPStan\DoctrineClassMetadataProvider tags: - phpstan.broker.dynamicMethodReturnTypeExtension - @@ -32,3 +33,9 @@ services: class: PHPStan\Type\Doctrine\ObjectRepositoryDynamicReturnTypeExtension tags: - phpstan.broker.dynamicMethodReturnTypeExtension + - + class: PHPStan\DoctrineClassMetadataProvider + arguments: + repositoryClass: %doctrine.repositoryClass% + mapping: %doctrine.metadata% + diff --git a/src/DoctrineClassMetadataProvider.php b/src/DoctrineClassMetadataProvider.php new file mode 100644 index 00000000..bc351076 --- /dev/null +++ b/src/DoctrineClassMetadataProvider.php @@ -0,0 +1,93 @@ + 0) { + $configuration = new Configuration(); + $configuration->setDefaultRepositoryClassName($repositoryClass); + $configuration->setMetadataDriverImpl($this->setupMappingDriver($mapping)); + $configuration->setProxyDir('/dev/null'); + $configuration->setProxyNamespace('__DP__'); + $configuration->setAutoGenerateProxyClasses(ProxyFactory::AUTOGENERATE_EVAL); + $evm = new EventManager(); + $this->em = EntityManager::create( + \Doctrine\DBAL\DriverManager::getConnection(['host' => '/:memory:', 'driver' => 'pdo_sqlite'], $configuration, $evm), + $configuration, + $evm + ); + } + $this->repositoryClass = $repositoryClass; + } + + /** + * @param mixed[] $mapping + */ + private function setupMappingDriver(array $mapping): MappingDriver + { + $driver = new MappingDriverChain(); + foreach ($mapping as $namespace => $config) { + switch ($config['type']) { + case 'annotation': + AnnotationRegistry::registerUniqueLoader('class_exists'); + $nested = new Mapping\Driver\AnnotationDriver(new AnnotationReader(), $config['paths']); + break; + case 'yml': + case 'yaml': + $nested = new Mapping\Driver\YamlDriver($config['paths']); + break; + case 'xml': + $nested = new Mapping\Driver\XmlDriver($config['paths']); + break; + default: + throw new \InvalidArgumentException('Unknown mapping type: ' . $config['type']); + } + $driver->addDriver($nested, $namespace); + } + return $driver; + } + + public function getBaseRepositoryClass(): string + { + return $this->repositoryClass; + } + + public function getRepositoryClass(string $className): string + { + if ($this->em === null) { + return $this->getBaseRepositoryClass(); + } + + try { + return $this->em->getClassMetadata($className)->customRepositoryClassName ?: $this->getBaseRepositoryClass(); + } catch (MappingException $e) { + return $this->getBaseRepositoryClass(); + } + } + +} diff --git a/src/Type/Doctrine/ObjectManagerGetRepositoryDynamicReturnTypeExtension.php b/src/Type/Doctrine/ObjectManagerGetRepositoryDynamicReturnTypeExtension.php index 871f12b0..f0d52100 100644 --- a/src/Type/Doctrine/ObjectManagerGetRepositoryDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/ObjectManagerGetRepositoryDynamicReturnTypeExtension.php @@ -4,6 +4,7 @@ use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; +use PHPStan\DoctrineClassMetadataProvider; use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\Constant\ConstantStringType; @@ -13,12 +14,12 @@ class ObjectManagerGetRepositoryDynamicReturnTypeExtension implements \PHPStan\Type\DynamicMethodReturnTypeExtension { - /** @var string */ - private $repositoryClass; + /** @var DoctrineClassMetadataProvider */ + private $metadataProvider; - public function __construct(string $repositoryClass) + public function __construct(DoctrineClassMetadataProvider $metadataProvider) { - $this->repositoryClass = $repositoryClass; + $this->metadataProvider = $metadataProvider; } public function getClass(): string @@ -47,7 +48,7 @@ public function getTypeFromMethodCall( return new MixedType(); } - return new ObjectRepositoryType($argType->getValue(), $this->repositoryClass); + return new ObjectRepositoryType($argType->getValue(), $this->metadataProvider->getRepositoryClass($argType->getValue())); } } diff --git a/tests/DoctrineIntegration/ORM/EntityManagerIntegrationTest.php b/tests/DoctrineIntegration/ORM/EntityManagerIntegrationTest.php index a6353a59..27ec7503 100644 --- a/tests/DoctrineIntegration/ORM/EntityManagerIntegrationTest.php +++ b/tests/DoctrineIntegration/ORM/EntityManagerIntegrationTest.php @@ -14,6 +14,9 @@ public function dataTopics(): array { return [ ['entityManagerDynamicReturn'], + ['entityManagerDynamicReturnCustomRepositoryAnnotations'], + ['entityManagerDynamicReturnCustomRepositoryXml'], + ['entityManagerDynamicReturnCustomRepositoryYml'], ['entityRepositoryDynamicReturn'], ['entityManagerMergeReturn'], ]; diff --git a/tests/DoctrineIntegration/ORM/data/entityManagerDynamicReturnCustomRepositoryAnnotations-2.json b/tests/DoctrineIntegration/ORM/data/entityManagerDynamicReturnCustomRepositoryAnnotations-2.json new file mode 100644 index 00000000..ed64c52e --- /dev/null +++ b/tests/DoctrineIntegration/ORM/data/entityManagerDynamicReturnCustomRepositoryAnnotations-2.json @@ -0,0 +1,7 @@ +[ + { + "message": "Call to an undefined method PHPStan\\DoctrineIntegration\\ORM\\EntityManagerDynamicReturnCustomRepositoryAnnotations\\MyEntityRepository::nonexistant().", + "line": 35, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/DoctrineIntegration/ORM/data/entityManagerDynamicReturnCustomRepositoryAnnotations.php b/tests/DoctrineIntegration/ORM/data/entityManagerDynamicReturnCustomRepositoryAnnotations.php new file mode 100644 index 00000000..7818c10c --- /dev/null +++ b/tests/DoctrineIntegration/ORM/data/entityManagerDynamicReturnCustomRepositoryAnnotations.php @@ -0,0 +1,66 @@ +entityManager = $entityManager; + } + + public function findDynamicType(): void + { + $test = $this->entityManager->getRepository(MyEntity::class)->findMyEntity(1); + + if ($test === null) { + throw new RuntimeException('Sorry, but no...'); + } + + $test->doSomething(); + } + + public function errorDynamicType(): void + { + $this->entityManager->getRepository(MyEntity::class)->nonexistant(); + } +} + +/** + * @ORM\Entity(repositoryClass="MyEntityRepository") + */ +class MyEntity +{ + /** + * @ORM\Id() + * @ORM\GeneratedValue() + * @ORM\Column(type="integer") + * + * @var int + */ + private $id; + + public function doSomething(): void + { + } +} + +class MyEntityRepository extends EntityRepository +{ + public function findMyEntity($id): ?MyEntity + { + return $this->findOneBy([ + 'id' => $id + ]); + } +} diff --git a/tests/DoctrineIntegration/ORM/data/entityManagerDynamicReturnCustomRepositoryXml-2.json b/tests/DoctrineIntegration/ORM/data/entityManagerDynamicReturnCustomRepositoryXml-2.json new file mode 100644 index 00000000..55597387 --- /dev/null +++ b/tests/DoctrineIntegration/ORM/data/entityManagerDynamicReturnCustomRepositoryXml-2.json @@ -0,0 +1,7 @@ +[ + { + "message": "Call to an undefined method PHPStan\\DoctrineIntegration\\ORM\\EntityManagerDynamicReturnCustomRepositoryXml\\MyEntityRepository::nonexistant().", + "line": 35, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/DoctrineIntegration/ORM/data/entityManagerDynamicReturnCustomRepositoryXml.php b/tests/DoctrineIntegration/ORM/data/entityManagerDynamicReturnCustomRepositoryXml.php new file mode 100644 index 00000000..a2ba1080 --- /dev/null +++ b/tests/DoctrineIntegration/ORM/data/entityManagerDynamicReturnCustomRepositoryXml.php @@ -0,0 +1,56 @@ +entityManager = $entityManager; + } + + public function findDynamicType(): void + { + $test = $this->entityManager->getRepository(MyEntity::class)->findMyEntity(1); + + if ($test === null) { + throw new RuntimeException('Sorry, but no...'); + } + + $test->doSomething(); + } + + public function errorDynamicType(): void + { + $this->entityManager->getRepository(MyEntity::class)->nonexistant(); + } +} + +class MyEntity +{ + private $id; + + public function doSomething(): void + { + } +} + +class MyEntityRepository extends EntityRepository +{ + public function findMyEntity($id): ?MyEntity + { + return $this->findOneBy([ + 'id' => $id + ]); + } +} diff --git a/tests/DoctrineIntegration/ORM/data/entityManagerDynamicReturnCustomRepositoryYml-2.json b/tests/DoctrineIntegration/ORM/data/entityManagerDynamicReturnCustomRepositoryYml-2.json new file mode 100644 index 00000000..f698df88 --- /dev/null +++ b/tests/DoctrineIntegration/ORM/data/entityManagerDynamicReturnCustomRepositoryYml-2.json @@ -0,0 +1,7 @@ +[ + { + "message": "Call to an undefined method PHPStan\\DoctrineIntegration\\ORM\\EntityManagerDynamicReturnCustomRepositoryYml\\MyEntityRepository::nonexistant().", + "line": 35, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/DoctrineIntegration/ORM/data/entityManagerDynamicReturnCustomRepositoryYml.php b/tests/DoctrineIntegration/ORM/data/entityManagerDynamicReturnCustomRepositoryYml.php new file mode 100644 index 00000000..69cf89b1 --- /dev/null +++ b/tests/DoctrineIntegration/ORM/data/entityManagerDynamicReturnCustomRepositoryYml.php @@ -0,0 +1,56 @@ +entityManager = $entityManager; + } + + public function findDynamicType(): void + { + $test = $this->entityManager->getRepository(MyEntity::class)->findMyEntity(1); + + if ($test === null) { + throw new RuntimeException('Sorry, but no...'); + } + + $test->doSomething(); + } + + public function errorDynamicType(): void + { + $this->entityManager->getRepository(MyEntity::class)->nonexistant(); + } +} + +class MyEntity +{ + private $id; + + public function doSomething(): void + { + } +} + +class MyEntityRepository extends EntityRepository +{ + public function findMyEntity($id): ?MyEntity + { + return $this->findOneBy([ + 'id' => $id + ]); + } +} diff --git a/tests/DoctrineIntegration/ORM/mappings/PHPStan.DoctrineIntegration.ORM.EntityManagerDynamicReturnCustomRepositoryXml.MyEntity.dcm.xml b/tests/DoctrineIntegration/ORM/mappings/PHPStan.DoctrineIntegration.ORM.EntityManagerDynamicReturnCustomRepositoryXml.MyEntity.dcm.xml new file mode 100644 index 00000000..cc7f2edd --- /dev/null +++ b/tests/DoctrineIntegration/ORM/mappings/PHPStan.DoctrineIntegration.ORM.EntityManagerDynamicReturnCustomRepositoryXml.MyEntity.dcm.xml @@ -0,0 +1,8 @@ + + + + + + + diff --git a/tests/DoctrineIntegration/ORM/mappings/PHPStan.DoctrineIntegration.ORM.EntityManagerDynamicReturnCustomRepositoryYml.MyEntity.dcm.yml b/tests/DoctrineIntegration/ORM/mappings/PHPStan.DoctrineIntegration.ORM.EntityManagerDynamicReturnCustomRepositoryYml.MyEntity.dcm.yml new file mode 100644 index 00000000..74739e1e --- /dev/null +++ b/tests/DoctrineIntegration/ORM/mappings/PHPStan.DoctrineIntegration.ORM.EntityManagerDynamicReturnCustomRepositoryYml.MyEntity.dcm.yml @@ -0,0 +1,8 @@ +PHPStan\DoctrineIntegration\ORM\EntityManagerDynamicReturnCustomRepositoryYml\MyEntity: + type: entity + repositoryClass: PHPStan\DoctrineIntegration\ORM\EntityManagerDynamicReturnCustomRepositoryYml\MyEntityRepository + id: + id: + type: integer + generator: + strategy: AUTO diff --git a/tests/DoctrineIntegration/ORM/phpstan.neon b/tests/DoctrineIntegration/ORM/phpstan.neon index e6dd2808..8d035ab1 100644 --- a/tests/DoctrineIntegration/ORM/phpstan.neon +++ b/tests/DoctrineIntegration/ORM/phpstan.neon @@ -1,2 +1,16 @@ includes: - ../../../extension.neon +parameters: + doctrine: + metadata: + PHPStan\DoctrineIntegration\ORM\EntityManagerDynamicReturnCustomRepositoryAnnotations: + type: annotation + paths: [] + PHPStan\DoctrineIntegration\ORM\EntityManagerDynamicReturnCustomRepositoryXml: + type: xml + paths: + - tests/DoctrineIntegration/ORM/mappings/ + PHPStan\DoctrineIntegration\ORM\EntityManagerDynamicReturnCustomRepositoryYml: + type: yml + paths: + - tests/DoctrineIntegration/ORM/mappings/