Skip to content

Commit 582a3b0

Browse files
MK-42sascha-egerer
authored andcommitted
Introduce QueryInterface as a generic type
Also implement some ReflectionExtensions to infer the model types from repository names
1 parent 1b159ea commit 582a3b0

8 files changed

+177
-7
lines changed

extension.neon

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@ services:
3131
contextApiGetAspectMapping: %typo3.contextApiGetAspectMapping%
3232
tags:
3333
- phpstan.rules.rule
34+
-
35+
class: SaschaEgerer\PhpstanTypo3\Type\RepositoryQueryDynamicReturnTypeExtension
36+
tags:
37+
- phpstan.broker.dynamicMethodReturnTypeExtension
38+
-
39+
class: SaschaEgerer\PhpstanTypo3\Type\RepositoryFindAllDynamicReturnTypeExtension
40+
tags:
41+
- phpstan.broker.dynamicMethodReturnTypeExtension
3442
parameters:
3543
bootstrapFiles:
3644
- phpstan.bootstrap.php
@@ -46,6 +54,9 @@ parameters:
4654
stubFiles:
4755
- stubs/GeneralUtility.stub
4856
- stubs/ObjectStorage.stub
57+
- stubs/QueryInterface.stub
58+
- stubs/QueryResultInterface.stub
59+
- stubs/QueryResult.stub
4960
dynamicConstantNames:
5061
- TYPO3_MODE
5162
- TYPO3_REQUESTTYPE

src/Reflection/RepositoryMethodsClassReflectionExtension.php

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use PHPStan\Reflection\ClassReflection;
88
use PHPStan\Reflection\MethodReflection;
99
use PHPStan\Reflection\MethodsClassReflectionExtension;
10+
use TYPO3\CMS\Core\Utility\ClassNamingUtility;
1011
use TYPO3\CMS\Extbase\Persistence\Repository;
1112

1213
class RepositoryMethodsClassReflectionExtension implements MethodsClassReflectionExtension, BrokerAwareExtension
@@ -26,7 +27,24 @@ public function hasMethod(ClassReflection $classReflection, string $methodName):
2627
!$classReflection->hasNativeMethod($methodName)
2728
&& in_array(Repository::class, $classReflection->getParentClassesNames(), true)
2829
) {
29-
return strpos($methodName, 'findBy') === 0 || strpos($methodName, 'findOneBy') === 0;
30+
if (strpos($methodName, 'findOneBy') === 0) {
31+
$propertyName = lcfirst(substr($methodName, 9));
32+
} elseif (strpos($methodName, 'findBy') === 0) {
33+
$propertyName = lcfirst(substr($methodName, 6));
34+
} else {
35+
return false;
36+
}
37+
38+
// ensure that a property with that name exists on the model, as there might
39+
// be methods starting with find[One]By... with custom implementations on
40+
// inherited repositories
41+
$className = $classReflection->getName();
42+
$modelName = ClassNamingUtility::translateRepositoryNameToModelName($className);
43+
44+
$modelReflection = $this->broker->getClass($modelName);
45+
if ($modelReflection->hasProperty($propertyName)) {
46+
return true;
47+
}
3048
}
3149
return false;
3250
}

src/Type/QueryInterfaceDynamicReturnTypeExtension.php

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use PHPStan\Type\ArrayType;
99
use PHPStan\Type\Constant\ConstantBooleanType;
1010
use PHPStan\Type\DynamicMethodReturnTypeExtension;
11+
use PHPStan\Type\Generic\GenericObjectType;
1112
use PHPStan\Type\IntegerType;
1213
use PHPStan\Type\ObjectType;
1314
use PHPStan\Type\Type;
@@ -38,20 +39,20 @@ public function getTypeFromMethodCall(
3839
{
3940
$argument = $methodCall->getArgs()[0] ?? null;
4041

42+
$classReflection = $scope->getClassReflection();
43+
$modelName = ClassNamingUtility::translateRepositoryNameToModelName(
44+
$classReflection->getName()
45+
);
46+
4147
if ($argument !== null) {
4248
$argType = $scope->getType($argument->value);
43-
$classReflection = $scope->getClassReflection();
4449

4550
if ($classReflection !== null && $argType instanceof ConstantBooleanType && $argType->getValue() === true) {
46-
$modelName = ClassNamingUtility::translateRepositoryNameToModelName(
47-
$classReflection->getName()
48-
);
49-
5051
return new ArrayType(new IntegerType(), new ObjectType($modelName));
5152
}
5253
}
5354

54-
return new ObjectType(QueryResult::class);
55+
return new GenericObjectType(QueryResult::class, [new ObjectType($modelName)]);
5556
}
5657

5758
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace SaschaEgerer\PhpstanTypo3\Type;
4+
5+
use PhpParser\Node\Expr\MethodCall;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Reflection\MethodReflection;
8+
use PHPStan\Reflection\ParametersAcceptorSelector;
9+
use PHPStan\Type\DynamicMethodReturnTypeExtension;
10+
use PHPStan\Type\Generic\GenericObjectType;
11+
use PHPStan\Type\ObjectType;
12+
use PHPStan\Type\Type;
13+
use PHPStan\Type\TypeCombinator;
14+
use TYPO3\CMS\Core\Utility\ClassNamingUtility;
15+
use TYPO3\CMS\Extbase\Persistence\QueryResultInterface;
16+
17+
class RepositoryFindAllDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
18+
{
19+
20+
public function getClass(): string
21+
{
22+
return \TYPO3\CMS\Extbase\Persistence\RepositoryInterface::class;
23+
}
24+
25+
public function isMethodSupported(
26+
MethodReflection $methodReflection
27+
): bool
28+
{
29+
return in_array($methodReflection->getName(), ['findAll'], true);
30+
}
31+
32+
public function getTypeFromMethodCall(
33+
MethodReflection $methodReflection,
34+
MethodCall $methodCall,
35+
Scope $scope
36+
): Type
37+
{
38+
$variableType = $scope->getType($methodCall->var);
39+
40+
if (!($variableType instanceof ObjectType) || !is_subclass_of($variableType->getClassName(), $this->getClass())) {
41+
return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
42+
}
43+
44+
$modelName = ClassNamingUtility::translateRepositoryNameToModelName($variableType->getClassName());
45+
46+
return new GenericObjectType(QueryResultInterface::class, [new ObjectType($modelName)]);
47+
}
48+
49+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace SaschaEgerer\PhpstanTypo3\Type;
4+
5+
use PhpParser\Node\Expr\MethodCall;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Reflection\MethodReflection;
8+
use PHPStan\Reflection\ParametersAcceptorSelector;
9+
use PHPStan\Type\DynamicMethodReturnTypeExtension;
10+
use PHPStan\Type\Generic\GenericObjectType;
11+
use PHPStan\Type\ObjectType;
12+
use PHPStan\Type\Type;
13+
use PHPStan\Type\TypeCombinator;
14+
use TYPO3\CMS\Core\Utility\ClassNamingUtility;
15+
use TYPO3\CMS\Extbase\Persistence\QueryInterface;
16+
17+
class RepositoryQueryDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
18+
{
19+
20+
public function getClass(): string
21+
{
22+
return \TYPO3\CMS\Extbase\Persistence\RepositoryInterface::class;
23+
}
24+
25+
public function isMethodSupported(
26+
MethodReflection $methodReflection
27+
): bool
28+
{
29+
return $methodReflection->getName() === 'createQuery';
30+
}
31+
32+
public function getTypeFromMethodCall(
33+
MethodReflection $methodReflection,
34+
MethodCall $methodCall,
35+
Scope $scope
36+
): Type
37+
{
38+
$variableType = $scope->getType($methodCall->var);
39+
40+
$modelName = ClassNamingUtility::translateRepositoryNameToModelName($variableType->getClassName());
41+
42+
return new GenericObjectType(QueryInterface::class, [new ObjectType($modelName)]);
43+
}
44+
45+
}

stubs/QueryInterface.stub

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
namespace TYPO3\CMS\Extbase\Persistence;
3+
4+
/**
5+
* @template ModelType
6+
*/
7+
interface QueryInterface
8+
{
9+
/**
10+
* @param bool $returnRawQueryResult
11+
* @return \TYPO3\CMS\Extbase\Persistence\QueryResultInterface<ModelType>|array<string, mixed>
12+
*/
13+
public function execute($returnRawQueryResult = false);
14+
15+
/**
16+
* @param mixed $constraint
17+
* @return \TYPO3\CMS\Extbase\Persistence\QueryInterface<ModelType>
18+
*/
19+
public function matching($constraint);
20+
}

stubs/QueryResult.stub

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
namespace TYPO3\CMS\Extbase\Persistence\Generic;
3+
4+
/**
5+
* @template ModelType
6+
*/
7+
class QueryResult
8+
{
9+
/**
10+
* @return ModelType
11+
*/
12+
public function getFirst();
13+
}

stubs/QueryResultInterface.stub

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
namespace TYPO3\CMS\Extbase\Persistence;
3+
4+
/**
5+
* @template ModelType
6+
*/
7+
interface QueryResultInterface
8+
{
9+
/**
10+
* @return ModelType
11+
*/
12+
public function getFirst();
13+
}

0 commit comments

Comments
 (0)