Skip to content

Commit ebf69ca

Browse files
authored
Merge pull request #27 from Fonata/content-entity-fields-magic
Entities: Allow FieldItemList access through magic __get/__set methods
2 parents 306a034 + 35e9765 commit ebf69ca

17 files changed

+457
-113
lines changed

.travis.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
language: php
22
php:
3-
- 7.1
4-
- 7.2
5-
- 7.3
3+
- "7.1"
4+
- "7.2"
5+
- "7.3"
66

77
before_install:
88
- composer global require "hirak/prestissimo:^0.3"

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
}
3838
},
3939
"autoload-dev": {
40-
"classmap": ["tests/"]
40+
"classmap": ["tests/src"]
4141
},
4242
"extra": {
4343
"installer-paths": {

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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,9 @@ protected function addCoreNamespaces(): void
164164
$this->namespaces['Drupal\\KernelTests'] = $core_tests_dir . '/KernelTests';
165165
$this->namespaces['Drupal\\FunctionalTests'] = $core_tests_dir . '/FunctionalTests';
166166
$this->namespaces['Drupal\\FunctionalJavascriptTests'] = $core_tests_dir . '/FunctionalJavascriptTests';
167+
$this->namespaces['Drupal\\Tests\\TestSuites'] = $this->drupalRoot . '/core/tests/TestSuites';
167168
}
169+
168170
protected function addModuleNamespaces(): void
169171
{
170172
foreach ($this->moduleData as $module) {
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?php
2+
3+
namespace PHPStan\Reflection;
4+
5+
use PHPStan\Type\MixedType;
6+
use PHPStan\Type\ObjectType;
7+
use PHPStan\Type\Type;
8+
9+
/**
10+
* Allows field access via magic methods
11+
*
12+
* See \Drupal\Core\Entity\ContentEntityBase::__get and ::__set.
13+
*/
14+
class EntityFieldReflection implements PropertyReflection
15+
{
16+
17+
/** @var ClassReflection */
18+
private $declaringClass;
19+
20+
/** @var string */
21+
private $propertyName;
22+
23+
public function __construct(ClassReflection $declaringClass, string $propertyName)
24+
{
25+
$this->declaringClass = $declaringClass;
26+
$this->propertyName = $propertyName;
27+
}
28+
29+
public function getType(): Type
30+
{
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.
44+
return new ObjectType('Drupal\Core\Field\FieldItemListInterface');
45+
}
46+
47+
return new MixedType();
48+
}
49+
50+
public function getDeclaringClass(): ClassReflection
51+
{
52+
return $this->declaringClass;
53+
}
54+
55+
public function isStatic(): bool
56+
{
57+
return false;
58+
}
59+
60+
public function isPrivate(): bool
61+
{
62+
return false;
63+
}
64+
65+
public function isPublic(): bool
66+
{
67+
return true;
68+
}
69+
70+
public function isReadable(): bool
71+
{
72+
return true;
73+
}
74+
75+
public function isWritable(): bool
76+
{
77+
return true;
78+
}
79+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
namespace PHPStan\Reflection;
4+
5+
/**
6+
* Allows field access via magic methods
7+
*
8+
* See \Drupal\Core\Entity\ContentEntityBase::__get and ::__set.
9+
*/
10+
class EntityFieldsViaMagicReflectionExtension implements PropertiesClassReflectionExtension
11+
{
12+
13+
public function hasProperty(ClassReflection $classReflection, string $propertyName): bool
14+
{
15+
if ($classReflection->hasNativeProperty($propertyName)) {
16+
// Let other parts of PHPStan handle this.
17+
return false;
18+
}
19+
20+
$reflection = $classReflection->getNativeReflection();
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.
27+
return true;
28+
}
29+
if ($reflection->implementsInterface('Drupal\Core\Field\FieldItemListInterface')) {
30+
return FieldItemListPropertyReflection::canHandleProperty($classReflection, $propertyName);
31+
}
32+
33+
return false;
34+
}
35+
36+
public function getProperty(ClassReflection $classReflection, string $propertyName): PropertyReflection
37+
{
38+
$reflection = $classReflection->getNativeReflection();
39+
if ($reflection->implementsInterface('Drupal\Core\Entity\EntityInterface')) {
40+
return new EntityFieldReflection($classReflection, $propertyName);
41+
}
42+
if ($reflection->implementsInterface('Drupal\Core\Field\FieldItemListInterface')) {
43+
return new FieldItemListPropertyReflection($classReflection, $propertyName);
44+
}
45+
46+
throw new \LogicException($classReflection->getName() . "::$propertyName should be handled earlier.");
47+
}
48+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
<?php
2+
3+
namespace PHPStan\Reflection;
4+
5+
use PHPStan\Type\NullType;
6+
use PHPStan\Type\ObjectType;
7+
use PHPStan\Type\StringType;
8+
use PHPStan\Type\Type;
9+
10+
/**
11+
* Allows field access via magic methods
12+
*
13+
* See \Drupal\Core\Field\FieldItemListInterface::__get and ::__set.
14+
*/
15+
class FieldItemListPropertyReflection implements PropertyReflection
16+
{
17+
18+
/** @var ClassReflection */
19+
private $declaringClass;
20+
21+
/** @var string */
22+
private $propertyName;
23+
24+
public function __construct(ClassReflection $declaringClass, string $propertyName)
25+
{
26+
$this->declaringClass = $declaringClass;
27+
$this->propertyName = $propertyName;
28+
}
29+
30+
public static function canHandleProperty(ClassReflection $classReflection, string $propertyName): bool
31+
{
32+
// @todo use the class reflection and be more specific about handled properties.
33+
// Currently \PHPStan\Reflection\EntityFieldReflection::getType always passes FieldItemListInterface.
34+
$names = ['entity', 'value', 'target_id'];
35+
return in_array($propertyName, $names, true);
36+
}
37+
38+
public function getType(): Type
39+
{
40+
if ($this->propertyName === 'entity') {
41+
return new ObjectType('Drupal\Core\Entity\EntityInterface');
42+
}
43+
if ($this->propertyName === 'target_id') {
44+
return new StringType();
45+
}
46+
if ($this->propertyName === 'value') {
47+
return new StringType();
48+
}
49+
50+
// Fallback.
51+
return new NullType();
52+
}
53+
54+
public function getDeclaringClass(): ClassReflection
55+
{
56+
return $this->declaringClass;
57+
}
58+
59+
public function isStatic(): bool
60+
{
61+
return false;
62+
}
63+
64+
public function isPrivate(): bool
65+
{
66+
return false;
67+
}
68+
69+
public function isPublic(): bool
70+
{
71+
return true;
72+
}
73+
74+
public function isReadable(): bool
75+
{
76+
return true;
77+
}
78+
79+
public function isWritable(): bool
80+
{
81+
return true;
82+
}
83+
}

tests/DrupalIntegrationTest.php

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

0 commit comments

Comments
 (0)