diff --git a/composer.json b/composer.json index 2115b6c..4a2fbf8 100644 --- a/composer.json +++ b/composer.json @@ -35,7 +35,8 @@ "SaschaEgerer\\PhpstanTypo3\\Tests\\": "tests/" }, "files": [ - "tests/Unit/Type/data/repository-stub-files.php" + "tests/Unit/Type/data/repository-stub-files.php", + "tests/Unit/Type/QueryResultToArrayDynamicReturnTypeExtension/data/query-result-to-array.php" ] }, "extra": { diff --git a/src/Type/QueryResultToArrayDynamicReturnTypeExtension.php b/src/Type/QueryResultToArrayDynamicReturnTypeExtension.php index ec3eacd..753d59d 100644 --- a/src/Type/QueryResultToArrayDynamicReturnTypeExtension.php +++ b/src/Type/QueryResultToArrayDynamicReturnTypeExtension.php @@ -9,9 +9,11 @@ use PHPStan\Type\DynamicMethodReturnTypeExtension; use PHPStan\Type\ErrorType; use PHPStan\Type\Generic\GenericObjectType; +use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\IntegerType; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; +use PHPStan\Type\TypeTraverser; use SaschaEgerer\PhpstanTypo3\Helpers\Typo3ClassNamingUtilityTrait; use TYPO3\CMS\Extbase\Persistence\QueryResultInterface; @@ -38,12 +40,18 @@ public function getTypeFromMethodCall( Scope $scope ): Type { - $classReflection = $scope->getClassReflection(); - $resultType = $scope->getType($methodCall->var); + + if (!($resultType instanceof ObjectType)) { + $resultType = $this->getGenericTypes( + $scope->getType($methodCall->var) + )[0] ?? null; + } + if ($resultType instanceof GenericObjectType) { $modelType = $resultType->getTypes(); } else { + $classReflection = $scope->getClassReflection(); if ($classReflection === null) { return new ErrorType(); } @@ -58,4 +66,32 @@ public function getTypeFromMethodCall( return new ArrayType(new IntegerType(), $modelType[0]); } + /** + * @return GenericObjectType[] + */ + private function getGenericTypes(Type $baseType): array + { + $genericObjectTypes = []; + TypeTraverser::map($baseType, static function (Type $type, callable $traverse) use (&$genericObjectTypes): Type { + if ($type instanceof GenericObjectType) { + $resolvedType = TypeTraverser::map($type, static function (Type $type, callable $traverse): Type { + if ($type instanceof TemplateType) { + return $traverse($type->getBound()); + } + return $traverse($type); + }); + if (!$resolvedType instanceof GenericObjectType) { + throw new \PHPStan\ShouldNotHappenException(); + } + $genericObjectTypes[] = $resolvedType; + $traverse($type); + return $type; + } + $traverse($type); + return $type; + }); + + return $genericObjectTypes; + } + } diff --git a/tests/Unit/Type/QueryResultToArrayDynamicReturnTypeExtension/QueryResultToArrayDynamicReturnTypeExtensionTest.php b/tests/Unit/Type/QueryResultToArrayDynamicReturnTypeExtension/QueryResultToArrayDynamicReturnTypeExtensionTest.php new file mode 100644 index 0000000..622b994 --- /dev/null +++ b/tests/Unit/Type/QueryResultToArrayDynamicReturnTypeExtension/QueryResultToArrayDynamicReturnTypeExtensionTest.php @@ -0,0 +1,39 @@ + + */ + public function dataFileAsserts(): iterable + { + yield from $this->gatherAssertTypes(__DIR__ . '/data/query-result-to-array.php'); + } + + /** + * @dataProvider dataFileAsserts + * + * @param string $assertType + * @param string $file + * @param mixed ...$args + */ + public function testFileAsserts( + string $assertType, + string $file, + ...$args + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [__DIR__ . '/../../../../extension.neon']; + } + +} diff --git a/tests/Unit/Type/QueryResultToArrayDynamicReturnTypeExtension/data/query-result-to-array.php b/tests/Unit/Type/QueryResultToArrayDynamicReturnTypeExtension/data/query-result-to-array.php new file mode 100644 index 0000000..1b4e541 --- /dev/null +++ b/tests/Unit/Type/QueryResultToArrayDynamicReturnTypeExtension/data/query-result-to-array.php @@ -0,0 +1,91 @@ + + */ +class FrontendUserGroupRepository extends Repository +{ + +} + +/** + * @extends Repository + */ +class FrontendUserCustomFindAllGroupRepository extends Repository +{ + + /** + * @return QueryResultInterface + */ + public function findAll(): QueryResultInterface // phpcs:ignore SlevomatCodingStandard.TypeHints.ReturnTypeHint.MissingAnyTypeHint + { + $queryResult = null; // phpcs:ignore SlevomatCodingStandard.Variables.UselessVariable.UselessVariable + /** @var QueryResult $queryResult */ + return $queryResult; + } + +} + +class FrontendUserGroup extends AbstractEntity +{ + +} + +class MyController extends ActionController +{ + + /** @var FrontendUserGroupRepository */ + private $myRepository; + + /** @var FrontendUserCustomFindAllGroupRepository */ + private $myCustomFindAllRepository; + + public function __construct( + FrontendUserGroupRepository $myRepository, + FrontendUserCustomFindAllGroupRepository $myCustomFindAllRepository + ) + { + $this->myRepository = $myRepository; + $this->myCustomFindAllRepository = $myCustomFindAllRepository; + } + + public function showAction(): void + { + assertType( + 'array', + $this->myRepository->findAll()->toArray() + ); + + $queryResult = $this->myRepository->findAll(); + $myObjects = $queryResult->toArray(); + assertType( + 'array', + $myObjects + ); + + assertType( + 'array', + $this->myCustomFindAllRepository->findAll()->toArray() + ); + + $queryResult = $this->myCustomFindAllRepository->findAll(); + $myObjects = $queryResult->toArray(); + assertType( + 'array', + $myObjects + ); + } + +} diff --git a/tests/Unit/Type/data/repository-stub-files.php b/tests/Unit/Type/data/repository-stub-files.php index 6328121..40a39b8 100644 --- a/tests/Unit/Type/data/repository-stub-files.php +++ b/tests/Unit/Type/data/repository-stub-files.php @@ -99,3 +99,26 @@ public function findAll(): array } } + +/** @extends \TYPO3\CMS\Extbase\Persistence\Repository<\RepositoryStubFiles\My\Test\Extension\Domain\Model\MyModel> */ +class FindAllWithoutReturnTestRepository extends \TYPO3\CMS\Extbase\Persistence\Repository +{ + + public function myTests(): void + { + assertType( + 'array|TYPO3\CMS\Extbase\Persistence\QueryResultInterface', + $this->findAll() + ); + } + + public function findAll() // phpcs:ignore SlevomatCodingStandard.TypeHints.ReturnTypeHint.MissingAnyTypeHint + { + $foo = null; // phpcs:ignore SlevomatCodingStandard.Variables.UselessVariable.UselessVariable + /** + * @var array|\TYPO3\CMS\Extbase\Persistence\QueryResultInterface<\RepositoryStubFiles\My\Test\Extension\Domain\Model\MyModel> $foo + */ + return $foo; + } + +}