Skip to content

Commit 5a7d06c

Browse files
committed
Provide a dynamic return type for EntityTypeManager::getStorage
Fixes #6
1 parent 752507b commit 5a7d06c

File tree

4 files changed

+109
-1
lines changed

4 files changed

+109
-1
lines changed

README.md

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ includes:
1717

1818
## Enabling rules one-by-one
1919

20-
If you don't want to start using all the available strict rules at once but only one or two, you can! Just don't include the whole `rules.neon` from this package in your configuration, but look at its contents and copy only the rules you want to your configuration under the `services` key:
20+
If you don't want to start using all the available strict rules at once but only one or two, you can! Just don't include
21+
the whole `rules.neon` from this package in your configuration, but look at its contents and copy only the rules you
22+
want to your configuration under the `services` key:
2123

2224
```
2325
services:
@@ -37,3 +39,29 @@ parameters:
3739
- *Test.php
3840
- *TestBase.php
3941
```
42+
43+
## Adapting to your project
44+
45+
### Entity storage mappings.
46+
47+
The `EntityTypeManagerGetStorageDynamicReturnTypeExtension` service helps map dynamic return types. This inspects the
48+
passed entity type ID and tries to return a known storage class, besides the default `EntityStorageInterface`. The
49+
default mapping can be found in `extension.neon`. For example:
50+
51+
```
52+
drupal:
53+
entityTypeStorageMapping:
54+
node: Drupal\node\NodeStorage
55+
taxonomy_term: Drupal\taxonomy\TermStorage
56+
user: Drupal\user\UserStorage
57+
```
58+
59+
To add support for custom entities, you may add the same definition in your project's `phpstan.neon`. See the following
60+
example for adding a mapping for Search API:
61+
62+
```
63+
drupal:
64+
entityTypeStorageMapping:
65+
search_api_index: Drupal\search_api\Entity\SearchApiConfigEntityStorage
66+
search_api_server: Drupal\search_api\Entity\SearchApiConfigEntityStorage
67+
```

extension.neon

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,19 @@ parameters:
88
- inc
99
extensions:
1010
drupal: PHPStan\DependencyInjection\DrupalExtension
11+
drupal:
12+
entityTypeStorageMapping:
13+
node: Drupal\node\NodeStorage
14+
taxonomy_term: Drupal\taxonomy\TermStorage
15+
user: Drupal\user\UserStorage
1116
rules:
1217
- PHPStan\Rules\Classes\PluginManagerInspectionRule
1318
- PHPStan\Rules\Drupal\Coder\DiscouragedFunctionsRule
1419
- PHPStan\Rules\Drupal\GlobalDrupalDependencyInjectionRule
1520
- PHPStan\Rules\Drupal\PluginManager\PluginManagerSetsCacheBackendRule
21+
services:
22+
-
23+
class: PHPStan\Type\EntityTypeManagerGetStorageDynamicReturnTypeExtension
24+
arguments:
25+
entityTypeStorageMapping: %drupal.entityTypeStorageMapping%
26+
tags: [phpstan.broker.dynamicMethodReturnTypeExtension]

src/DependencyInjection/DrupalExtension.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ public function loadConfiguration(): void
8080
$this->modules = $config['modules'] ?? [];
8181
$this->themes = $config['themes'] ?? [];
8282

83+
$builder->parameters['drupal']['entityTypeStorageMapping'] = $config['entityTypeStorageMapping'];
84+
8385
$builder = $this->getContainerBuilder();
8486
foreach ($builder->getDefinitions() as $definition) {
8587
$factory = $definition->getFactory();
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace PHPStan\Type;
4+
5+
use PhpParser\Node\Expr\BinaryOp\Concat;
6+
use PhpParser\Node\Expr\MethodCall;
7+
use PhpParser\Node\Scalar\String_;
8+
use PHPStan\Analyser\Scope;
9+
use PHPStan\Reflection\MethodReflection;
10+
use PHPStan\Reflection\ParametersAcceptorSelector;
11+
12+
class EntityTypeManagerGetStorageDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
13+
{
14+
/**
15+
* @var array
16+
*/
17+
private $entityTypeStorageMapping;
18+
19+
public function __construct(array $entityTypeStorageMapping = [])
20+
{
21+
$this->entityTypeStorageMapping = $entityTypeStorageMapping;
22+
}
23+
24+
public function getClass(): string
25+
{
26+
return 'Drupal\Core\Entity\EntityTypeManagerInterface';
27+
}
28+
29+
public function isMethodSupported(MethodReflection $methodReflection): bool
30+
{
31+
return $methodReflection->getName() === 'getStorage';
32+
}
33+
34+
public function getTypeFromMethodCall(
35+
MethodReflection $methodReflection,
36+
MethodCall $methodCall,
37+
Scope $scope
38+
): Type {
39+
$returnType = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
40+
if (!isset($methodCall->args[0])) {
41+
return $returnType;
42+
}
43+
44+
$arg1 = $methodCall->args[0]->value;
45+
46+
// @todo handle where the first param is EntityTypeInterface::id()
47+
if ($arg1 instanceof MethodCall) {
48+
// There may not be much that can be done, since it's a generic EntityTypeInterface.
49+
return $returnType;
50+
}
51+
// @todo handle concat ie: entity_{$display_context}_display for entity_form_display or entity_view_display
52+
if ($arg1 instanceof Concat) {
53+
return $returnType;
54+
}
55+
if (!$arg1 instanceof String_) {
56+
// @todo determine what these types are, and try to resolve entity name from.
57+
return $returnType;
58+
}
59+
60+
$entityTypeId = $arg1->value;
61+
62+
if (isset($this->entityTypeStorageMapping[$entityTypeId])) {
63+
return new ObjectType($this->entityTypeStorageMapping[$entityTypeId]);
64+
}
65+
return $returnType;
66+
}
67+
}

0 commit comments

Comments
 (0)