Skip to content

Commit 35e9765

Browse files
committed
Review items and adjustments for magic reflection extension
1 parent 18f4368 commit 35e9765

File tree

8 files changed

+105
-21
lines changed

8 files changed

+105
-21
lines changed

extension.neon

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,6 @@ services:
3535
-
3636
class: PHPStan\Type\ServiceDynamicReturnTypeExtension
3737
tags: [phpstan.broker.dynamicMethodReturnTypeExtension]
38+
-
39+
class: PHPStan\Reflection\EntityFieldsViaMagicReflectionExtension
40+
tags: [phpstan.broker.propertiesClassReflectionExtension]

src/Drupal/Bootstrap.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ protected function addCoreNamespaces(): void
166166
$this->namespaces['Drupal\\FunctionalJavascriptTests'] = $core_tests_dir . '/FunctionalJavascriptTests';
167167
$this->namespaces['Drupal\\Tests\\TestSuites'] = $this->drupalRoot . '/core/tests/TestSuites';
168168
}
169+
169170
protected function addModuleNamespaces(): void
170171
{
171172
foreach ($this->moduleData as $module) {

src/Reflection/EntityFieldReflection.php

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

33
namespace PHPStan\Reflection;
44

5+
use PHPStan\Type\MixedType;
56
use PHPStan\Type\ObjectType;
67
use PHPStan\Type\Type;
78

@@ -27,12 +28,23 @@ public function __construct(ClassReflection $declaringClass, string $propertyNam
2728

2829
public function getType(): Type
2930
{
30-
if ($this->propertyName == 'original') {
31-
// See Drupal\Core\Entity\EntityStorageBase::doPreSave
31+
if ($this->propertyName === 'original') {
32+
if ($this->declaringClass->isSubclassOf('Drupal\Core\Entity\ContentEntityInterface')) {
33+
$objectType = 'Drupal\Core\Entity\ContentEntityInterface';
34+
} elseif ($this->declaringClass->isSubclassOf('Drupal\Core\Config\Entity\ConfigEntityInterface')) {
35+
$objectType = 'Drupal\Core\Config\Entity\ConfigEntityInterface';
36+
} else {
37+
$objectType = 'Drupal\Core\Entity\EntityInterface';
38+
}
39+
return new ObjectType($objectType);
40+
}
41+
42+
if ($this->declaringClass->isSubclassOf('Drupal\Core\Entity\ContentEntityInterface')) {
43+
// Assume the property is a field.
3244
return new ObjectType('Drupal\Core\Field\FieldItemListInterface');
3345
}
3446

35-
return new ObjectType('Drupal\Core\Field\FieldItemListInterface');
47+
return new MixedType();
3648
}
3749

3850
public function getDeclaringClass(): ClassReflection

src/Reflection/EntityFieldsViaMagicReflectionExtension.php

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,21 @@ class EntityFieldsViaMagicReflectionExtension implements PropertiesClassReflecti
1313
public function hasProperty(ClassReflection $classReflection, string $propertyName): bool
1414
{
1515
if ($classReflection->hasNativeProperty($propertyName)) {
16-
// Let other parts of PHPStan handle this.
16+
// Let other parts of PHPStan handle this.
1717
return false;
1818
}
1919

2020
$reflection = $classReflection->getNativeReflection();
21-
if ($reflection->implementsInterface('Drupal\Core\Entity\EntityInterface')) {
21+
// We need to find a way to parse the entity annotation so that at the minimum the `entity_keys` are
22+
// supported. The real fix is Drupal developers _really_ need to start writing @property definitions in the
23+
// class doc if they don't get `get` methods.
24+
if ($reflection->implementsInterface('Drupal\Core\Entity\ContentEntityInterface')) {
25+
// @todo revisit if it's a good idea to be true.
26+
// Content entities have magical __get... so it is kind of true.
2227
return true;
2328
}
2429
if ($reflection->implementsInterface('Drupal\Core\Field\FieldItemListInterface')) {
25-
return FieldItemListPropertyReflection::canHandleProperty($propertyName);
30+
return FieldItemListPropertyReflection::canHandleProperty($classReflection, $propertyName);
2631
}
2732

2833
return false;

src/Reflection/FieldItemListPropertyReflection.php

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
namespace PHPStan\Reflection;
44

5-
use PHPStan\Type\IntegerType;
6-
use PHPStan\Type\MixedType;
5+
use PHPStan\Type\NullType;
76
use PHPStan\Type\ObjectType;
7+
use PHPStan\Type\StringType;
88
use PHPStan\Type\Type;
99

1010
/**
@@ -27,25 +27,28 @@ public function __construct(ClassReflection $declaringClass, string $propertyNam
2727
$this->propertyName = $propertyName;
2828
}
2929

30-
public static function canHandleProperty(string $propertyName): bool
30+
public static function canHandleProperty(ClassReflection $classReflection, string $propertyName): bool
3131
{
32+
// @todo use the class reflection and be more specific about handled properties.
33+
// Currently \PHPStan\Reflection\EntityFieldReflection::getType always passes FieldItemListInterface.
3234
$names = ['entity', 'value', 'target_id'];
3335
return in_array($propertyName, $names, true);
3436
}
3537

3638
public function getType(): Type
3739
{
38-
if ($this->propertyName == 'entity') {
39-
// It was a EntityReferenceFieldItemList
40-
return new ObjectType('Drupal\Core\Entity\FieldableEntityInterface');
40+
if ($this->propertyName === 'entity') {
41+
return new ObjectType('Drupal\Core\Entity\EntityInterface');
4142
}
42-
if ($this->propertyName == 'target_id') {
43-
// It was a EntityReferenceFieldItemList
44-
return new IntegerType();
43+
if ($this->propertyName === 'target_id') {
44+
return new StringType();
4545
}
46-
if ($this->propertyName == 'value') {
47-
return new MixedType();
46+
if ($this->propertyName === 'value') {
47+
return new StringType();
4848
}
49+
50+
// Fallback.
51+
return new NullType();
4952
}
5053

5154
public function getDeclaringClass(): ClassReflection
Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
<?php
22

3-
namespace Drupal\phpstan_fixtures;
3+
namespace Drupal\phpstan_fixtures\EntityFieldReflection;
44

55
use Drupal\entity_test\Entity\EntityTest;
66

7-
class EntityFieldFixture {
8-
public function testMagicalFanciness() {
7+
class EntityFieldMagicalGetters {
8+
public function testLabel() {
99

1010
/** @var EntityTest $testEntity */
1111
$testEntity = EntityTest::create([
@@ -15,9 +15,11 @@ public function testMagicalFanciness() {
1515

1616
// 🤦‍♂
1717
$label1 = $testEntity->label();
18+
// @todo Access to an undefined property Drupal\Core\TypedData\TypedDataInterface::$value.
1819
$label2 = $testEntity->get('name')->first()->value;
20+
// @todo Access to an undefined property Drupal\Core\TypedData\TypedDataInterface::$value.
1921
$label3 = $testEntity->name->first()->value;
22+
// This doesn't fail because of EntityFieldsViaMagicReflectionExtension
2023
$label4 = $testEntity->name->value;
21-
2224
}
2325
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
namespace Drupal\phpstan_fixtures\EntityFieldReflection;
4+
5+
use Drupal\entity_test\Entity\EntityTest;
6+
7+
class EntityFieldOriginalProperty {
8+
9+
public function testOriginal() {
10+
/** @var EntityTest $testEntity */
11+
$testEntity = EntityTest::create([
12+
'name' => 'Llama',
13+
'type' => 'entity_test',
14+
]);
15+
if ($testEntity->getRevisionId() !== $testEntity->original->getRevisionId()) {
16+
$testEntity->setSyncing(TRUE);
17+
}
18+
19+
if (empty($testEntity->original) || $testEntity->getRevisionId() !== $testEntity->original->getRevisionId()) {
20+
$testEntity->setSyncing(TRUE);
21+
}
22+
}
23+
}

tests/src/EntityTestClass.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace PHPStan\Drupal;
4+
5+
final class EntityTestClass extends AnalyzerTestBase
6+
{
7+
/**
8+
* @dataProvider dataEntitySamples
9+
*/
10+
public function testEntityFields(string $path, int $count, array $errorMessages) {
11+
$errors = $this->runAnalyze($path);
12+
$this->assertCount($count, $errors, print_r($errors, true));
13+
foreach ($errors as $key => $error) {
14+
$this->assertEquals($errorMessages[$key], $error->getMessage());
15+
}
16+
}
17+
18+
19+
public function dataEntitySamples(): \Generator
20+
{
21+
yield [
22+
__DIR__ . '/../fixtures/drupal/modules/phpstan_fixtures/src/EntityFieldReflection/EntityFieldMagicalGetters.php',
23+
2,
24+
[
25+
'Access to an undefined property Drupal\Core\TypedData\TypedDataInterface::$value.',
26+
'Access to an undefined property Drupal\Core\TypedData\TypedDataInterface::$value.',
27+
]
28+
];
29+
yield [
30+
__DIR__ . '/../fixtures/drupal/modules/phpstan_fixtures/src/EntityFieldReflection/EntityFieldOriginalProperty.php',
31+
0,
32+
[]
33+
];
34+
}
35+
}

0 commit comments

Comments
 (0)