Skip to content

Commit dfcef3d

Browse files
authored
Proper return type for config entity queries (#309)
* Proper return type for config entity queries * Use specific type classes for entity query type * Keep entity query return type restrict to count/execute * Delete old EntityQueryType
1 parent 74b356d commit dfcef3d

8 files changed

+193
-5
lines changed

extension.neon

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,9 @@ services:
284284
-
285285
class: mglaman\PHPStanDrupal\Type\DrupalServiceDynamicReturnTypeExtension
286286
tags: [phpstan.broker.dynamicStaticMethodReturnTypeExtension]
287+
-
288+
class: mglaman\PHPStanDrupal\Type\DrupalStaticEntityQueryDynamicReturnTypeExtension
289+
tags: [phpstan.broker.dynamicStaticMethodReturnTypeExtension]
287290
-
288291
class: mglaman\PHPStanDrupal\Reflection\EntityFieldsViaMagicReflectionExtension
289292
tags: [phpstan.broker.propertiesClassReflectionExtension]

src/Drupal/EntityData.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public function getClassType(): ?ObjectType
4242
return $this->className === null ? null : new ObjectType($this->className);
4343
}
4444

45-
public function getStorageType(): ?ObjectType
45+
public function getStorageType(): ?EntityStorageType
4646
{
4747
if ($this->storageClassName === null) {
4848
$classType = $this->getClassType();
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace mglaman\PHPStanDrupal\Type;
4+
5+
use mglaman\PHPStanDrupal\Drupal\EntityDataRepository;
6+
use mglaman\PHPStanDrupal\Type\EntityQuery\ConfigEntityQueryType;
7+
use mglaman\PHPStanDrupal\Type\EntityQuery\ContentEntityQueryType;
8+
use mglaman\PHPStanDrupal\Type\EntityStorage\ConfigEntityStorageType;
9+
use mglaman\PHPStanDrupal\Type\EntityStorage\ContentEntityStorageType;
10+
use PhpParser\Node\Expr\StaticCall;
11+
use PHPStan\Analyser\Scope;
12+
use PHPStan\Reflection\MethodReflection;
13+
use PHPStan\Reflection\ParametersAcceptorSelector;
14+
use PHPStan\Type\Constant\ConstantStringType;
15+
use PHPStan\Type\DynamicStaticMethodReturnTypeExtension;
16+
use PHPStan\Type\ObjectType;
17+
use PHPStan\Type\Type;
18+
19+
class DrupalStaticEntityQueryDynamicReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension
20+
{
21+
22+
/**
23+
* @var EntityDataRepository
24+
*/
25+
private $entityDataRepository;
26+
27+
public function __construct(EntityDataRepository $entityDataRepository)
28+
{
29+
$this->entityDataRepository = $entityDataRepository;
30+
}
31+
32+
public function getClass(): string
33+
{
34+
return \Drupal::class;
35+
}
36+
37+
public function isStaticMethodSupported(MethodReflection $methodReflection): bool
38+
{
39+
return $methodReflection->getName() === 'entityQuery';
40+
}
41+
42+
public function getTypeFromStaticMethodCall(
43+
MethodReflection $methodReflection,
44+
StaticCall $methodCall,
45+
Scope $scope
46+
): Type {
47+
$returnType = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
48+
if (!$returnType instanceof ObjectType) {
49+
return $returnType;
50+
}
51+
$args = $methodCall->getArgs();
52+
if (count($args) !== 1) {
53+
return $returnType;
54+
}
55+
$type = $scope->getType($args[0]->value);
56+
if ($type instanceof ConstantStringType) {
57+
$entityTypeId = $type->getValue();
58+
} else {
59+
// @todo determine what these types are, and try to resolve entity name from.
60+
return $returnType;
61+
}
62+
$entityType = $this->entityDataRepository->get($entityTypeId);
63+
$entityStorageType = $entityType->getStorageType();
64+
if ($entityStorageType === null) {
65+
return $returnType;
66+
}
67+
68+
if ($entityStorageType instanceof ContentEntityStorageType) {
69+
return new ContentEntityQueryType(
70+
$returnType->getClassName(),
71+
$returnType->getSubtractedType(),
72+
$returnType->getClassReflection()
73+
);
74+
}
75+
if ($entityStorageType instanceof ConfigEntityStorageType) {
76+
return new ConfigEntityQueryType(
77+
$returnType->getClassName(),
78+
$returnType->getSubtractedType(),
79+
$returnType->getClassReflection()
80+
);
81+
}
82+
83+
return $returnType;
84+
}
85+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace mglaman\PHPStanDrupal\Type\EntityQuery;
4+
5+
use PHPStan\Type\ObjectType;
6+
7+
/**
8+
* Type used to represent an entity query instance for config entity query.
9+
*/
10+
final class ConfigEntityQueryType extends ObjectType
11+
{
12+
13+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace mglaman\PHPStanDrupal\Type\EntityQuery;
4+
5+
use PHPStan\Type\ObjectType;
6+
7+
/**
8+
* Type used to represent an entity query instance for content entity query.
9+
*/
10+
final class ContentEntityQueryType extends ObjectType
11+
{
12+
13+
}

src/Type/EntityQuery/EntityQueryDynamicReturnTypeExtension.php

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ public function getTypeFromMethodCall(
3838
$defaultReturnType = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
3939
$varType = $scope->getType($methodCall->var);
4040
$methodName = $methodReflection->getName();
41+
4142
if ($methodName === 'count') {
4243
if ($varType instanceof ObjectType) {
4344
return new EntityQueryCountType(
@@ -53,14 +54,15 @@ public function getTypeFromMethodCall(
5354
if ($varType instanceof EntityQueryCountType) {
5455
return new IntegerType();
5556
}
56-
if ($varType instanceof ObjectType) {
57-
// @todo if this is a config storage, it'd string keys.
58-
// revisit after https://github.com/mglaman/phpstan-drupal/pull/239
59-
// then we can check what kind of storage we have.
57+
if ($varType instanceof ConfigEntityQueryType) {
58+
return new ArrayType(new StringType(), new StringType());
59+
}
60+
if ($varType instanceof ContentEntityQueryType) {
6061
return new ArrayType(new IntegerType(), new StringType());
6162
}
6263
return $defaultReturnType;
6364
}
65+
6466
return $defaultReturnType;
6567
}
6668
}

src/Type/EntityStorage/EntityStorageDynamicReturnTypeExtension.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@
44

55
use Drupal\Core\Entity\EntityStorageInterface;
66
use mglaman\PHPStanDrupal\Drupal\EntityDataRepository;
7+
use mglaman\PHPStanDrupal\Type\EntityQuery\ConfigEntityQueryType;
8+
use mglaman\PHPStanDrupal\Type\EntityQuery\ContentEntityQueryType;
79
use PhpParser\Node\Expr\MethodCall;
810
use PHPStan\Analyser\Scope;
911
use PHPStan\Reflection\MethodReflection;
1012
use PHPStan\Reflection\ParametersAcceptorSelector;
13+
use PHPStan\ShouldNotHappenException;
1114
use PHPStan\Type\ArrayType;
1215
use PHPStan\Type\DynamicMethodReturnTypeExtension;
1316
use PHPStan\Type\IntegerType;
@@ -43,6 +46,7 @@ public function isMethodSupported(MethodReflection $methodReflection): bool
4346
'loadMultiple',
4447
'loadByProperties',
4548
'loadUnchanged',
49+
'getQuery',
4650
],
4751
true
4852
);
@@ -74,6 +78,27 @@ public function getTypeFromMethodCall(
7478

7579
return new ArrayType(new IntegerType(), $type);
7680
}
81+
if ($methodReflection->getName() === 'getQuery') {
82+
$returnType = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
83+
if (!$returnType instanceof ObjectType) {
84+
return $returnType;
85+
}
86+
if ($callerType instanceof ContentEntityStorageType) {
87+
return new ContentEntityQueryType(
88+
$returnType->getClassName(),
89+
$returnType->getSubtractedType(),
90+
$returnType->getClassReflection()
91+
);
92+
}
93+
if ($callerType instanceof ConfigEntityStorageType) {
94+
return new ConfigEntityQueryType(
95+
$returnType->getClassName(),
96+
$returnType->getSubtractedType(),
97+
$returnType->getClassReflection()
98+
);
99+
}
100+
return $returnType;
101+
}
77102

78103
return $type;
79104
}

tests/src/Type/data/entity-query-execute.php

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@
2121
->count()
2222
->execute()
2323
);
24+
assertType(
25+
'int',
26+
\Drupal::entityTypeManager()->getStorage('node')->getQuery()
27+
->count()
28+
->condition('foo', 'bar')
29+
->accessCheck(TRUE)
30+
->execute()
31+
);
2432
assertType(
2533
'int',
2634
\Drupal::entityQuery('node')
@@ -41,3 +49,42 @@
4149
$query = \Drupal::entityTypeManager()->getStorage('node')->getQuery()
4250
->accessCheck(TRUE)->count();
4351
assertType('int', $query->execute());
52+
53+
assertType(
54+
'array<string, string>',
55+
\Drupal::entityTypeManager()->getStorage('block')->getQuery()
56+
->execute()
57+
);
58+
assertType(
59+
'array<string, string>',
60+
\Drupal::entityTypeManager()->getStorage('block')->getQuery()
61+
->accessCheck(TRUE)
62+
->execute()
63+
);
64+
assertType(
65+
'int',
66+
\Drupal::entityTypeManager()->getStorage('block')->getQuery()
67+
->accessCheck(TRUE)
68+
->count()
69+
->execute()
70+
);
71+
assertType(
72+
'int',
73+
\Drupal::entityQuery('block')
74+
->accessCheck(TRUE)
75+
->count()
76+
->execute()
77+
);
78+
assertType(
79+
'array<string, string>',
80+
\Drupal::entityQuery('block')
81+
->accessCheck(TRUE)
82+
->execute()
83+
);
84+
85+
$query = \Drupal::entityTypeManager()->getStorage('block')->getQuery()
86+
->accessCheck(TRUE);
87+
assertType('array<string, string>', $query->execute());
88+
$query = \Drupal::entityTypeManager()->getStorage('block')->getQuery()
89+
->accessCheck(TRUE)->count();
90+
assertType('int', $query->execute());

0 commit comments

Comments
 (0)