Skip to content

Commit e4b8620

Browse files
committed
Validate entity field types against column types
1 parent 6c779cc commit e4b8620

35 files changed

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

0 commit comments

Comments
 (0)