Fix typed properties for default metadata (#7939) (#8589)

* Make Column::$type, Column::$nullable and JoinColumn::$nullable nullable by default

* Add tests for mapped typed properties (type and nullable)

* Fix Yaml driver tests and remove driver exceptions thrown too early

* Fix PHP driver tests

* Fix static PHP driver tests

* Fix XML driver tests

* Coding Standards

* Deprecate unused MappingException method

* Add manyToOne test and check nullable at the right place

* Coding Standards

* Bugfix: Temporarily change association join columns in CascadeRemoveOrderTest to circumvent new CommitOrderCalculator bug.

* phpcs

Co-authored-by: Benjamin Eberlei <kontakt@beberlei.de>
This commit is contained in:
Jakub Caban
2021-04-18 14:26:41 +02:00
committed by GitHub
parent a68aa580c5
commit a2230485b2
17 changed files with 285 additions and 25 deletions

View File

@@ -541,7 +541,7 @@
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:sequence>
<xs:attribute name="field" type="xs:NMTOKEN" use="required" />
<xs:attribute name="target-entity" type="xs:string" use="required" />
<xs:attribute name="target-entity" type="xs:string" />
<xs:attribute name="inversed-by" type="xs:NMTOKEN" />
<xs:attribute name="fetch" type="orm:fetch-type" default="LAZY" />
<xs:anyAttribute namespace="##other"/>
@@ -559,7 +559,7 @@
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:sequence>
<xs:attribute name="field" type="xs:NMTOKEN" use="required" />
<xs:attribute name="target-entity" type="xs:string" use="required" />
<xs:attribute name="target-entity" type="xs:string" />
<xs:attribute name="mapped-by" type="xs:NMTOKEN" />
<xs:attribute name="inversed-by" type="xs:NMTOKEN" />
<xs:attribute name="fetch" type="orm:fetch-type" default="LAZY" />

View File

@@ -1758,6 +1758,7 @@ class ClassMetadataInfo implements ClassMetadata
[
'name' => $this->namingStrategy->joinColumnName($mapping['fieldName'], $this->name),
'referencedColumnName' => $this->namingStrategy->referenceColumnName(),
'nullable' => true,
],
];
}
@@ -1775,6 +1776,10 @@ class ClassMetadataInfo implements ClassMetadata
}
}
if (! isset($joinColumn['nullable'])) {
$joinColumn['nullable'] = true;
}
if (empty($joinColumn['name'])) {
$joinColumn['name'] = $this->namingStrategy->joinColumnName($mapping['fieldName'], $this->name);
}

View File

@@ -35,7 +35,7 @@ final class Column implements Annotation
public $name;
/** @var mixed */
public $type = 'string';
public $type;
/** @var int */
public $length;
@@ -57,8 +57,8 @@ final class Column implements Annotation
/** @var bool */
public $unique = false;
/** @var bool */
public $nullable = false;
/** @var bool|null */
public $nullable;
/** @var array<string,mixed> */
public $options = [];
@@ -71,12 +71,12 @@ final class Column implements Annotation
*/
public function __construct(
?string $name = null,
string $type = 'string',
?string $type = null,
?int $length = null,
?int $precision = null,
?int $scale = null,
bool $unique = false,
bool $nullable = false,
?bool $nullable = null,
array $options = [],
?string $columnDefinition = null
) {

View File

@@ -370,10 +370,6 @@ class AnnotationDriver extends AbstractAnnotationDriver
// @Column, @OneToOne, @OneToMany, @ManyToOne, @ManyToMany
$columnAnnot = $this->reader->getPropertyAnnotation($property, Mapping\Column::class);
if ($columnAnnot) {
if ($columnAnnot->type === null) {
throw MappingException::propertyTypeIsRequired($className, $property->getName());
}
$mapping = $this->columnToArray($property->getName(), $columnAnnot);
$idAnnot = $this->reader->getPropertyAnnotation($property, Mapping\Id::class);

View File

@@ -258,10 +258,6 @@ class AttributeDriver extends AnnotationDriver
$embeddedAttribute = $this->reader->getPropertyAnnotation($property, Mapping\Embedded::class);
if ($columnAttribute !== null) {
if ($columnAttribute->type === null) {
throw MappingException::propertyTypeIsRequired($className, $property->getName());
}
$mapping = $this->columnToArray($property->getName(), $columnAttribute);
if ($this->reader->getPropertyAnnotation($property, Mapping\Id::class)) {

View File

@@ -409,9 +409,12 @@ class XmlDriver extends FileDriver
foreach ($xmlRoot->{'one-to-one'} as $oneToOneElement) {
$mapping = [
'fieldName' => (string) $oneToOneElement['field'],
'targetEntity' => (string) $oneToOneElement['target-entity'],
];
if (isset($oneToOneElement['target-entity'])) {
$mapping['targetEntity'] = (string) $oneToOneElement['target-entity'];
}
if (isset($associationIds[$mapping['fieldName']])) {
$mapping['id'] = true;
}
@@ -462,10 +465,13 @@ class XmlDriver extends FileDriver
foreach ($xmlRoot->{'one-to-many'} as $oneToManyElement) {
$mapping = [
'fieldName' => (string) $oneToManyElement['field'],
'targetEntity' => (string) $oneToManyElement['target-entity'],
'mappedBy' => (string) $oneToManyElement['mapped-by'],
];
if (isset($oneToManyElement['target-entity'])) {
$mapping['targetEntity'] = (string) $oneToManyElement['target-entity'];
}
if (isset($oneToManyElement['fetch'])) {
$mapping['fetch'] = constant('Doctrine\ORM\Mapping\ClassMetadata::FETCH_' . (string) $oneToManyElement['fetch']);
}
@@ -509,9 +515,12 @@ class XmlDriver extends FileDriver
foreach ($xmlRoot->{'many-to-one'} as $manyToOneElement) {
$mapping = [
'fieldName' => (string) $manyToOneElement['field'],
'targetEntity' => (string) $manyToOneElement['target-entity'],
];
if (isset($manyToOneElement['target-entity'])) {
$mapping['targetEntity'] = (string) $manyToOneElement['target-entity'];
}
if (isset($associationIds[$mapping['fieldName']])) {
$mapping['id'] = true;
}
@@ -554,9 +563,12 @@ class XmlDriver extends FileDriver
foreach ($xmlRoot->{'many-to-many'} as $manyToManyElement) {
$mapping = [
'fieldName' => (string) $manyToManyElement['field'],
'targetEntity' => (string) $manyToManyElement['target-entity'],
];
if (isset($manyToManyElement['target-entity'])) {
$mapping['targetEntity'] = (string) $manyToManyElement['target-entity'];
}
if (isset($manyToManyElement['fetch'])) {
$mapping['fetch'] = constant('Doctrine\ORM\Mapping\ClassMetadata::FETCH_' . (string) $manyToManyElement['fetch']);
}

View File

@@ -420,7 +420,7 @@ class YamlDriver extends FileDriver
foreach ($element['oneToOne'] as $name => $oneToOneElement) {
$mapping = [
'fieldName' => $name,
'targetEntity' => $oneToOneElement['targetEntity'],
'targetEntity' => $oneToOneElement['targetEntity'] ?? null,
];
if (isset($associationIds[$mapping['fieldName']])) {
@@ -515,7 +515,7 @@ class YamlDriver extends FileDriver
foreach ($element['manyToOne'] as $name => $manyToOneElement) {
$mapping = [
'fieldName' => $name,
'targetEntity' => $manyToOneElement['targetEntity'],
'targetEntity' => $manyToOneElement['targetEntity'] ?? null,
];
if (isset($associationIds[$mapping['fieldName']])) {

View File

@@ -40,8 +40,8 @@ final class JoinColumn implements Annotation
/** @var bool */
public $unique = false;
/** @var bool */
public $nullable = true;
/** @var bool|null */
public $nullable;
/** @var mixed */
public $onDelete;
@@ -60,7 +60,7 @@ final class JoinColumn implements Annotation
?string $name = null,
string $referencedColumnName = 'id',
bool $unique = false,
bool $nullable = true,
?bool $nullable = null,
$onDelete = null,
?string $columnDefinition = null,
?string $fieldName = null

View File

@@ -52,7 +52,7 @@ final class ManyToOne implements Annotation
* @param array<string> $cascade
*/
public function __construct(
string $targetEntity,
?string $targetEntity = null,
?array $cascade = null,
string $fetch = 'LAZY',
?string $inversedBy = null

View File

@@ -382,6 +382,8 @@ class MappingException extends ORMException
}
/**
* @deprecated 2.9 no longer in use
*
* @param string $className
* @param string $propertyName
*

View File

@@ -7,46 +7,129 @@ namespace Doctrine\Tests\Models\TypedProperties;
use DateInterval;
use DateTime;
use DateTimeImmutable;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
use Doctrine\Tests\Models\CMS\CmsEmail;
/**
* @Entity
* @Table(name="cms_users_typed")
*/
#[ORM\Entity, ORM\Table(name: 'cms_users_typed')]
class UserTyped
{
/**
* @Id @Column
* @GeneratedValue
*/
#[ORM\Id, ORM\Column, ORM\GeneratedValue]
public int $id;
/** @Column(length=50) */
#[ORM\Column(length: 50)]
public ?string $status;
/** @Column(length=255, unique=true) */
#[ORM\Column(length: 255, unique: true)]
public string $username;
/** @Column */
#[ORM\Column]
public DateInterval $dateInterval;
/** @Column */
#[ORM\Column]
public DateTime $dateTime;
/** @Column */
#[ORM\Column]
public DateTimeImmutable $dateTimeImmutable;
/** @Column */
#[ORM\Column]
public array $array;
/** @Column */
#[ORM\Column]
public bool $boolean;
/** @Column */
#[ORM\Column]
public float $float;
/**
* @OneToOne(cascade={"persist"}, orphanRemoval=true)
* @JoinColumn
*/
#[ORM\OneToOne(cascade: ['persist'], orphanRemoval: true), ORM\JoinColumn]
public CmsEmail $email;
/** @ManyToOne */
#[ORM\ManyToOne]
public ?CmsEmail $mainEmail;
public static function loadMetadata(ClassMetadataInfo $metadata): void
{
$metadata->setInheritanceType(ClassMetadataInfo::INHERITANCE_TYPE_NONE);
$metadata->setPrimaryTable(
['name' => 'cms_users_typed']
);
$metadata->mapField(
[
'id' => true,
'fieldName' => 'id',
]
);
$metadata->setIdGeneratorType(ClassMetadataInfo::GENERATOR_TYPE_AUTO);
$metadata->mapField(
[
'fieldName' => 'status',
'length' => 50,
]
);
$metadata->mapField(
[
'fieldName' => 'username',
'length' => 255,
'unique' => true,
]
);
$metadata->mapField(
['fieldName' => 'dateInterval']
);
$metadata->mapField(
['fieldName' => 'dateTime']
);
$metadata->mapField(
['fieldName' => 'dateTimeImmutable']
);
$metadata->mapField(
['fieldName' => 'array']
);
$metadata->mapField(
['fieldName' => 'boolean']
);
$metadata->mapField(
['fieldName' => 'float']
);
$metadata->mapOneToOne(
[
'fieldName' => 'email',
'cascade' =>
[0 => 'persist'],
'joinColumns' =>
[
0 =>
[],
],
'orphanRemoval' => true,
]
);
$metadata->mapManyToOne(
['fieldName' => 'mainEmail']
);
}
}

View File

@@ -158,6 +158,7 @@ class CascadeRemoveOrderEntityG
* targetEntity="Doctrine\Tests\ORM\Functional\CascadeRemoveOrderEntityO",
* inversedBy="oneToMany"
* )
* @JoinColumn(nullable=true, onDelete="SET NULL")
*/
private $ownerO;

View File

@@ -21,6 +21,7 @@ use Doctrine\Persistence\Mapping\RuntimeReflectionService;
use Doctrine\Tests\Models\Cache\City;
use Doctrine\Tests\Models\CMS\CmsAddress;
use Doctrine\Tests\Models\CMS\CmsAddressListener;
use Doctrine\Tests\Models\CMS\CmsEmail;
use Doctrine\Tests\Models\CMS\CmsUser;
use Doctrine\Tests\Models\Company\CompanyContract;
use Doctrine\Tests\Models\Company\CompanyContractListener;
@@ -41,6 +42,7 @@ use Doctrine\Tests\Models\DDC889\DDC889Class;
use Doctrine\Tests\Models\DDC889\DDC889Entity;
use Doctrine\Tests\Models\DDC964\DDC964Admin;
use Doctrine\Tests\Models\DDC964\DDC964Guest;
use Doctrine\Tests\Models\TypedProperties\UserTyped;
use Doctrine\Tests\OrmTestCase;
use function assert;
@@ -51,6 +53,7 @@ use function strpos;
use function strtolower;
use const CASE_UPPER;
use const PHP_VERSION_ID;
abstract class AbstractMappingDriverTest extends OrmTestCase
{
@@ -262,6 +265,46 @@ abstract class AbstractMappingDriverTest extends OrmTestCase
return $class;
}
public function testFieldIsNullableByType(): void
{
if (PHP_VERSION_ID < 70400) {
$this->markTestSkipped('requies PHP 7.4');
}
$class = $this->createClassMetadata(UserTyped::class);
// Explicit Nullable
$this->assertTrue($class->isNullable('status'));
// Explicit Not Nullable
$this->assertFalse($class->isNullable('username'));
// Join table Nullable
$this->assertFalse($class->getAssociationMapping('email')['joinColumns'][0]['nullable']);
$this->assertEquals(CmsEmail::class, $class->getAssociationMapping('email')['targetEntity']);
$this->assertTrue($class->getAssociationMapping('mainEmail')['joinColumns'][0]['nullable']);
$this->assertEquals(CmsEmail::class, $class->getAssociationMapping('mainEmail')['targetEntity']);
}
public function testFieldTypeFromReflection(): void
{
if (PHP_VERSION_ID < 70400) {
$this->markTestSkipped('requies PHP 7.4');
}
$class = $this->createClassMetadata(UserTyped::class);
$this->assertEquals('integer', $class->getTypeOfField('id'));
$this->assertEquals('string', $class->getTypeOfField('username'));
$this->assertEquals('dateinterval', $class->getTypeOfField('dateInterval'));
$this->assertEquals('datetime', $class->getTypeOfField('dateTime'));
$this->assertEquals('datetime_immutable', $class->getTypeOfField('dateTimeImmutable'));
$this->assertEquals('json', $class->getTypeOfField('array'));
$this->assertEquals('boolean', $class->getTypeOfField('boolean'));
$this->assertEquals('float', $class->getTypeOfField('float'));
}
/**
* @depends testEntityTableNameAndInheritance
*/

View File

@@ -132,6 +132,10 @@ class ClassMetadataTest extends OrmTestCase
$cm->mapOneToOne(['fieldName' => 'email', 'joinColumns' => [[]]]);
$this->assertFalse($cm->getAssociationMapping('email')['joinColumns'][0]['nullable']);
$this->assertEquals(CmsEmail::class, $cm->getAssociationMapping('email')['targetEntity']);
$cm->mapManyToOne(['fieldName' => 'mainEmail']);
$this->assertTrue($cm->getAssociationMapping('mainEmail')['joinColumns'][0]['nullable']);
$this->assertEquals(CmsEmail::class, $cm->getAssociationMapping('mainEmail')['targetEntity']);
}
public function testFieldTypeFromReflection(): void

View File

@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
use Doctrine\ORM\Mapping\ClassMetadataInfo;
$metadata->setInheritanceType(ClassMetadataInfo::INHERITANCE_TYPE_NONE);
$metadata->setPrimaryTable(
['name' => 'cms_users_typed']
);
$metadata->mapField(
[
'id' => true,
'fieldName' => 'id',
]
);
$metadata->setIdGeneratorType(ClassMetadataInfo::GENERATOR_TYPE_AUTO);
$metadata->mapField(
[
'fieldName' => 'status',
'length' => 50,
]
);
$metadata->mapField(
[
'fieldName' => 'username',
'length' => 255,
'unique' => true,
]
);
$metadata->mapField(
['fieldName' => 'dateInterval']
);
$metadata->mapField(
['fieldName' => 'dateTime']
);
$metadata->mapField(
['fieldName' => 'dateTimeImmutable']
);
$metadata->mapField(
['fieldName' => 'array']
);
$metadata->mapField(
['fieldName' => 'boolean']
);
$metadata->mapField(
['fieldName' => 'float']
);
$metadata->mapOneToOne(
[
'fieldName' => 'email',
'cascade' => [0 => 'persist'],
'joinColumns' => [[]],
'orphanRemoval' => true,
]
);
$metadata->mapManyToOne(
['fieldName' => 'mainEmail']
);

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
https://www.doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
<entity name="Doctrine\Tests\Models\TypedProperties\UserTyped" table="cms_users_typed">
<id name="id">
<generator strategy="AUTO"/>
</id>
<field name="status" length="50"/>
<field name="username" length="255" unique="true"/>
<field name="dateInterval"/>
<field name="dateTime"/>
<field name="dateTimeImmutable"/>
<field name="array"/>
<field name="boolean"/>
<field name="float"/>
<one-to-one field="email" orphan-removal="true">
<cascade><cascade-persist /></cascade>
<join-column/>
</one-to-one>
<many-to-one field="mainEmail"/>
</entity>
</doctrine-mapping>

View File

@@ -0,0 +1,26 @@
Doctrine\Tests\Models\TypedProperties\UserTyped:
type: entity
table: cms_users_typed
id:
id:
generator:
strategy: AUTO
fields:
status:
length: 50
username:
length: 255
unique: true
dateInterval: ~
dateTime: ~
dateTimeImmutable: ~
array: ~
boolean: ~
float: ~
oneToOne:
email:
cascade: [ persist ]
orphanRemoval: true
joinColumn: []
manyToOne:
mainEmail: []