Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions src/Rules/Doctrine/ORM/EntityColumnRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
20 changes: 20 additions & 0 deletions tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
71 changes: 71 additions & 0 deletions tests/Rules/Doctrine/ORM/data/single-table-inheritance.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Doctrine\ORM\SingleTableInheritance;

use Doctrine\ORM\Mapping as ORM;

/**
* @ORM\Entity()
* @ORM\InheritanceType("SINGLE_TABLE")
* @ORM\DiscriminatorColumn(name="discr", type="string")
* @ORM\DiscriminatorMap({
* "base"="PHPStan\Rules\Doctrine\ORM\SingleTableInheritance\BaseEntity",
* "child"="PHPStan\Rules\Doctrine\ORM\SingleTableInheritance\ChildEntity"
* })
*/
class BaseEntity
{

/**
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer")
*
* @var int
*/
private $id;

/**
* @ORM\Column(type="string")
*
* @var string
*/
private $baseColumn;

}

/**
* @ORM\Entity()
*/
class ChildEntity extends BaseEntity
{

/**
* The column must be nullable in the database because it is shared with the rest of the
* single table inheritance hierarchy, but the property itself is always set.
*
* @ORM\Column(type="string", nullable=true)
*
* @var string
*/
private $childColumn;

/**
* Nullable property is fine too.
*
* @ORM\Column(type="string", nullable=true)
*
* @var string|null
*/
private $childNullableColumn;

/**
* Genuine type mismatches in child entities are still reported.
*
* @ORM\Column(type="string", nullable=true)
*
* @var int
*/
private $childBrokenColumn;

}
Loading