Skip to content

Commit ecc4aec

Browse files
committed
Always written/read/initialized properties using ReadWritePropertiesExtension
1 parent cf8865d commit ecc4aec

File tree

3 files changed

+123
-10
lines changed

3 files changed

+123
-10
lines changed

extension.neon

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,6 @@ parameters:
2424
- stubs/Persistence/ObjectManagerDecorator.stub
2525
- stubs/Persistence/ObjectRepository.stub
2626
- stubs/RepositoryFactory.stub
27-
propertyAlwaysReadTags:
28-
- '@@ORM\Column'
29-
- '@@ORM\Id'
30-
- '@@ORM\OneToOne'
31-
- '@@ORM\OneToMany'
32-
- '@@ORM\ManyToOne'
33-
- '@@ORM\ManyToMany'
34-
- '@@ORM\Embedded'
35-
propertyAlwaysWrittenTags:
36-
- '@@ORM\GeneratedValue'
3727

3828
parametersSchema:
3929
doctrine: structure([
@@ -116,6 +106,11 @@ services:
116106
tags:
117107
- phpstan.broker.dynamicMethodReturnTypeExtension
118108

109+
-
110+
class: PHPStan\Rules\Doctrine\ORM\PropertiesExtension
111+
tags:
112+
- phpstan.properties.readWriteExtension
113+
119114
doctrineQueryBuilderArgumentsProcessor:
120115
class: PHPStan\Type\Doctrine\ArgumentsProcessor
121116
autowired: false
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Doctrine\ORM;
4+
5+
use PHPStan\Reflection\PropertyReflection;
6+
use PHPStan\Rules\Properties\ReadWritePropertiesExtension;
7+
use PHPStan\Type\Doctrine\ObjectMetadataResolver;
8+
9+
class PropertiesExtension implements ReadWritePropertiesExtension
10+
{
11+
12+
/** @var \PHPStan\Type\Doctrine\ObjectMetadataResolver */
13+
private $objectMetadataResolver;
14+
15+
public function __construct(ObjectMetadataResolver $objectMetadataResolver)
16+
{
17+
$this->objectMetadataResolver = $objectMetadataResolver;
18+
}
19+
20+
public function isAlwaysRead(PropertyReflection $property, string $propertyName): bool
21+
{
22+
$className = $property->getDeclaringClass()->getName();
23+
$metadata = $this->findMetadata($className);
24+
if ($metadata === null) {
25+
return false;
26+
}
27+
28+
return $metadata->hasField($propertyName) || $metadata->hasAssociation($propertyName);
29+
}
30+
31+
private function findMetadata(string $className): ?\Doctrine\ORM\Mapping\ClassMetadataInfo
32+
{
33+
$objectManager = $this->objectMetadataResolver->getObjectManager();
34+
if ($objectManager === null) {
35+
return null;
36+
}
37+
38+
try {
39+
if ($objectManager->getMetadataFactory()->isTransient($className)) {
40+
return null;
41+
}
42+
} catch (\ReflectionException $e) {
43+
return null;
44+
}
45+
46+
try {
47+
$metadata = $objectManager->getClassMetadata($className);
48+
} catch (\Doctrine\ORM\Mapping\MappingException $e) {
49+
return null;
50+
}
51+
52+
$classMetadataInfo = 'Doctrine\ORM\Mapping\ClassMetadataInfo';
53+
if (!$metadata instanceof $classMetadataInfo) {
54+
return null;
55+
}
56+
57+
return $metadata;
58+
}
59+
60+
public function isAlwaysWritten(PropertyReflection $property, string $propertyName): bool
61+
{
62+
$declaringClass = $property->getDeclaringClass();
63+
$className = $declaringClass->getName();
64+
$metadata = $this->findMetadata($className);
65+
if ($metadata === null) {
66+
return false;
67+
}
68+
69+
if (!$metadata->hasField($propertyName) && !$metadata->hasAssociation($propertyName)) {
70+
return false;
71+
}
72+
73+
if ($metadata->isReadOnly && !$declaringClass->hasConstructor()) {
74+
return true;
75+
}
76+
77+
if ($metadata->isIdentifierNatural()) {
78+
return false;
79+
}
80+
81+
try {
82+
$identifiers = $metadata->getIdentifierFieldNames();
83+
} catch (\Throwable $e) {
84+
$mappingException = 'Doctrine\ORM\Mapping\MappingException';
85+
if (!$e instanceof $mappingException) {
86+
throw $e;
87+
}
88+
89+
return false;
90+
}
91+
92+
return in_array($propertyName, $identifiers, true);
93+
}
94+
95+
public function isInitialized(PropertyReflection $property, string $propertyName): bool
96+
{
97+
$declaringClass = $property->getDeclaringClass();
98+
$className = $declaringClass->getName();
99+
$metadata = $this->findMetadata($className);
100+
if ($metadata === null) {
101+
return false;
102+
}
103+
104+
if (!$metadata->hasField($propertyName) && !$metadata->hasAssociation($propertyName)) {
105+
return false;
106+
}
107+
108+
return $metadata->isReadOnly && !$declaringClass->hasConstructor();
109+
}
110+
111+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[
2+
{
3+
"message": "Class PHPStan\\DoctrineIntegration\\Persistence\\ManagerRegistryRepositoryDynamicReturn\\MyEntity has an unused property $id.",
4+
"line": 47,
5+
"ignorable": true
6+
}
7+
]

0 commit comments

Comments
 (0)