Skip to content

Commit 554f44f

Browse files
lookymanondrejmirtes
authored andcommitted
Validate entity field types against column types
1 parent 6c779cc commit 554f44f

36 files changed

+1053
-1
lines changed

extension.neon

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ conditionalTags:
2525
phpstan.broker.methodsClassReflectionExtension: %doctrine.allCollectionsSelectable%
2626

2727
services:
28+
-
29+
class: PHPStan\Type\Doctrine\DescriptorRegistryFactory
30+
-
31+
class: PHPStan\Type\Doctrine\DescriptorRegistry
32+
factory: @PHPStan\Type\Doctrine\DescriptorRegistryFactory::createRegistry
33+
2834
-
2935
class: PHPStan\Reflection\Doctrine\DoctrineSelectableClassReflectionExtension
3036
-
@@ -177,3 +183,77 @@ services:
177183
arguments:
178184
class: Doctrine\ORM\Query\Expr
179185
argumentsProcessor: @doctrineQueryBuilderArgumentsProcessor
186+
187+
# Type descriptors
188+
-
189+
class: PHPStan\Type\Doctrine\Descriptors\ArrayType
190+
tags: [phpstan.doctrine.typeDescriptor]
191+
-
192+
class: PHPStan\Type\Doctrine\Descriptors\BigIntType
193+
tags: [phpstan.doctrine.typeDescriptor]
194+
-
195+
class: PHPStan\Type\Doctrine\Descriptors\BinaryType
196+
tags: [phpstan.doctrine.typeDescriptor]
197+
-
198+
class: PHPStan\Type\Doctrine\Descriptors\BlobType
199+
tags: [phpstan.doctrine.typeDescriptor]
200+
-
201+
class: PHPStan\Type\Doctrine\Descriptors\DateImmutableType
202+
tags: [phpstan.doctrine.typeDescriptor]
203+
-
204+
class: PHPStan\Type\Doctrine\Descriptors\DateIntervalType
205+
tags: [phpstan.doctrine.typeDescriptor]
206+
-
207+
class: PHPStan\Type\Doctrine\Descriptors\DateTimeImmutableType
208+
tags: [phpstan.doctrine.typeDescriptor]
209+
-
210+
class: PHPStan\Type\Doctrine\Descriptors\DateTimeType
211+
tags: [phpstan.doctrine.typeDescriptor]
212+
-
213+
class: PHPStan\Type\Doctrine\Descriptors\DateTimeTzImmutableType
214+
tags: [phpstan.doctrine.typeDescriptor]
215+
-
216+
class: PHPStan\Type\Doctrine\Descriptors\DateTimeTzType
217+
tags: [phpstan.doctrine.typeDescriptor]
218+
-
219+
class: PHPStan\Type\Doctrine\Descriptors\DateType
220+
tags: [phpstan.doctrine.typeDescriptor]
221+
-
222+
class: PHPStan\Type\Doctrine\Descriptors\DecimalType
223+
tags: [phpstan.doctrine.typeDescriptor]
224+
-
225+
class: PHPStan\Type\Doctrine\Descriptors\FloatType
226+
tags: [phpstan.doctrine.typeDescriptor]
227+
-
228+
class: PHPStan\Type\Doctrine\Descriptors\GuidType
229+
tags: [phpstan.doctrine.typeDescriptor]
230+
-
231+
class: PHPStan\Type\Doctrine\Descriptors\IntegerType
232+
tags: [phpstan.doctrine.typeDescriptor]
233+
-
234+
class: PHPStan\Type\Doctrine\Descriptors\JsonArrayType
235+
tags: [phpstan.doctrine.typeDescriptor]
236+
-
237+
class: PHPStan\Type\Doctrine\Descriptors\JsonType
238+
tags: [phpstan.doctrine.typeDescriptor]
239+
-
240+
class: PHPStan\Type\Doctrine\Descriptors\ObjectType
241+
tags: [phpstan.doctrine.typeDescriptor]
242+
-
243+
class: PHPStan\Type\Doctrine\Descriptors\SimpleArrayType
244+
tags: [phpstan.doctrine.typeDescriptor]
245+
-
246+
class: PHPStan\Type\Doctrine\Descriptors\SmallIntType
247+
tags: [phpstan.doctrine.typeDescriptor]
248+
-
249+
class: PHPStan\Type\Doctrine\Descriptors\StringType
250+
tags: [phpstan.doctrine.typeDescriptor]
251+
-
252+
class: PHPStan\Type\Doctrine\Descriptors\TextType
253+
tags: [phpstan.doctrine.typeDescriptor]
254+
-
255+
class: PHPStan\Type\Doctrine\Descriptors\TimeImmutableType
256+
tags: [phpstan.doctrine.typeDescriptor]
257+
-
258+
class: PHPStan\Type\Doctrine\Descriptors\TimeType
259+
tags: [phpstan.doctrine.typeDescriptor]

rules.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ rules:
1717
- PHPStan\Rules\Doctrine\ORM\DqlRule
1818
- PHPStan\Rules\Doctrine\ORM\MagicRepositoryMethodCallRule
1919
- PHPStan\Rules\Doctrine\ORM\RepositoryMethodCallRule
20+
- PHPStan\Rules\Doctrine\ORM\EntityColumnRule
2021

2122
services:
2223
-
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Doctrine\ORM;
4+
5+
use PhpParser\Node;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Reflection\MissingPropertyFromReflectionException;
8+
use PHPStan\Rules\Rule;
9+
use PHPStan\Type\Doctrine\DescriptorNotRegisteredException;
10+
use PHPStan\Type\Doctrine\DescriptorRegistry;
11+
use PHPStan\Type\Doctrine\ObjectMetadataResolver;
12+
use PHPStan\Type\TypeCombinator;
13+
use PHPStan\Type\VerbosityLevel;
14+
use function sprintf;
15+
16+
class EntityColumnRule implements Rule
17+
{
18+
19+
/** @var \PHPStan\Type\Doctrine\ObjectMetadataResolver */
20+
private $objectMetadataResolver;
21+
22+
/** @var \PHPStan\Type\Doctrine\DescriptorRegistry */
23+
private $descriptorRegistry;
24+
25+
public function __construct(ObjectMetadataResolver $objectMetadataResolver, DescriptorRegistry $descriptorRegistry)
26+
{
27+
$this->objectMetadataResolver = $objectMetadataResolver;
28+
$this->descriptorRegistry = $descriptorRegistry;
29+
}
30+
31+
public function getNodeType(): string
32+
{
33+
return Node\Stmt\PropertyProperty::class;
34+
}
35+
36+
/**
37+
* @param \PhpParser\Node\Stmt\PropertyProperty $node
38+
* @param \PHPStan\Analyser\Scope $scope
39+
* @return string[]
40+
*/
41+
public function processNode(Node $node, Scope $scope): array
42+
{
43+
$class = $scope->getClassReflection();
44+
if ($class === null) {
45+
return [];
46+
}
47+
48+
$objectManager = $this->objectMetadataResolver->getObjectManager();
49+
if ($objectManager === null) {
50+
return [];
51+
}
52+
53+
$className = $class->getName();
54+
if ($objectManager->getMetadataFactory()->isTransient($className)) {
55+
return [];
56+
}
57+
58+
/** @var \Doctrine\ORM\Mapping\ClassMetadataInfo $metadata */
59+
$metadata = $objectManager->getClassMetadata($className);
60+
$classMetadataInfo = 'Doctrine\ORM\Mapping\ClassMetadataInfo';
61+
if (!$metadata instanceof $classMetadataInfo) {
62+
return [];
63+
}
64+
65+
$propertyName = (string) $node->name;
66+
try {
67+
$property = $class->getNativeProperty($propertyName);
68+
} catch (MissingPropertyFromReflectionException $e) {
69+
return [];
70+
}
71+
72+
if (!isset($metadata->fieldMappings[$propertyName])) {
73+
return [];
74+
}
75+
$fieldMapping = $metadata->fieldMappings[$propertyName];
76+
77+
$errors = [];
78+
try {
79+
$descriptor = $this->descriptorRegistry->get($fieldMapping['type']);
80+
} catch (DescriptorNotRegisteredException $e) {
81+
return [];
82+
}
83+
84+
$writableToPropertyType = $descriptor->getWritableToPropertyType();
85+
$writableToDatabaseType = $descriptor->getWritableToDatabaseType();
86+
if ($fieldMapping['nullable'] === true) {
87+
$writableToPropertyType = TypeCombinator::addNull($writableToPropertyType);
88+
$writableToDatabaseType = TypeCombinator::addNull($writableToDatabaseType);
89+
}
90+
91+
if (!$property->getWritableType()->isSuperTypeOf($writableToPropertyType)->yes()) {
92+
$errors[] = sprintf('Database can contain %s but property expects %s.', $writableToPropertyType->describe(VerbosityLevel::typeOnly()), $property->getWritableType()->describe(VerbosityLevel::typeOnly()));
93+
}
94+
if (!$writableToDatabaseType->isSuperTypeOf($property->getReadableType())->yes()) {
95+
$errors[] = sprintf('Property can contain %s but database expects %s.', $property->getReadableType()->describe(VerbosityLevel::typeOnly()), $writableToDatabaseType->describe(VerbosityLevel::typeOnly()));
96+
}
97+
return $errors;
98+
}
99+
100+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Doctrine;
4+
5+
class DescriptorNotRegisteredException extends \RuntimeException
6+
{
7+
8+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Doctrine;
4+
5+
use PHPStan\Type\Doctrine\Descriptors\DoctrineTypeDescriptor;
6+
7+
class DescriptorRegistry
8+
{
9+
10+
/** @var array<string, \PHPStan\Type\Doctrine\Descriptors\DoctrineTypeDescriptor> */
11+
private $descriptors = [];
12+
13+
/**
14+
* @param \PHPStan\Type\Doctrine\Descriptors\DoctrineTypeDescriptor[] $descriptors
15+
*/
16+
public function __construct(array $descriptors)
17+
{
18+
foreach ($descriptors as $descriptor) {
19+
$this->descriptors[$descriptor->getType()] = $descriptor;
20+
}
21+
}
22+
23+
public function get(string $type): DoctrineTypeDescriptor
24+
{
25+
if (!isset($this->descriptors[$type])) {
26+
throw new \PHPStan\Type\Doctrine\DescriptorNotRegisteredException();
27+
}
28+
return $this->descriptors[$type];
29+
}
30+
31+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Doctrine;
4+
5+
use PHPStan\DependencyInjection\Container;
6+
7+
class DescriptorRegistryFactory
8+
{
9+
10+
public const TYPE_DESCRIPTOR_TAG = 'phpstan.doctrine.typeDescriptor';
11+
12+
/** @var \PHPStan\DependencyInjection\Container */
13+
private $container;
14+
15+
public function __construct(Container $container)
16+
{
17+
$this->container = $container;
18+
}
19+
20+
public function createRegistry(): DescriptorRegistry
21+
{
22+
return new DescriptorRegistry($this->container->getServicesByTag(self::TYPE_DESCRIPTOR_TAG));
23+
}
24+
25+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Doctrine\Descriptors;
4+
5+
use PHPStan\Type\MixedType;
6+
use PHPStan\Type\Type;
7+
8+
class ArrayType implements DoctrineTypeDescriptor
9+
{
10+
11+
public function getType(): string
12+
{
13+
return 'array';
14+
}
15+
16+
public function getWritableToPropertyType(): Type
17+
{
18+
return new \PHPStan\Type\ArrayType(new MixedType(), new MixedType());
19+
}
20+
21+
public function getWritableToDatabaseType(): Type
22+
{
23+
return new \PHPStan\Type\ArrayType(new MixedType(), new MixedType());
24+
}
25+
26+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Doctrine\Descriptors;
4+
5+
use PHPStan\Type\Type;
6+
use PHPStan\Type\TypeCombinator;
7+
8+
class BigIntType implements DoctrineTypeDescriptor
9+
{
10+
11+
public function getType(): string
12+
{
13+
return 'bigint';
14+
}
15+
16+
public function getWritableToPropertyType(): Type
17+
{
18+
return new \PHPStan\Type\StringType();
19+
}
20+
21+
public function getWritableToDatabaseType(): Type
22+
{
23+
return TypeCombinator::union(new \PHPStan\Type\StringType(), new \PHPStan\Type\IntegerType());
24+
}
25+
26+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Doctrine\Descriptors;
4+
5+
use PHPStan\Type\MixedType;
6+
use PHPStan\Type\ResourceType;
7+
use PHPStan\Type\Type;
8+
9+
class BinaryType implements DoctrineTypeDescriptor
10+
{
11+
12+
public function getType(): string
13+
{
14+
return 'binary';
15+
}
16+
17+
public function getWritableToPropertyType(): Type
18+
{
19+
return new ResourceType();
20+
}
21+
22+
public function getWritableToDatabaseType(): Type
23+
{
24+
return new MixedType();
25+
}
26+
27+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Doctrine\Descriptors;
4+
5+
use PHPStan\Type\MixedType;
6+
use PHPStan\Type\ResourceType;
7+
use PHPStan\Type\Type;
8+
9+
class BlobType implements DoctrineTypeDescriptor
10+
{
11+
12+
public function getType(): string
13+
{
14+
return 'blob';
15+
}
16+
17+
public function getWritableToPropertyType(): Type
18+
{
19+
return new ResourceType();
20+
}
21+
22+
public function getWritableToDatabaseType(): Type
23+
{
24+
return new MixedType();
25+
}
26+
27+
}

0 commit comments

Comments
 (0)