Skip to content

Commit 2062852

Browse files
authored
Add EntityRepositoryInterface return type extension (#739)
1 parent c3f8d80 commit 2062852

File tree

4 files changed

+179
-0
lines changed

4 files changed

+179
-0
lines changed

extension.neon

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,9 @@ services:
272272
-
273273
class: mglaman\PHPStanDrupal\Type\EntityStorage\EntityStorageDynamicReturnTypeExtension
274274
tags: [phpstan.broker.dynamicMethodReturnTypeExtension]
275+
-
276+
class: mglaman\PHPStanDrupal\Type\EntityRepositoryReturnTypeExtension
277+
tags: [phpstan.broker.dynamicMethodReturnTypeExtension]
275278
-
276279
class: mglaman\PHPStanDrupal\Type\EntityStorage\GetQueryReturnTypeExtension
277280
tags: [phpstan.broker.dynamicMethodReturnTypeExtension]
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace mglaman\PHPStanDrupal\Type;
6+
7+
use Drupal\Core\Config\Entity\ConfigEntityInterface;
8+
use Drupal\Core\Entity\EntityRepositoryInterface;
9+
use mglaman\PHPStanDrupal\Drupal\EntityDataRepository;
10+
use PhpParser\Node\Expr\MethodCall;
11+
use PHPStan\Analyser\Scope;
12+
use PHPStan\Reflection\MethodReflection;
13+
use PHPStan\Reflection\ParametersAcceptorSelector;
14+
use PHPStan\Type\ArrayType;
15+
use PHPStan\Type\DynamicMethodReturnTypeExtension;
16+
use PHPStan\Type\IntegerType;
17+
use PHPStan\Type\ObjectType;
18+
use PHPStan\Type\StringType;
19+
use PHPStan\Type\Type;
20+
use PHPStan\Type\TypeCombinator;
21+
22+
final class EntityRepositoryReturnTypeExtension implements DynamicMethodReturnTypeExtension
23+
{
24+
25+
/**
26+
* @var EntityDataRepository
27+
*/
28+
private $entityDataRepository;
29+
30+
public function __construct(EntityDataRepository $entityDataRepository)
31+
{
32+
$this->entityDataRepository = $entityDataRepository;
33+
}
34+
35+
public function getClass(): string
36+
{
37+
return EntityRepositoryInterface::class;
38+
}
39+
40+
public function isMethodSupported(MethodReflection $methodReflection): bool
41+
{
42+
return in_array(
43+
$methodReflection->getName(),
44+
[
45+
'getTranslationFromContext',
46+
'loadEntityByUuid',
47+
'loadEntityByConfigTarget',
48+
'getActive',
49+
'getActiveMultiple',
50+
'getCanonical',
51+
'getCanonicalMultiple',
52+
],
53+
true
54+
);
55+
}
56+
57+
public function getTypeFromMethodCall(
58+
MethodReflection $methodReflection,
59+
MethodCall $methodCall,
60+
Scope $scope
61+
): ?Type {
62+
$methodName = $methodReflection->getName();
63+
$methodArgs = $methodCall->getArgs();
64+
$returnType = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
65+
66+
if (count($methodArgs) === 0) {
67+
return $returnType;
68+
}
69+
70+
if ($methodName === 'getTranslationFromContext') {
71+
return $scope->getType($methodArgs[0]->value);
72+
}
73+
74+
$entityObjectTypes = [];
75+
$entityIdArg = $scope->getType($methodArgs[0]->value);
76+
foreach ($entityIdArg->getConstantStrings() as $constantStringType) {
77+
$entityObjectTypes[] = $this->entityDataRepository->get($constantStringType->getValue())->getClassType() ?? $returnType;
78+
}
79+
$entityTypes = TypeCombinator::union(...$entityObjectTypes);
80+
81+
if ($returnType->isArray()->no()) {
82+
if ($returnType->isNull()->maybe()) {
83+
$entityTypes = TypeCombinator::addNull($entityTypes);
84+
}
85+
return $entityTypes;
86+
}
87+
88+
if ((new ObjectType(ConfigEntityInterface::class))->isSuperTypeOf($entityTypes)->yes()) {
89+
$keyType = new StringType();
90+
} else {
91+
$keyType = new IntegerType();
92+
}
93+
94+
return new ArrayType($keyType, $entityTypes);
95+
}
96+
}
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\Tests\Type;
6+
7+
use mglaman\PHPStanDrupal\Tests\AdditionalConfigFilesTrait;
8+
use PHPStan\Testing\TypeInferenceTestCase;
9+
10+
final class EntityRepositoryTypeTest extends TypeInferenceTestCase
11+
{
12+
use AdditionalConfigFilesTrait;
13+
14+
public function dataFileAsserts(): iterable
15+
{
16+
yield from self::gatherAssertTypes(__DIR__ . '/data/entity-repository.php');
17+
}
18+
19+
/**
20+
* @dataProvider dataFileAsserts
21+
* @param string $assertType
22+
* @param string $file
23+
* @param mixed ...$args
24+
*/
25+
public function testFileAsserts(
26+
string $assertType,
27+
string $file,
28+
...$args
29+
): void
30+
{
31+
$this->assertFileAsserts($assertType, $file, ...$args);
32+
}
33+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
namespace EntityRepository;
4+
5+
use Drupal\node\Entity\Node;
6+
use function PHPStan\Testing\assertType;
7+
8+
$entityRepository = \Drupal::service('entity.repository');
9+
10+
assertType(
11+
'Drupal\node\Entity\Node|null',
12+
$entityRepository->loadEntityByUuid('node', '3f205175-04f7-4f57-b48b-9799299252c3')
13+
);
14+
15+
assertType(
16+
'Drupal\Core\Entity\Entity\EntityViewMode|null',
17+
$entityRepository->loadEntityByConfigTarget('entity_view_mode', 'media.default')
18+
);
19+
20+
assertType(
21+
'Drupal\node\Entity\Node',
22+
$entityRepository->getTranslationFromContext(Node::create())
23+
);
24+
25+
assertType(
26+
'Drupal\node\Entity\Node|null',
27+
$entityRepository->getActive('node', 5)
28+
);
29+
30+
assertType(
31+
'array<int, Drupal\node\Entity\Node>',
32+
$entityRepository->getActiveMultiple('node', [5])
33+
);
34+
assertType(
35+
'array<string, Drupal\block\Entity\Block>',
36+
$entityRepository->getActiveMultiple('block', ['foo'])
37+
);
38+
39+
assertType(
40+
'Drupal\node\Entity\Node|null',
41+
$entityRepository->getCanonical('node', 5)
42+
);
43+
44+
assertType(
45+
'array<int, Drupal\node\Entity\Node>',
46+
$entityRepository->getCanonicalMultiple('node', [5])
47+
);

0 commit comments

Comments
 (0)