diff --git a/extension.neon b/extension.neon index b9d3e7b..86801f5 100644 --- a/extension.neon +++ b/extension.neon @@ -3,6 +3,10 @@ services: class: Nextras\OrmPhpStan\Types\CollectionReturnTypeExtension tags: - phpstan.broker.dynamicMethodReturnTypeExtension + - + class: Nextras\OrmPhpStan\Types\MapperMethodReturnTypeExtension + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension - class: Nextras\OrmPhpStan\Types\RelationshipReturnTypeExtension tags: diff --git a/src/Types/MapperMethodReturnTypeExtension.php b/src/Types/MapperMethodReturnTypeExtension.php new file mode 100644 index 0000000..99eb4df --- /dev/null +++ b/src/Types/MapperMethodReturnTypeExtension.php @@ -0,0 +1,94 @@ +repositoryEntityTypeHelper = $repositoryEntityTypeHelper; + } + + + public function getClass(): string + { + return DbalMapper::class; + } + + + public function isMethodSupported(MethodReflection $methodReflection): bool + { + static $methods = [ + 'toEntity', + 'toCollection', + ]; + return in_array($methodReflection->getName(), $methods, true); + } + + + public function getTypeFromMethodCall( + MethodReflection $methodReflection, + MethodCall $methodCall, + Scope $scope + ): Type + { + $mapper = $scope->getType($methodCall->var); + \assert($mapper instanceof TypeWithClassName); + + $defaultReturn = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + + if ($mapper->getClassName() === DbalMapper::class) { + return $defaultReturn; + } + + $mapperClass = $mapper->getClassName(); + do { + $repositoryClass = \str_replace('Mapper', 'Repository', $mapperClass); + $mapperClass = \get_parent_class($mapperClass); + } while (!\class_exists($repositoryClass) && $mapperClass !== DbalMapper::class); + + try { + $repository = new \ReflectionClass($repositoryClass); + } catch (\ReflectionException $e) { + return $defaultReturn; + } + + $entityType = $this->repositoryEntityTypeHelper->resolveFirst( + $repository, + $scope + ); + + $methodName = $methodReflection->getName(); + if ($methodName === 'toEntity') { + return TypeCombinator::addNull($entityType); + } elseif ($methodName === 'toCollection') { + return TypeCombinator::intersect( + new ObjectType(ICollection::class), + new IterableType(new IntegerType(), $entityType) + ); + } + + throw new ShouldNotHappenException(); + } +} diff --git a/tests/testbox/Types/MapperTest.php b/tests/testbox/Types/MapperTest.php new file mode 100644 index 0000000..bf2223b --- /dev/null +++ b/tests/testbox/Types/MapperTest.php @@ -0,0 +1,26 @@ +takeAuthors($this->toCollection([])); + $this->takeAuthor($this->toEntity([])); + } + + + private function takeAuthor(?Author $author) + { + } + + + /** + * @param iterable $authors + */ + private function takeAuthors($authors) + { + } +} diff --git a/tests/testbox/Types/fixtures/AuthorsMapper.php b/tests/testbox/Types/fixtures/AuthorsMapper.php new file mode 100644 index 0000000..54d1748 --- /dev/null +++ b/tests/testbox/Types/fixtures/AuthorsMapper.php @@ -0,0 +1,10 @@ +