Skip to content

Commit 76d9e0b

Browse files
eiriksmmglaman
andauthored
Allow ::referencedEntities on field items (#265)
* First stab at this * Tiny refactor that makes more sense * Refactor and add tests * Fix phpstan warnings (leverage ObjectType) * Put back items lost in rebase for property tags * Refactor method detection, so method reflection can be used for any item list Co-authored-by: Matt Glaman <[email protected]>
1 parent f95b05f commit 76d9e0b

5 files changed

+222
-4
lines changed

extension.neon

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,9 @@ services:
281281
-
282282
class: mglaman\PHPStanDrupal\Reflection\EntityFieldsViaMagicReflectionExtension
283283
tags: [phpstan.broker.propertiesClassReflectionExtension]
284+
-
285+
class: mglaman\PHPStanDrupal\Reflection\EntityFieldMethodsViaMagicReflectionExtension
286+
tags: [phpstan.broker.methodsClassReflectionExtension]
284287
-
285288
class: mglaman\PHPStanDrupal\Rules\Classes\ClassExtendsInternalClassRule
286289
tags: [phpstan.rules.rule]
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
namespace mglaman\PHPStanDrupal\Reflection;
4+
5+
use PHPStan\Reflection\ClassReflection;
6+
use PHPStan\Reflection\MethodsClassReflectionExtension;
7+
use PHPStan\Reflection\MethodReflection;
8+
use PHPStan\Type\ObjectType;
9+
10+
/**
11+
* Allows some common methods on fields.
12+
*/
13+
class EntityFieldMethodsViaMagicReflectionExtension implements MethodsClassReflectionExtension
14+
{
15+
16+
public function hasMethod(ClassReflection $classReflection, string $methodName): bool
17+
{
18+
if ($classReflection->hasNativeMethod($methodName) || array_key_exists($methodName, $classReflection->getMethodTags())) {
19+
// Let other parts of PHPStan handle this.
20+
return false;
21+
}
22+
$interfaceObject = new ObjectType('Drupal\Core\Field\FieldItemListInterface');
23+
$objectType = new ObjectType($classReflection->getName());
24+
if (!$interfaceObject->isSuperTypeOf($objectType)->yes()) {
25+
return false;
26+
}
27+
28+
if ($methodName === 'referencedEntities') {
29+
return true;
30+
}
31+
32+
return false;
33+
}
34+
35+
public function getMethod(ClassReflection $classReflection, string $methodName): MethodReflection
36+
{
37+
if ($methodName === 'referencedEntities') {
38+
$entityReferenceFieldItemListInterfaceType = new ObjectType('Drupal\Core\Field\EntityReferenceFieldItemListInterface');
39+
$classReflection = $entityReferenceFieldItemListInterfaceType->getClassReflection();
40+
assert($classReflection !== null);
41+
}
42+
43+
return new FieldItemListMethodReflection(
44+
$classReflection,
45+
$methodName
46+
);
47+
}
48+
}

src/Reflection/EntityFieldsViaMagicReflectionExtension.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public function hasProperty(ClassReflection $classReflection, string $propertyNa
3737
// Content entities have magical __get... so it is kind of true.
3838
return true;
3939
}
40-
if (self::classObjectIsSuperOfFieldItemList($reflection)->yes()) {
40+
if (self::classObjectIsSuperOfInterface($reflection, self::getFieldItemListInterfaceObject())->yes()) {
4141
return FieldItemListPropertyReflection::canHandleProperty($classReflection, $propertyName);
4242
}
4343

@@ -50,17 +50,17 @@ public function getProperty(ClassReflection $classReflection, string $propertyNa
5050
if ($reflection->implementsInterface('Drupal\Core\Entity\EntityInterface')) {
5151
return new EntityFieldReflection($classReflection, $propertyName);
5252
}
53-
if (self::classObjectIsSuperOfFieldItemList($reflection)->yes()) {
53+
if (self::classObjectIsSuperOfInterface($reflection, self::getFieldItemListInterfaceObject())->yes()) {
5454
return new FieldItemListPropertyReflection($classReflection, $propertyName);
5555
}
5656

5757
throw new \LogicException($classReflection->getName() . "::$propertyName should be handled earlier.");
5858
}
5959

60-
protected static function classObjectIsSuperOfFieldItemList(\ReflectionClass $reflection) : TrinaryLogic
60+
public static function classObjectIsSuperOfInterface(\ReflectionClass $reflection, ObjectType $interfaceObject) : TrinaryLogic
6161
{
6262
$classObject = new ObjectType($reflection->getName());
63-
return self::getFieldItemListInterfaceObject()->isSuperTypeOf($classObject);
63+
return $interfaceObject->isSuperTypeOf($classObject);
6464
}
6565

6666
protected static function getFieldItemListInterfaceObject() : ObjectType
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
<?php
2+
3+
namespace mglaman\PHPStanDrupal\Reflection;
4+
5+
use Drupal\Core\Field\EntityReferenceFieldItemListInterface;
6+
use PHPStan\Reflection\ClassReflection;
7+
use PHPStan\Reflection\MethodReflection;
8+
use PHPStan\Reflection\ClassMemberReflection;
9+
use PHPStan\Reflection\ReflectionProviderStaticAccessor;
10+
use PHPStan\Reflection\TrivialParametersAcceptor;
11+
use PHPStan\ShouldNotHappenException;
12+
use PHPStan\TrinaryLogic;
13+
use PHPStan\Type\NullType;
14+
use PHPStan\Type\ObjectType;
15+
use PHPStan\Type\StringType;
16+
use PHPStan\Type\Type;
17+
18+
/**
19+
* Allows field access to common methods.
20+
*/
21+
class FieldItemListMethodReflection implements MethodReflection
22+
{
23+
24+
/** @var ClassReflection */
25+
private $declaringClass;
26+
27+
/** @var string */
28+
private $methodName;
29+
30+
public function __construct(ClassReflection $declaringClass, string $methodName)
31+
{
32+
$this->declaringClass = $declaringClass;
33+
$this->methodName = $methodName;
34+
}
35+
36+
public function getDeclaringClass(): ClassReflection
37+
{
38+
return $this->declaringClass;
39+
}
40+
41+
public function isStatic(): bool
42+
{
43+
return false;
44+
}
45+
46+
public function isPrivate(): bool
47+
{
48+
return false;
49+
}
50+
51+
public function isPublic(): bool
52+
{
53+
return true;
54+
}
55+
56+
public function getDocComment(): ?string
57+
{
58+
return null;
59+
}
60+
61+
public function getName(): string
62+
{
63+
return $this->methodName;
64+
}
65+
66+
public function getPrototype(): ClassMemberReflection
67+
{
68+
return $this;
69+
}
70+
71+
/**
72+
* @return \PHPStan\Reflection\ParametersAcceptor[]
73+
*/
74+
public function getVariants(): array
75+
{
76+
return [
77+
new TrivialParametersAcceptor(),
78+
];
79+
}
80+
81+
public function isDeprecated(): TrinaryLogic
82+
{
83+
return TrinaryLogic::createNo();
84+
}
85+
86+
public function getDeprecatedDescription(): ?string
87+
{
88+
return '';
89+
}
90+
91+
public function isFinal(): TrinaryLogic
92+
{
93+
return TrinaryLogic::createYes();
94+
}
95+
96+
public function isInternal(): TrinaryLogic
97+
{
98+
return TrinaryLogic::createNo();
99+
}
100+
101+
public function getThrowType(): ?Type
102+
{
103+
return null;
104+
}
105+
106+
public function hasSideEffects(): TrinaryLogic
107+
{
108+
return TrinaryLogic::createNo();
109+
}
110+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace mglaman\PHPStanDrupal\Tests\Reflection;
4+
5+
use Drupal\Core\Field\FieldItemListInterface;
6+
use Drupal\entity_test\Entity\EntityTest;
7+
use Drupal\node\Entity\Node;
8+
use mglaman\PHPStanDrupal\Reflection\EntityFieldMethodsViaMagicReflectionExtension;
9+
use mglaman\PHPStanDrupal\Tests\AdditionalConfigFilesTrait;
10+
use PHPStan\Testing\PHPStanTestCase;
11+
12+
final class EntityFieldMethodsViaMagicReflectionExtensionTest extends PHPStanTestCase {
13+
14+
use AdditionalConfigFilesTrait;
15+
16+
/**
17+
* @var \mglaman\PHPStanDrupal\Reflection\EntityFieldMethodsViaMagicReflectionExtension
18+
*/
19+
private $extension;
20+
21+
protected function setUp(): void
22+
{
23+
parent::setUp();
24+
$this->extension = new EntityFieldMethodsViaMagicReflectionExtension();
25+
}
26+
27+
/**
28+
* @dataProvider dataHasMethod
29+
*
30+
* @param string $method
31+
* @param bool $result
32+
*/
33+
public function testHasMethod(string $class, string $method, bool $result): void
34+
{
35+
$reflection = $this->createReflectionProvider()->getClass($class);
36+
self::assertEquals($result, $this->extension->hasMethod($reflection, $method));
37+
}
38+
39+
public function dataHasMethod(): \Generator
40+
{
41+
// Technically it does not have this method. But we allow it for now.
42+
yield 'field item list: referencedEntities' => [
43+
FieldItemListInterface::class,
44+
'referencedEntities',
45+
true,
46+
];
47+
48+
// A content entity for sure does not have this method.
49+
yield 'Content entity: referencedEntities' => [
50+
// @phpstan-ignore-next-line
51+
EntityTest::class,
52+
'referencedEntities',
53+
false,
54+
];
55+
}
56+
57+
}

0 commit comments

Comments
 (0)