Skip to content

Commit abfcb51

Browse files
committed
Allow resolving entity type based on type hinted storage
1 parent a77ca90 commit abfcb51

8 files changed

+159
-155
lines changed

src/Drupal/EntityDataRepository.php

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,44 @@
22

33
namespace mglaman\PHPStanDrupal\Drupal;
44

5-
use PHPStan\Reflection\ReflectionProvider;
5+
use PHPStan\Type\ObjectType;
66

77
final class EntityDataRepository
88
{
9-
10-
/**
11-
* @var array<string, array<string, string>>
12-
*/
13-
private $entityMapping;
149
/**
15-
* @var array<string, EntityData|null>
10+
* @var array<string, EntityData>
1611
*/
1712
private $entityData;
1813

1914
public function __construct(array $entityMapping)
2015
{
21-
$this->entityMapping = $entityMapping;
16+
foreach ($entityMapping as $entityTypeId => $entityData) {
17+
$this->entityData[$entityTypeId] = new EntityData(
18+
$entityTypeId,
19+
$entityData
20+
);
21+
}
2222
}
2323

2424
public function get(string $entityTypeId): EntityData
2525
{
2626
if (!isset($this->entityData[$entityTypeId])) {
2727
$this->entityData[$entityTypeId] = new EntityData(
2828
$entityTypeId,
29-
$this->entityMapping[$entityTypeId] ?? []
29+
[]
3030
);
3131
}
3232
return $this->entityData[$entityTypeId];
3333
}
34+
35+
public function resolveFromStorage(ObjectType $callerType): ?EntityData
36+
{
37+
foreach ($this->entityData as $entityData) {
38+
$storageType = $entityData->getStorageType();
39+
if ($storageType !== null && $callerType->isSuperTypeOf($storageType)->yes()) {
40+
return $entityData;
41+
}
42+
}
43+
return null;
44+
}
3445
}

src/Type/EntityStorage/EntityStorageDynamicReturnTypeExtension.php

Lines changed: 36 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace mglaman\PHPStanDrupal\Type\EntityStorage;
44

5+
use Drupal\Core\Config\Entity\ConfigEntityStorageInterface;
6+
use Drupal\Core\Entity\ContentEntityStorageInterface;
57
use Drupal\Core\Entity\EntityStorageInterface;
68
use mglaman\PHPStanDrupal\Drupal\EntityDataRepository;
79
use mglaman\PHPStanDrupal\Type\EntityQuery\ConfigEntityQueryType;
@@ -59,24 +61,43 @@ public function getTypeFromMethodCall(
5961
Scope $scope
6062
): \PHPStan\Type\Type {
6163
$callerType = $scope->getType($methodCall->var);
64+
if (!$callerType instanceof ObjectType) {
65+
return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
66+
}
6267

63-
if (!$callerType instanceof EntityStorageType) {
64-
if (!$callerType instanceof ObjectType) {
65-
return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
68+
if ($methodReflection->getName() === 'getQuery') {
69+
$returnType = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
70+
if (!$returnType instanceof ObjectType) {
71+
return $returnType;
6672
}
6773

68-
// Workaround because cannot figure out why the caller type is not an EntityStorageType
69-
// when it has been type hinted.
70-
// Instead, we try to infer the type, i.e. ContentEntityStorageType or ConfigEntityStorageType, here.
71-
// @todo: we should definitively look for other cases that getQuery.
72-
if ($methodReflection->getName() === 'getQuery') {
73-
return $this->getReturnTypeForGetQueryMethod($methodReflection, EntityStorageHelper::getTypeFromStorageObjectType($callerType));
74+
if ((new ObjectType(ContentEntityStorageInterface::class))->isSuperTypeOf($callerType)->yes()) {
75+
return new ContentEntityQueryType(
76+
$returnType->getClassName(),
77+
$returnType->getSubtractedType(),
78+
$returnType->getClassReflection()
79+
);
80+
}
81+
if ((new ObjectType(ConfigEntityStorageInterface::class))->isSuperTypeOf($callerType)->yes()) {
82+
return new ConfigEntityQueryType(
83+
$returnType->getClassName(),
84+
$returnType->getSubtractedType(),
85+
$returnType->getClassReflection()
86+
);
7487
}
88+
return $returnType;
89+
}
7590

76-
return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
91+
if (!$callerType instanceof EntityStorageType) {
92+
$resolvedEntityType = $this->entityDataRepository->resolveFromStorage($callerType);
93+
if ($resolvedEntityType === null) {
94+
return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
95+
}
96+
$type = $resolvedEntityType->getClassType();
97+
} else {
98+
$type = $this->entityDataRepository->get($callerType->getEntityTypeId())->getClassType();
7799
}
78100

79-
$type = $this->entityDataRepository->get($callerType->getEntityTypeId())->getClassType();
80101
if ($type === null) {
81102
return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
82103
}
@@ -91,33 +112,11 @@ public function getTypeFromMethodCall(
91112

92113
return new ArrayType(new IntegerType(), $type);
93114
}
94-
if ($methodReflection->getName() === 'getQuery') {
95-
return $this->getReturnTypeForGetQueryMethod($methodReflection, $callerType);
96-
}
97-
98-
return $type;
99-
}
100115

101-
private function getReturnTypeForGetQueryMethod(MethodReflection $methodReflection, ?Type $entityStorageType): Type
102-
{
103-
$returnType = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
104-
if (!$returnType instanceof ObjectType) {
105-
return $returnType;
106-
}
107-
if ($entityStorageType instanceof ContentEntityStorageType) {
108-
return new ContentEntityQueryType(
109-
$returnType->getClassName(),
110-
$returnType->getSubtractedType(),
111-
$returnType->getClassReflection()
112-
);
116+
if ($methodReflection->getName() === 'create') {
117+
return $type;
113118
}
114-
if ($entityStorageType instanceof ConfigEntityStorageType) {
115-
return new ConfigEntityQueryType(
116-
$returnType->getClassName(),
117-
$returnType->getSubtractedType(),
118-
$returnType->getClassReflection()
119-
);
120-
}
121-
return $returnType;
119+
120+
return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
122121
}
123122
}

src/Type/EntityStorage/EntityStorageHelper.php

Lines changed: 0 additions & 39 deletions
This file was deleted.

tests/src/Type/EntityDynamicReturnTypeTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public function dataFileAsserts(): iterable
1515
{
1616
yield from $this->gatherAssertTypes(__DIR__ . '/data/entity.php');
1717
yield from $this->gatherAssertTypes(__DIR__ . '/data/entity-storage.php');
18+
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-355-entity-storage.php');
1819
}
1920

2021
/**

tests/src/Type/EntityQuery/EntityQueryDynamicReturnTypeExtensionTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ final class EntityQueryDynamicReturnTypeExtensionTest extends TypeInferenceTestC
1414
public function dataFileAsserts(): iterable
1515
{
1616
yield from $this->gatherAssertTypes(__DIR__ . '/../data/entity-query-execute.php');
17+
yield from $this->gatherAssertTypes(__DIR__ . '/../data/bug-355-entity-query.php');
1718
}
1819

1920
/**
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<?php
2+
3+
namespace DrupalEntity;
4+
5+
use Drupal\node\NodeStorage;
6+
use function PHPStan\Testing\assertType;
7+
8+
/** @var \Drupal\node\NodeStorage $typedNodeStorage */
9+
$typedNodeStorage = \Drupal::entityTypeManager()->getStorage('node');
10+
assertType(
11+
'array<int, string>',
12+
$typedNodeStorage->getQuery()
13+
->accessCheck(TRUE)
14+
->execute()
15+
);
16+
$query = $typedNodeStorage->getQuery()
17+
->accessCheck(TRUE);
18+
assertType('array<int, string>', $query->execute());
19+
assertType(
20+
'int',
21+
$typedNodeStorage->getQuery()
22+
->accessCheck(TRUE)
23+
->count()
24+
->execute()
25+
);
26+
$query = $typedNodeStorage->getQuery()
27+
->accessCheck(TRUE)
28+
->count();
29+
assertType('int', $query->execute());
30+
31+
/** @var \Drupal\node\NodeStorageInterface $anotherTypedNodeStorage */
32+
$anotherTypedNodeStorage = \Drupal::entityTypeManager()->getStorage('node');
33+
assertType(
34+
'array<int, string>',
35+
$anotherTypedNodeStorage->getQuery()
36+
->accessCheck(TRUE)
37+
->execute()
38+
);
39+
$query = $anotherTypedNodeStorage->getQuery()
40+
->accessCheck(TRUE);
41+
assertType('array<int, string>', $query->execute());
42+
assertType(
43+
'int',
44+
$anotherTypedNodeStorage->getQuery()
45+
->accessCheck(TRUE)
46+
->count()
47+
->execute()
48+
);
49+
$query = $anotherTypedNodeStorage->getQuery()
50+
->accessCheck(TRUE)
51+
->count();
52+
assertType('int', $query->execute());
53+
54+
$instanceOfNodeStorage = \Drupal::entityTypeManager()->getStorage('node');
55+
if ($instanceOfNodeStorage instanceof NodeStorage) {
56+
assertType(
57+
'array<int, string>',
58+
$instanceOfNodeStorage->getQuery()
59+
->accessCheck(TRUE)
60+
->execute()
61+
);
62+
$query = $instanceOfNodeStorage->getQuery()
63+
->accessCheck(TRUE);
64+
assertType('array<int, string>', $query->execute());
65+
assertType(
66+
'int',
67+
$instanceOfNodeStorage->getQuery()
68+
->accessCheck(TRUE)
69+
->count()
70+
->execute()
71+
);
72+
$query = $instanceOfNodeStorage->getQuery()
73+
->accessCheck(TRUE)
74+
->count();
75+
assertType('int', $query->execute());
76+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
namespace DrupalEntity;
4+
5+
use Drupal\node\NodeStorageInterface;
6+
use function PHPStan\Testing\assertType;
7+
8+
$nodeStorage = \Drupal::entityTypeManager()->getStorage('node');
9+
if ($nodeStorage instanceof NodeStorageInterface) {
10+
assertType('Drupal\node\Entity\Node', $nodeStorage->create(['type' => 'page', 'title' => 'foo']));
11+
assertType('Drupal\node\Entity\Node|null', $nodeStorage->load(42));
12+
assertType('array<int, Drupal\node\Entity\Node>', $nodeStorage->loadMultiple([42, 29]));
13+
}
14+
15+
/** @var \Drupal\node\NodeStorage $typedNodeStorage */
16+
$typedNodeStorage = \Drupal::entityTypeManager()->getStorage('node');
17+
assertType('Drupal\node\Entity\Node', $typedNodeStorage->create(['type' => 'page', 'title' => 'foo']));
18+
assertType('Drupal\node\Entity\Node|null', $typedNodeStorage->load(42));
19+
assertType('array<int, Drupal\node\Entity\Node>', $typedNodeStorage->loadMultiple([42, 29]));
20+
21+
/** @var \Drupal\taxonomy\TermStorageInterface $termStorage */
22+
$termStorage = \Drupal::entityTypeManager()->getStorage('taxonomy_term');
23+
assertType('Drupal\taxonomy\Entity\Term', $termStorage->create(['vid' => 'tag', 'name' => 'foo']));
24+
assertType('Drupal\taxonomy\Entity\Term|null', $termStorage->load('42'));
25+
assertType('array<int, Drupal\taxonomy\Entity\Term>', $termStorage->loadMultiple([42, 29]));

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

Lines changed: 0 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
namespace DrupalEntity;
44

5-
use Drupal\node\NodeStorage;
65
use function PHPStan\Testing\assertType;
76
assertType(
87
'array<int, string>',
@@ -51,75 +50,6 @@
5150
->accessCheck(TRUE)->count();
5251
assertType('int', $query->execute());
5352

54-
$nodeStorage = \Drupal::entityTypeManager()->getStorage('node');
55-
assertType(
56-
'array<int, string>',
57-
$nodeStorage->getQuery()
58-
->accessCheck(TRUE)
59-
->execute()
60-
);
61-
$query = $nodeStorage->getQuery()
62-
->accessCheck(TRUE);
63-
assertType('array<int, string>', $query->execute());
64-
assertType(
65-
'int',
66-
$nodeStorage->getQuery()
67-
->accessCheck(TRUE)
68-
->count()
69-
->execute()
70-
);
71-
$query = $nodeStorage->getQuery()
72-
->accessCheck(TRUE)
73-
->count();
74-
assertType('int', $query->execute());
75-
76-
/** @var \Drupal\node\NodeStorage $typedNodeStorage */
77-
$typedNodeStorage = \Drupal::entityTypeManager()->getStorage('node');
78-
assertType(
79-
'array<int, string>',
80-
$typedNodeStorage->getQuery()
81-
->accessCheck(TRUE)
82-
->execute()
83-
);
84-
$query = $typedNodeStorage->getQuery()
85-
->accessCheck(TRUE);
86-
assertType('array<int, string>', $query->execute());
87-
assertType(
88-
'int',
89-
$typedNodeStorage->getQuery()
90-
->accessCheck(TRUE)
91-
->count()
92-
->execute()
93-
);
94-
$query = $typedNodeStorage->getQuery()
95-
->accessCheck(TRUE)
96-
->count();
97-
assertType('int', $query->execute());
98-
99-
$anotherTypedNodeStorage = \Drupal::entityTypeManager()->getStorage('node');
100-
if ($anotherTypedNodeStorage instanceof NodeStorage) {
101-
assertType(
102-
'array<int, string>',
103-
$anotherTypedNodeStorage->getQuery()
104-
->accessCheck(TRUE)
105-
->execute()
106-
);
107-
$query = $anotherTypedNodeStorage->getQuery()
108-
->accessCheck(TRUE);
109-
assertType('array<int, string>', $query->execute());
110-
assertType(
111-
'int',
112-
$anotherTypedNodeStorage->getQuery()
113-
->accessCheck(TRUE)
114-
->count()
115-
->execute()
116-
);
117-
$query = $anotherTypedNodeStorage->getQuery()
118-
->accessCheck(TRUE)
119-
->count();
120-
assertType('int', $query->execute());
121-
}
122-
12353
assertType(
12454
'array<string, string>',
12555
\Drupal::entityTypeManager()->getStorage('block')->getQuery()

0 commit comments

Comments
 (0)