diff --git a/README.md b/README.md index db360efc..ba1e0644 100644 --- a/README.md +++ b/README.md @@ -12,14 +12,31 @@ This extension provides following features: * Provides correct return type for `Doctrine\ORM\EntityManager::find`, `getReference` and `getPartialReference` when `Foo::class` entity class name is provided as the first argument * Adds missing `matching` method on `Doctrine\Common\Collections\Collection`. This can be turned off by setting `parameters.doctrine.allCollectionsSelectable` to `false`. -This extension does not yet support custom `repositoryClass` specified for each entity class. However, if your repositories have a common base class, you can configure it in your `phpstan.neon` and PHPStan will see additional methods you define in it: +If your repositories have a common base class, you can configure it in your `phpstan.neon` and PHPStan will see additional methods you define in it: -``` +```neon parameters: doctrine: repositoryClass: MyApp\Doctrine\BetterEntityRepository ``` +Alternatively, you can provide your object manager and all custom repositories will be loaded + +```neon +parameters: + doctrine: + objectManagerLoader: tests/object-manager.php +``` + +For example, in a Symfony project, `object-manager.php` would look something like this: + +```php +require dirname(__DIR__).'/../config/bootstrap.php'; +$kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']); +$kernel->boot(); +return $kernel->getContainer()->get('doctrine')->getManager(); +``` + ## Usage To use this extension, require it in [Composer](https://getcomposer.org/): diff --git a/extension.neon b/extension.neon index 4a8c5099..9c117320 100644 --- a/extension.neon +++ b/extension.neon @@ -1,7 +1,8 @@ parameters: doctrine: - repositoryClass: Doctrine\ORM\EntityRepository + repositoryClass: null allCollectionsSelectable: true + objectManagerLoader: null conditionalTags: PHPStan\Reflection\Doctrine\DoctrineSelectableClassReflectionExtension: @@ -20,8 +21,6 @@ services: - phpstan.broker.dynamicMethodReturnTypeExtension - class: PHPStan\Type\Doctrine\ObjectManagerGetRepositoryDynamicReturnTypeExtension - arguments: - repositoryClass: %doctrine.repositoryClass% tags: - phpstan.broker.dynamicMethodReturnTypeExtension - @@ -34,7 +33,10 @@ services: - phpstan.broker.dynamicMethodReturnTypeExtension - class: PHPStan\Type\Doctrine\ManagerRegistryGetRepositoryDynamicReturnTypeExtension - arguments: - repositoryClass: %doctrine.repositoryClass% tags: - phpstan.broker.dynamicMethodReturnTypeExtension + - + class: PHPStan\Type\Doctrine\ObjectMetadataResolver + arguments: + objectManagerLoader: %doctrine.objectManagerLoader% + repositoryClass: %doctrine.repositoryClass% diff --git a/src/Type/Doctrine/GetRepositoryDynamicReturnTypeExtension.php b/src/Type/Doctrine/GetRepositoryDynamicReturnTypeExtension.php index ffac342b..f8412a49 100644 --- a/src/Type/Doctrine/GetRepositoryDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/GetRepositoryDynamicReturnTypeExtension.php @@ -13,12 +13,12 @@ abstract class GetRepositoryDynamicReturnTypeExtension implements \PHPStan\Type\DynamicMethodReturnTypeExtension { - /** @var string */ - private $repositoryClass; + /** @var ObjectMetadataResolver */ + private $metadataResolver; - public function __construct(string $repositoryClass) + public function __construct(ObjectMetadataResolver $metadataResolver) { - $this->repositoryClass = $repositoryClass; + $this->metadataResolver = $metadataResolver; } public function isMethodSupported(MethodReflection $methodReflection): bool @@ -42,7 +42,10 @@ public function getTypeFromMethodCall( return new MixedType(); } - return new ObjectRepositoryType($argType->getValue(), $this->repositoryClass); + $objectName = $argType->getValue(); + $repositoryClass = $this->metadataResolver->getRepositoryClass($objectName); + + return new ObjectRepositoryType($objectName, $repositoryClass); } } diff --git a/src/Type/Doctrine/ObjectMetadataResolver.php b/src/Type/Doctrine/ObjectMetadataResolver.php new file mode 100644 index 00000000..acb9a578 --- /dev/null +++ b/src/Type/Doctrine/ObjectMetadataResolver.php @@ -0,0 +1,72 @@ +objectManager = $this->getObjectManager($objectManagerLoader); + } + if ($repositoryClass !== null) { + $this->repositoryClass = $repositoryClass; + } elseif ($this->objectManager !== null && get_class($this->objectManager) === 'Doctrine\ODM\MongoDB\DocumentManager') { + $this->repositoryClass = 'Doctrine\ODM\MongoDB\DocumentRepository'; + } else { + $this->repositoryClass = 'Doctrine\ORM\EntityRepository'; + } + } + + /** + * @phpcsSuppress SlevomatCodingStandard.TypeHints.TypeHintDeclaration.MissingReturnTypeHint + * @param string $objectManagerLoader + * @return ObjectManager + */ + private function getObjectManager(string $objectManagerLoader) + { + if (! file_exists($objectManagerLoader) && ! is_readable($objectManagerLoader)) { + throw new RuntimeException('Object manager could not be loaded'); + } + + return require $objectManagerLoader; + } + + public function getRepositoryClass(string $className): string + { + if ($this->objectManager === null) { + return $this->repositoryClass; + } + + $metadata = $this->objectManager->getClassMetadata($className); + + $ormMetadataClass = 'Doctrine\ORM\Mapping\ClassMetadata'; + if ($metadata instanceof $ormMetadataClass) { + /** @var \Doctrine\ORM\Mapping\ClassMetadata $ormMetadata */ + $ormMetadata = $metadata; + return $ormMetadata->customRepositoryClassName ?? $this->repositoryClass; + } + + $odmMetadataClass = 'Doctrine\ODM\MongoDB\Mapping\ClassMetadata'; + if ($metadata instanceof $odmMetadataClass) { + /** @var \Doctrine\ODM\MongoDB\Mapping\ClassMetadata $odmMetadata */ + $odmMetadata = $metadata; + return $odmMetadata->customRepositoryClassName ?? $this->repositoryClass; + } + + return $this->repositoryClass; + } + +} diff --git a/tests/DoctrineIntegration/ODM/DocumentManagerIntegrationTest.php b/tests/DoctrineIntegration/ODM/DocumentManagerIntegrationTest.php index f46b795b..fccbceb4 100644 --- a/tests/DoctrineIntegration/ODM/DocumentManagerIntegrationTest.php +++ b/tests/DoctrineIntegration/ODM/DocumentManagerIntegrationTest.php @@ -16,6 +16,7 @@ public function dataTopics(): array ['documentManagerDynamicReturn'], ['documentRepositoryDynamicReturn'], ['documentManagerMergeReturn'], + ['customRepositoryUsage'], ]; } diff --git a/tests/DoctrineIntegration/ODM/data/customRepositoryUsage-2.json b/tests/DoctrineIntegration/ODM/data/customRepositoryUsage-2.json new file mode 100644 index 00000000..47d27e81 --- /dev/null +++ b/tests/DoctrineIntegration/ODM/data/customRepositoryUsage-2.json @@ -0,0 +1,7 @@ +[ + { + "message": "Call to an undefined method PHPStan\\DoctrineIntegration\\ODM\\CustomRepositoryUsage\\MyRepository::nonexistant().", + "line": 31, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/DoctrineIntegration/ODM/data/customRepositoryUsage.php b/tests/DoctrineIntegration/ODM/data/customRepositoryUsage.php new file mode 100644 index 00000000..72c4e498 --- /dev/null +++ b/tests/DoctrineIntegration/ODM/data/customRepositoryUsage.php @@ -0,0 +1,64 @@ +repository = $documentManager->getRepository(MyDocument::class); + } + + public function get(): void + { + $test = $this->repository->get('testing'); + $test->doSomethingElse(); + } + + public function nonexistant(): void + { + $this->repository->nonexistant(); + } +} + +/** + * @Document(repositoryClass=MyRepository::class) + */ +class MyDocument +{ + /** + * @Id(strategy="NONE", type="string") + * + * @var string + */ + private $id; + + public function doSomethingElse(): void + { + } +} + +class MyRepository extends DocumentRepository +{ + public function get(string $id): MyDocument + { + $document = $this->find($id); + + if ($document === null) { + throw new RuntimeException('Not found...'); + } + + return $document; + } +} diff --git a/tests/DoctrineIntegration/ODM/document-manager.php b/tests/DoctrineIntegration/ODM/document-manager.php new file mode 100644 index 00000000..403a6ab6 --- /dev/null +++ b/tests/DoctrineIntegration/ODM/document-manager.php @@ -0,0 +1,29 @@ +setProxyDir(__DIR__); +$config->setProxyNamespace('PHPstan\Doctrine\OdmProxies'); +$config->setMetadataCacheImpl(new ArrayCache()); +$config->setHydratorDir(__DIR__); +$config->setHydratorNamespace('PHPstan\Doctrine\OdmHydrators'); + +$config->setMetadataDriverImpl( + new AnnotationDriver( + new AnnotationReader(), + [__DIR__ . '/data'] + ) +); + +return DocumentManager::create( + null, + $config +); diff --git a/tests/DoctrineIntegration/ODM/phpstan.neon b/tests/DoctrineIntegration/ODM/phpstan.neon index 446f214a..c83c9fb1 100644 --- a/tests/DoctrineIntegration/ODM/phpstan.neon +++ b/tests/DoctrineIntegration/ODM/phpstan.neon @@ -3,4 +3,4 @@ includes: parameters: doctrine: - repositoryClass: Doctrine\ODM\MongoDB\DocumentRepository + objectManagerLoader: tests/DoctrineIntegration/ODM/document-manager.php diff --git a/tests/DoctrineIntegration/ORM/EntityManagerIntegrationTest.php b/tests/DoctrineIntegration/ORM/EntityManagerIntegrationTest.php index a6353a59..ab226b07 100644 --- a/tests/DoctrineIntegration/ORM/EntityManagerIntegrationTest.php +++ b/tests/DoctrineIntegration/ORM/EntityManagerIntegrationTest.php @@ -16,6 +16,7 @@ public function dataTopics(): array ['entityManagerDynamicReturn'], ['entityRepositoryDynamicReturn'], ['entityManagerMergeReturn'], + ['customRepositoryUsage'], ]; } diff --git a/tests/DoctrineIntegration/ORM/data/customRepositoryUsage-2.json b/tests/DoctrineIntegration/ORM/data/customRepositoryUsage-2.json new file mode 100644 index 00000000..c89ab944 --- /dev/null +++ b/tests/DoctrineIntegration/ORM/data/customRepositoryUsage-2.json @@ -0,0 +1,7 @@ +[ + { + "message": "Call to an undefined method PHPStan\\DoctrineIntegration\\ORM\\CustomRepositoryUsage\\MyRepository::nonexistant().", + "line": 30, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/DoctrineIntegration/ORM/data/customRepositoryUsage.php b/tests/DoctrineIntegration/ORM/data/customRepositoryUsage.php new file mode 100644 index 00000000..8580916f --- /dev/null +++ b/tests/DoctrineIntegration/ORM/data/customRepositoryUsage.php @@ -0,0 +1,65 @@ +repository = $entityManager->getRepository(MyEntity::class); + } + + public function get(): void + { + $test = $this->repository->get(1); + $test->doSomethingElse(); + } + + public function nonexistant(): void + { + $this->repository->nonexistant(); + } +} + +/** + * @ORM\Entity(repositoryClass=MyRepository::class) + */ +class MyEntity +{ + /** + * @ORM\Id() + * @ORM\GeneratedValue() + * @ORM\Column(type="integer") + * + * @var int + */ + private $id; + + public function doSomethingElse(): void + { + } +} + +class MyRepository extends EntityRepository +{ + public function get(int $id): MyEntity + { + $entity = $this->find($id); + + if ($entity === null) { + throw new RuntimeException('Not found...'); + } + + return $entity; + } +} diff --git a/tests/DoctrineIntegration/ORM/entity-manager.php b/tests/DoctrineIntegration/ORM/entity-manager.php new file mode 100644 index 00000000..45e13b3a --- /dev/null +++ b/tests/DoctrineIntegration/ORM/entity-manager.php @@ -0,0 +1,30 @@ +setProxyDir(__DIR__); +$config->setProxyNamespace('PHPstan\Doctrine\OrmProxies'); +$config->setMetadataCacheImpl(new ArrayCache()); + +$config->setMetadataDriverImpl( + new AnnotationDriver( + new AnnotationReader(), + [__DIR__ . '/data'] + ) +); + +return EntityManager::create( + [ + 'driver' => 'pdo_sqlite', + 'memory' => true, + ], + $config +); diff --git a/tests/DoctrineIntegration/ORM/phpstan.neon b/tests/DoctrineIntegration/ORM/phpstan.neon index e6dd2808..80883c95 100644 --- a/tests/DoctrineIntegration/ORM/phpstan.neon +++ b/tests/DoctrineIntegration/ORM/phpstan.neon @@ -1,2 +1,6 @@ includes: - ../../../extension.neon + +parameters: + doctrine: + objectManagerLoader: tests/DoctrineIntegration/ORM/entity-manager.php