Skip to content

Commit a5121d6

Browse files
Merge pull request #105 from sascha-egerer/issue/103-2
2 parents 778aa50 + ac956ae commit a5121d6

File tree

4 files changed

+140
-20
lines changed

4 files changed

+140
-20
lines changed

phpstan.neon

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,17 @@ parameters:
2222
message: "#^Calling PHPStan\\\\Reflection\\\\InitializerExprTypeResolver\\:\\:getClassConstFetchType\\(\\) is not covered by backward compatibility promise\\. The method might change in a minor PHPStan version\\.$#"
2323
count: 1
2424
path: src/Rule/ValidatorResolverOptionsRule.php
25+
-
26+
message: "#^Method SaschaEgerer\\\\PhpstanTypo3\\\\Tests\\\\Unit\\\\Type\\\\QueryResultToArrayDynamicReturnTypeExtension\\\\FrontendUserGroupCustomFindAllWithoutModelTypeRepository\\:\\:findAll\\(\\) return type with generic interface TYPO3\\\\CMS\\\\Extbase\\\\Persistence\\\\QueryResultInterface does not specify its types\\: ModelType$#"
27+
count: 1
28+
path: tests/Unit/Type/QueryResultToArrayDynamicReturnTypeExtension/data/query-result-to-array.php
29+
30+
-
31+
message: "#^PHPDoc tag @var for variable \\$queryResult contains generic class TYPO3\\\\CMS\\\\Extbase\\\\Persistence\\\\Generic\\\\QueryResult but does not specify its types\\: ModelType$#"
32+
count: 2
33+
path: tests/Unit/Type/QueryResultToArrayDynamicReturnTypeExtension/data/query-result-to-array.php
34+
35+
-
36+
message: "#^Return type \\(TYPO3\\\\CMS\\\\Extbase\\\\Persistence\\\\QueryResultInterface\\) of method SaschaEgerer\\\\PhpstanTypo3\\\\Tests\\\\Unit\\\\Type\\\\QueryResultToArrayDynamicReturnTypeExtension\\\\FrontendUserGroupCustomFindAllWithoutModelTypeRepository\\:\\:findAll\\(\\) should be covariant with return type \\(array\\<int, SaschaEgerer\\\\PhpstanTypo3\\\\Tests\\\\Unit\\\\Type\\\\QueryResultToArrayDynamicReturnTypeExtension\\\\FrontendUserGroup\\>\\|TYPO3\\\\CMS\\\\Extbase\\\\Persistence\\\\QueryResultInterface\\<SaschaEgerer\\\\PhpstanTypo3\\\\Tests\\\\Unit\\\\Type\\\\QueryResultToArrayDynamicReturnTypeExtension\\\\FrontendUserGroup\\>\\) of method TYPO3\\\\CMS\\\\Extbase\\\\Persistence\\\\Repository\\<SaschaEgerer\\\\PhpstanTypo3\\\\Tests\\\\Unit\\\\Type\\\\QueryResultToArrayDynamicReturnTypeExtension\\\\FrontendUserGroup\\>\\:\\:findAll\\(\\)$#"
37+
count: 1
38+
path: tests/Unit/Type/QueryResultToArrayDynamicReturnTypeExtension/data/query-result-to-array.php

src/Type/QueryResultToArrayDynamicReturnTypeExtension.php

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -49,21 +49,14 @@ public function getTypeFromMethodCall(
4949
}
5050

5151
if ($resultType instanceof GenericObjectType) {
52-
$modelType = $resultType->getTypes();
52+
$modelType = $resultType->getTypes()[0] ?? new ErrorType();
5353
} else {
54-
$classReflection = $scope->getClassReflection();
55-
if ($classReflection === null) {
56-
return new ErrorType();
57-
}
58-
59-
$modelName = $this->translateRepositoryNameToModelName(
60-
$classReflection->getName()
61-
);
62-
63-
$modelType = [new ObjectType($modelName)];
54+
$modelType = $methodReflection->getDeclaringClass()
55+
->getPossiblyIncompleteActiveTemplateTypeMap()
56+
->getType('ModelType') ?? new ErrorType();
6457
}
6558

66-
return new ArrayType(new IntegerType(), $modelType[0]);
59+
return new ArrayType(new IntegerType(), $modelType);
6760
}
6861

6962
/**

src/Type/RepositoryFindAllDynamicReturnTypeExtension.php

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,19 @@
44

55
use PhpParser\Node\Expr\MethodCall;
66
use PHPStan\Analyser\Scope;
7+
use PHPStan\PhpDoc\Tag\ExtendsTag;
78
use PHPStan\Reflection\MethodReflection;
89
use PHPStan\Reflection\ParametersAcceptorSelector;
910
use PHPStan\Type\DynamicMethodReturnTypeExtension;
11+
use PHPStan\Type\ErrorType;
1012
use PHPStan\Type\Generic\GenericObjectType;
13+
use PHPStan\Type\Generic\TemplateType;
1114
use PHPStan\Type\ObjectType;
1215
use PHPStan\Type\Type;
16+
use PHPStan\Type\TypeTraverser;
1317
use PHPStan\Type\TypeWithClassName;
1418
use SaschaEgerer\PhpstanTypo3\Helpers\Typo3ClassNamingUtilityTrait;
19+
use TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface;
1520
use TYPO3\CMS\Extbase\Persistence\QueryResultInterface;
1621
use TYPO3\CMS\Extbase\Persistence\Repository;
1722

@@ -39,18 +44,73 @@ public function getTypeFromMethodCall(
3944
): Type
4045
{
4146
$variableType = $scope->getType($methodCall->var);
47+
$methodReturnType = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
48+
if (!$variableType instanceof TypeWithClassName) {
49+
return $methodReturnType;
50+
}
4251

43-
if (!$variableType instanceof TypeWithClassName
44-
|| $methodReflection->getDeclaringClass()->getName() !== Repository::class) {
45-
return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
52+
$methodReturnTypeGeneric = $this->getGenericTypes($methodReturnType)[0] ?? null;
53+
if (
54+
$methodReturnTypeGeneric instanceof GenericObjectType &&
55+
($methodReturnTypeGeneric->getTypes()[0] ?? null) instanceof ObjectType &&
56+
$methodReturnTypeGeneric->getTypes()[0]->getClassName() !== DomainObjectInterface::class
57+
) {
58+
if ($methodReflection->getDeclaringClass()->getName() !== Repository::class) {
59+
return $methodReturnType;
60+
}
61+
return $methodReturnTypeGeneric;
4662
}
4763

4864
/** @var class-string $className */
4965
$className = $variableType->getClassName();
5066

67+
// if we have a custom findAll method...
68+
if ($methodReflection->getDeclaringClass()->getName() !== Repository::class) {
69+
if ($methodReturnType->getIterableValueType() instanceof ObjectType) {
70+
return $methodReturnType;
71+
}
72+
73+
if ($variableType->getClassReflection() !== null) {
74+
$repositoryExtendsTags = $variableType->getClassReflection()->getExtendsTags()[Repository::class] ?? null;
75+
if ($repositoryExtendsTags instanceof ExtendsTag && $repositoryExtendsTags->getType() instanceof GenericObjectType) {
76+
return new GenericObjectType(QueryResultInterface::class, [$repositoryExtendsTags->getType()->getTypes()[0] ?? new ErrorType()]);
77+
}
78+
}
79+
/** @var class-string $className */
80+
$className = $methodReflection->getDeclaringClass()->getName();
81+
}
82+
5183
$modelName = $this->translateRepositoryNameToModelName($className);
5284

5385
return new GenericObjectType(QueryResultInterface::class, [new ObjectType($modelName)]);
5486
}
5587

88+
/**
89+
* @return GenericObjectType[]
90+
*/
91+
private function getGenericTypes(Type $baseType): array
92+
{
93+
$genericObjectTypes = [];
94+
TypeTraverser::map($baseType, static function (Type $type, callable $traverse) use (&$genericObjectTypes): Type {
95+
if ($type instanceof GenericObjectType) {
96+
$resolvedType = TypeTraverser::map($type, static function (Type $type, callable $traverse): Type {
97+
if ($type instanceof TemplateType) {
98+
return $traverse($type->getBound());
99+
}
100+
return $traverse($type);
101+
});
102+
if (!$resolvedType instanceof GenericObjectType) {
103+
throw new \PHPStan\ShouldNotHappenException();
104+
}
105+
$genericObjectTypes[] = $resolvedType;
106+
$traverse($type);
107+
return $type;
108+
}
109+
$traverse($type);
110+
return $type;
111+
});
112+
113+
return $genericObjectTypes;
114+
}
115+
56116
}

tests/Unit/Type/QueryResultToArrayDynamicReturnTypeExtension/data/query-result-to-array.php

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ class FrontendUserGroupRepository extends Repository
2323
/**
2424
* @extends Repository<FrontendUserGroup>
2525
*/
26-
class FrontendUserCustomFindAllGroupRepository extends Repository
26+
class FrontendUserGroupCustomFindAllRepository extends Repository
2727
{
2828

2929
/**
@@ -38,6 +38,36 @@ public function findAll(): QueryResultInterface // phpcs:ignore SlevomatCodingSt
3838

3939
}
4040

41+
/**
42+
* @extends Repository<FrontendUserGroup>
43+
*/
44+
class FrontendUserGroupCustomFindAllWithoutModelTypeRepository extends Repository
45+
{
46+
47+
public function findAll(): QueryResultInterface // phpcs:ignore SlevomatCodingStandard.TypeHints.ReturnTypeHint.MissingAnyTypeHint
48+
{
49+
$queryResult = null; // phpcs:ignore SlevomatCodingStandard.Variables.UselessVariable.UselessVariable
50+
/** @var QueryResult $queryResult */
51+
return $queryResult;
52+
}
53+
54+
}
55+
56+
/**
57+
* @extends Repository<FrontendUserGroup>
58+
*/
59+
class FrontendUserGroupCustomFindAllWithoutAnnotationRepository extends Repository
60+
{
61+
62+
public function findAll() // phpcs:ignore SlevomatCodingStandard.TypeHints.ReturnTypeHint.MissingAnyTypeHint
63+
{
64+
$queryResult = null; // phpcs:ignore SlevomatCodingStandard.Variables.UselessVariable.UselessVariable
65+
/** @var QueryResult $queryResult */
66+
return $queryResult;
67+
}
68+
69+
}
70+
4171
class FrontendUserGroup extends AbstractEntity
4272
{
4373

@@ -49,16 +79,26 @@ class MyController extends ActionController
4979
/** @var FrontendUserGroupRepository */
5080
private $myRepository;
5181

52-
/** @var FrontendUserCustomFindAllGroupRepository */
82+
/** @var FrontendUserGroupCustomFindAllRepository */
5383
private $myCustomFindAllRepository;
5484

85+
/** @var FrontendUserGroupCustomFindAllWithoutModelTypeRepository */
86+
private $myCustomFindAllRepositoryWithoutModelAnnotation;
87+
88+
/** @var FrontendUserGroupCustomFindAllWithoutAnnotationRepository */
89+
private $myCustomFindAllRepositoryWithoutAnnotationRepository;
90+
5591
public function __construct(
5692
FrontendUserGroupRepository $myRepository,
57-
FrontendUserCustomFindAllGroupRepository $myCustomFindAllRepository
93+
FrontendUserGroupCustomFindAllRepository $myCustomFindAllRepository,
94+
FrontendUserGroupCustomFindAllWithoutModelTypeRepository $myCustomFindAllRepositoryWithoutModelAnnotation,
95+
FrontendUserGroupCustomFindAllWithoutAnnotationRepository $myCustomFindAllRepositoryWithoutAnnotationRepository
5896
)
5997
{
6098
$this->myRepository = $myRepository;
6199
$this->myCustomFindAllRepository = $myCustomFindAllRepository;
100+
$this->myCustomFindAllRepositoryWithoutModelAnnotation = $myCustomFindAllRepositoryWithoutModelAnnotation;
101+
$this->myCustomFindAllRepositoryWithoutAnnotationRepository = $myCustomFindAllRepositoryWithoutAnnotationRepository;
62102
}
63103

64104
public function showAction(): void
@@ -75,12 +115,25 @@ public function showAction(): void
75115
$myObjects
76116
);
77117

118+
$queryResult = $this->myCustomFindAllRepository->findAll();
119+
$myObjects = $queryResult->toArray();
78120
assertType(
79121
'array<int, SaschaEgerer\PhpstanTypo3\Tests\Unit\Type\QueryResultToArrayDynamicReturnTypeExtension\FrontendUserGroup>',
80-
$this->myCustomFindAllRepository->findAll()->toArray()
122+
$myObjects
81123
);
82124

83-
$queryResult = $this->myCustomFindAllRepository->findAll();
125+
$queryResult = $this->myCustomFindAllRepositoryWithoutModelAnnotation->findAll();
126+
$myObjects = $queryResult->toArray();
127+
assertType(
128+
'array<int, SaschaEgerer\PhpstanTypo3\Tests\Unit\Type\QueryResultToArrayDynamicReturnTypeExtension\FrontendUserGroup>',
129+
$myObjects
130+
);
131+
132+
$queryResult = $this->myCustomFindAllRepositoryWithoutAnnotationRepository->findAll();
133+
if (is_array($queryResult)) {
134+
return;
135+
}
136+
84137
$myObjects = $queryResult->toArray();
85138
assertType(
86139
'array<int, SaschaEgerer\PhpstanTypo3\Tests\Unit\Type\QueryResultToArrayDynamicReturnTypeExtension\FrontendUserGroup>',

0 commit comments

Comments
 (0)