Skip to content

Commit f7fa157

Browse files
committed
Support entity class return type extension from entity storage methods
1 parent 958911d commit f7fa157

17 files changed

+489
-2
lines changed

README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ parameters:
116116
node: Drupal\node\NodeStorage
117117
taxonomy_term: Drupal\taxonomy\TermStorage
118118
user: Drupal\user\UserStorage
119+
block: Drupal\Core\Config\Entity\ConfigEntityStorage
119120
```
120121

121122
To add support for custom entities, you may add the same definition in your project's `phpstan.neon`. See the following
@@ -128,3 +129,27 @@ parameters:
128129
search_api_index: Drupal\search_api\Entity\SearchApiConfigEntityStorage
129130
search_api_server: Drupal\search_api\Entity\SearchApiConfigEntityStorage
130131
```
132+
133+
Similarly, the `EntityStorageDynamicReturnTypeExtension` service helps to determine the type of the entity which is
134+
loaded, created etc.. when using an entity storage.
135+
For instance when using
136+
137+
```php
138+
$node = \Drupal::entityTypeManager()->getStorage('node')->create(['type' => 'page', 'title' => 'foo']);
139+
```
140+
141+
It helps with knowing the type of the `$node` variable is `Drupal\node\Entity\Node`.
142+
143+
The default mapping can be found in `extension.neon`:
144+
145+
```neon
146+
parameters:
147+
drupal:
148+
entityStorageMapping:
149+
node: Drupal\node\Entity\Node
150+
taxonomy_term: Drupal\taxonomy\Entity\Term
151+
user: Drupal\user\Entity\User
152+
block: Drupal\block\Entity\Block
153+
```
154+
155+
To add support for custom entities, you may add the same definition in your project's `phpstan.neon` likewise.

extension.neon

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,17 @@ parameters:
1717
node: Drupal\node\NodeStorage
1818
taxonomy_term: Drupal\taxonomy\TermStorage
1919
user: Drupal\user\UserStorage
20+
block: Drupal\Core\Config\Entity\ConfigEntityStorage
21+
entityStorageMapping:
22+
node: Drupal\node\Entity\Node
23+
taxonomy_term: Drupal\taxonomy\Entity\Term
24+
user: Drupal\user\Entity\User
25+
block: Drupal\block\Entity\Block
2026
parametersSchema:
2127
drupal: structure([
2228
drupal_root: string()
2329
entityTypeStorageMapping: arrayOf(string())
30+
entityStorageMapping: arrayOf(string())
2431
])
2532
rules:
2633
- mglaman\PHPStanDrupal\Rules\Classes\PluginManagerInspectionRule
@@ -35,8 +42,14 @@ services:
3542
-
3643
class: mglaman\PHPStanDrupal\Type\EntityTypeManagerGetStorageDynamicReturnTypeExtension
3744
arguments:
45+
reflectionProvider: @reflectionProvider
3846
entityTypeStorageMapping: %drupal.entityTypeStorageMapping%
3947
tags: [phpstan.broker.dynamicMethodReturnTypeExtension]
48+
-
49+
class: mglaman\PHPStanDrupal\Type\EntityStorage\EntityStorageDynamicReturnTypeExtension
50+
arguments:
51+
entityStorageMapping: %drupal.entityStorageMapping%
52+
tags: [phpstan.broker.dynamicMethodReturnTypeExtension]
4053
-
4154
class: mglaman\PHPStanDrupal\Type\ContainerDynamicReturnTypeExtension
4255
tags: [phpstan.broker.dynamicMethodReturnTypeExtension]
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace mglaman\PHPStanDrupal\Type\EntityStorage;
6+
7+
final class ConfigEntityStorageType extends EntityStorageType
8+
{
9+
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace mglaman\PHPStanDrupal\Type\EntityStorage;
6+
7+
final class ContentEntityStorageType extends EntityStorageType
8+
{
9+
10+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace mglaman\PHPStanDrupal\Type\EntityStorage;
4+
5+
use Drupal\Core\Entity\EntityStorageInterface;
6+
use PhpParser\Node\Expr\MethodCall;
7+
use PHPStan\Analyser\Scope;
8+
use PHPStan\Reflection\MethodReflection;
9+
use PHPStan\Reflection\ParametersAcceptorSelector;
10+
use PHPStan\Type\ArrayType;
11+
use PHPStan\Type\DynamicMethodReturnTypeExtension;
12+
use PHPStan\Type\IntegerType;
13+
use PHPStan\Type\ObjectType;
14+
use PHPStan\Type\StringType;
15+
use PHPStan\Type\TypeCombinator;
16+
17+
class EntityStorageDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
18+
{
19+
20+
/**
21+
* @var string[]
22+
*/
23+
private $entityStorageMapping;
24+
25+
/**
26+
* EntityStorageDynamicReturnTypeExtension constructor.
27+
*
28+
* @param string[] $entityStorageMapping
29+
*/
30+
public function __construct(
31+
array $entityStorageMapping = []
32+
) {
33+
$this->entityStorageMapping = $entityStorageMapping;
34+
}
35+
36+
public function getClass(): string
37+
{
38+
return EntityStorageInterface::class;
39+
}
40+
41+
public function isMethodSupported(MethodReflection $methodReflection): bool
42+
{
43+
return \in_array(
44+
$methodReflection->getName(),
45+
[
46+
'create',
47+
'load',
48+
'loadMultiple',
49+
'loadByProperties',
50+
'loadUnchanged',
51+
],
52+
true
53+
);
54+
}
55+
56+
public function getTypeFromMethodCall(
57+
MethodReflection $methodReflection,
58+
MethodCall $methodCall,
59+
Scope $scope
60+
): \PHPStan\Type\Type {
61+
$callerType = $scope->getType($methodCall->var);
62+
63+
if (!$callerType instanceof EntityStorageType) {
64+
return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
65+
}
66+
67+
$entityClassName = $this->entityStorageMapping[$callerType->getEntityTypeId()] ?? null;
68+
if (null === $entityClassName) {
69+
return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
70+
}
71+
72+
$type = new ObjectType($entityClassName);
73+
if (\in_array($methodReflection->getName(), ['load', 'loadUnchanged'], true)) {
74+
return TypeCombinator::addNull($type);
75+
}
76+
77+
if (\in_array($methodReflection->getName(), ['loadMultiple', 'loadByProperties'], true)) {
78+
if ($callerType instanceof ConfigEntityStorageType) {
79+
return new ArrayType(new StringType(), $type);
80+
}
81+
82+
return new ArrayType(new IntegerType(), $type);
83+
}
84+
85+
return $type;
86+
}
87+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace mglaman\PHPStanDrupal\Type\EntityStorage;
6+
7+
use PHPStan\Reflection\ClassReflection;
8+
use PHPStan\Type\ObjectType;
9+
use PHPStan\Type\Type;
10+
11+
class EntityStorageType extends ObjectType
12+
{
13+
/**
14+
* @var string
15+
*/
16+
private $entityTypeId;
17+
18+
public function __construct(
19+
string $entityTypeId,
20+
string $className,
21+
?Type $subtractedType = null,
22+
?ClassReflection $classReflection = null
23+
) {
24+
parent::__construct($className, $subtractedType, $classReflection);
25+
26+
$this->entityTypeId = $entityTypeId;
27+
}
28+
29+
public function getEntityTypeId(): string
30+
{
31+
return $this->entityTypeId;
32+
}
33+
}

src/Type/EntityTypeManagerGetStorageDynamicReturnTypeExtension.php

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,30 @@
22

33
namespace mglaman\PHPStanDrupal\Type;
44

5+
use Drupal\Core\Config\Entity\ConfigEntityStorageInterface;
6+
use Drupal\Core\Entity\ContentEntityStorageInterface;
7+
use mglaman\PHPStanDrupal\Type\EntityStorage\ConfigEntityStorageType;
8+
use mglaman\PHPStanDrupal\Type\EntityStorage\ContentEntityStorageType;
9+
use mglaman\PHPStanDrupal\Type\EntityStorage\EntityStorageType;
510
use PhpParser\Node\Expr\BinaryOp\Concat;
611
use PhpParser\Node\Expr\MethodCall;
712
use PhpParser\Node\Scalar\String_;
813
use PhpParser\Node\VariadicPlaceholder;
914
use PHPStan\Analyser\Scope;
1015
use PHPStan\Reflection\MethodReflection;
1116
use PHPStan\Reflection\ParametersAcceptorSelector;
17+
use PHPStan\Reflection\ReflectionProvider;
1218
use PHPStan\ShouldNotHappenException;
1319
use PHPStan\Type\DynamicMethodReturnTypeExtension;
1420
use PHPStan\Type\ObjectType;
1521

1622
class EntityTypeManagerGetStorageDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
1723
{
24+
/**
25+
* @var ReflectionProvider
26+
*/
27+
private $reflectionProvider;
28+
1829
/**
1930
* @var string[]
2031
*/
@@ -23,10 +34,12 @@ class EntityTypeManagerGetStorageDynamicReturnTypeExtension implements DynamicMe
2334
/**
2435
* EntityTypeManagerGetStorageDynamicReturnTypeExtension constructor.
2536
*
37+
* @param ReflectionProvider $reflectionProvider
2638
* @param string[] $entityTypeStorageMapping
2739
*/
28-
public function __construct(array $entityTypeStorageMapping = [])
40+
public function __construct(ReflectionProvider $reflectionProvider, array $entityTypeStorageMapping = [])
2941
{
42+
$this->reflectionProvider = $reflectionProvider;
3043
$this->entityTypeStorageMapping = $entityTypeStorageMapping;
3144
}
3245

@@ -74,10 +87,25 @@ public function getTypeFromMethodCall(
7487
$entityTypeId = $arg1->value;
7588

7689
if (isset($this->entityTypeStorageMapping[$entityTypeId])) {
77-
return new ObjectType($this->entityTypeStorageMapping[$entityTypeId]);
90+
$storageClassName = $this->entityTypeStorageMapping[$entityTypeId];
91+
$interfaces = \array_keys($this->reflectionProvider->getClass($storageClassName)->getInterfaces());
92+
93+
if (\in_array(ConfigEntityStorageInterface::class, $interfaces, true)) {
94+
return new ConfigEntityStorageType($entityTypeId, $storageClassName);
95+
}
96+
97+
if (\in_array(ContentEntityStorageInterface::class, $interfaces, true)) {
98+
return new ContentEntityStorageType($entityTypeId, $storageClassName);
99+
}
100+
101+
return new EntityStorageType($entityTypeId, $storageClassName);
78102
}
103+
79104
// @todo get entity type class reflection and return proper storage for entity type
80105
// example: config storage, sqlcontententitystorage, etc.
106+
if ($returnType instanceof ObjectType) {
107+
return new EntityStorageType($entityTypeId, $returnType->getClassName());
108+
}
81109
return $returnType;
82110
}
83111
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
parameters:
22
drupal:
33
drupal_root: tests/fixtures/drupal
4+
entityTypeStorageMapping:
5+
content_entity_using_custom_storage: Drupal\phpstan_fixtures\CustomContentEntityStorage
6+
config_entity_using_default_storage: Drupal\Core\Config\Entity\ConfigEntityStorage
7+
config_entity_using_custom_storage: Drupal\phpstan_fixtures\CustomConfigEntityStorage
8+
entityStorageMapping:
9+
content_entity_using_default_storage: Drupal\phpstan_fixtures\Entity\ContentEntityUsingDefaultStorage
10+
content_entity_using_custom_storage: Drupal\phpstan_fixtures\Entity\ContentEntityUsingCustomStorage
11+
config_entity_using_default_storage: Drupal\phpstan_fixtures\Entity\ConfigEntityUsingDefaultStorage
12+
config_entity_using_custom_storage: Drupal\phpstan_fixtures\Entity\ConfigEntityUsingCustomStorage
413
includes:
514
- ../../../extension.neon
615
- ../../../vendor/phpstan/phpstan-deprecation-rules/rules.neon
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Drupal\phpstan_fixtures;
6+
7+
use Drupal\Core\Config\Entity\ConfigEntityStorage;
8+
9+
final class CustomConfigEntityStorage extends ConfigEntityStorage {
10+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Drupal\phpstan_fixtures;
6+
7+
use Drupal\Core\Entity\ContentEntityInterface;
8+
use Drupal\Core\Entity\ContentEntityStorageBase;
9+
use Drupal\Core\Entity\EntityInterface;
10+
use Drupal\Core\Field\FieldDefinitionInterface;
11+
12+
final class CustomContentEntityStorage extends ContentEntityStorageBase {
13+
protected function readFieldItemsToPurge(FieldDefinitionInterface $field_definition, $batch_size) {}
14+
protected function purgeFieldItems(ContentEntityInterface $entity, FieldDefinitionInterface $field_definition) {}
15+
protected function doLoadRevisionFieldItems($revision_id) {}
16+
protected function doLoadMultipleRevisionsFieldItems($revision_ids) {}
17+
protected function doSaveFieldItems(ContentEntityInterface $entity, array $names = []) {}
18+
protected function doDeleteFieldItems($entities) {}
19+
protected function doDeleteRevisionFieldItems(ContentEntityInterface $revision) {}
20+
protected function doLoadMultiple(array $ids = null) {}
21+
protected function has($id, EntityInterface $entity) {}
22+
protected function getQueryServiceName() {}
23+
public function countFieldData($storage_definition, $as_bool = false) {}
24+
}

0 commit comments

Comments
 (0)