diff --git a/src/Rules/Doctrine/ORM/EntityColumnRule.php b/src/Rules/Doctrine/ORM/EntityColumnRule.php index 75aa1ae6..c3d454ca 100644 --- a/src/Rules/Doctrine/ORM/EntityColumnRule.php +++ b/src/Rules/Doctrine/ORM/EntityColumnRule.php @@ -192,10 +192,21 @@ public function processNode(Node $node, Scope $scope): array } $nullable = isset($fieldMapping['nullable']) ? $fieldMapping['nullable'] === true : false; - if ($nullable) { - $writableToPropertyType = TypeCombinator::addNull($writableToPropertyType); + + // In single table inheritance, the database columns of child entities are always nullable, + // because the column is shared with the rest of the hierarchy that does not have the field set + // (see Doctrine\ORM\Tools\SchemaTool::gatherColumn). The property itself, however, may stay + // non-nullable, so the forced database nullability must not be required on the property side. + $nullabilityForcedBySingleTableInheritance = $metadata->isInheritanceTypeSingleTable() + && $metadata->parentClasses !== [] + && !isset($fieldMapping['inherited']); + + if ($nullable || $nullabilityForcedBySingleTableInheritance) { $writableToDatabaseType = TypeCombinator::addNull($writableToDatabaseType); } + if ($nullable && !$nullabilityForcedBySingleTableInheritance) { + $writableToPropertyType = TypeCombinator::addNull($writableToPropertyType); + } $phpDocType = $node->getPhpDocType(); $nativeType = $node->getNativeType() ?? new MixedType(); diff --git a/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php b/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php index a29a1d57..c6d687cc 100644 --- a/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php +++ b/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php @@ -548,6 +548,26 @@ public function testBugSingleEnum(?string $objectManagerLoader): void $this->analyse([__DIR__ . '/data/bug-single-enum.php'], []); } + /** + * @dataProvider dataObjectManagerLoader + */ + public function testSingleTableInheritance(?string $objectManagerLoader): void + { + $this->allowNullablePropertyForRequiredField = false; + $this->objectManagerLoader = $objectManagerLoader; + + $this->analyse([__DIR__ . '/data/single-table-inheritance.php'], [ + [ + 'Property PHPStan\Rules\Doctrine\ORM\SingleTableInheritance\ChildEntity::$childBrokenColumn type mapping mismatch: database can contain string but property expects int.', + 69, + ], + [ + 'Property PHPStan\Rules\Doctrine\ORM\SingleTableInheritance\ChildEntity::$childBrokenColumn type mapping mismatch: property can contain int but database expects string|null.', + 69, + ], + ]); + } + /** * @dataProvider dataObjectManagerLoader */ diff --git a/tests/Rules/Doctrine/ORM/data/single-table-inheritance.php b/tests/Rules/Doctrine/ORM/data/single-table-inheritance.php new file mode 100644 index 00000000..0a6af8c1 --- /dev/null +++ b/tests/Rules/Doctrine/ORM/data/single-table-inheritance.php @@ -0,0 +1,71 @@ +