Skip to content

Commit

Permalink
Use typed properties for default metadata for #7939
Browse files Browse the repository at this point in the history
  • Loading branch information
Lustmored committed Jan 26, 2021
1 parent f0ad5f7 commit c776207
Show file tree
Hide file tree
Showing 3 changed files with 223 additions and 0 deletions.
78 changes: 78 additions & 0 deletions lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
namespace Doctrine\ORM\Mapping;

use BadMethodCallException;
use DateInterval;
use DateTime;
use DateTimeImmutable;
use Doctrine\DBAL\Types\Type;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\Instantiator\Instantiator;
Expand All @@ -28,11 +31,14 @@
use Doctrine\Persistence\Mapping\ReflectionService;
use InvalidArgumentException;
use ReflectionClass;
use ReflectionNamedType;
use ReflectionProperty;
use RuntimeException;
use function count;
use function explode;

use const PHP_VERSION_ID;

/**
* A <tt>ClassMetadata</tt> instance holds all the object-relational mapping metadata
* of an entity and its associations.
Expand Down Expand Up @@ -1414,6 +1420,53 @@ protected function _validateAndCompleteFieldMapping(array &$mapping)
throw MappingException::missingFieldName($this->name);
}

if (
PHP_VERSION_ID >= 70400
&& isset($this->reflClass)
&& $this->reflClass->hasProperty($mapping['fieldName'])
) {
$property = $this->reflClass->getProperty($mapping['fieldName']);
$type = $property->getType();

if ($type) {
if (!isset($mapping['nullable'])) {
$mapping['nullable'] = $type->allowsNull();
}

if (
!isset($mapping['type'])
&& ($type instanceof ReflectionNamedType)
) {
switch($type->getName()) {
case DateInterval::class:
$mapping['type'] = 'dateinterval';
break;
case DateTime::class:
$mapping['type'] = 'datetime';
break;
case DateTimeImmutable::class:
$mapping['type'] = 'datetime_immutable';
break;
case 'array':
$mapping['type'] = 'json';
break;
case 'bool':
$mapping['type'] = 'boolean';
break;
case 'float':
$mapping['type'] = 'float';
break;
case 'int':
$mapping['type'] = 'integer';
break;
case 'string':
$mapping['type'] = 'string';
break;
}
}
}
}

if ( ! isset($mapping['type'])) {
// Default to string
$mapping['type'] = 'string';
Expand Down Expand Up @@ -1511,6 +1564,31 @@ protected function _validateAndCompleteAssociationMapping(array $mapping)
// the sourceEntity.
$mapping['sourceEntity'] = $this->name;

if (
PHP_VERSION_ID >= 70400
&& isset($this->reflClass)
&& $this->reflClass->hasProperty($mapping['fieldName'])
) {
$property = $this->reflClass->getProperty($mapping['fieldName']);
$type = $property->getType();

if (
! isset($mapping['targetEntity'])
&& ($mapping['type'] & self::TO_ONE) > 0
&& $type instanceof ReflectionNamedType
) {
$mapping['targetEntity'] = $type->getName();
}

if ($type !== null && isset($mapping['joinColumns'])) {
foreach ($mapping['joinColumns'] as &$joinColumn) {
if (!isset($joinColumn['nullable'])) {
$joinColumn['nullable'] = $type->allowsNull();
}
}
}
}

if (isset($mapping['targetEntity'])) {
$mapping['targetEntity'] = $this->fullyQualifiedClassName($mapping['targetEntity']);
$mapping['targetEntity'] = ltrim($mapping['targetEntity'], '\\');
Expand Down
71 changes: 71 additions & 0 deletions tests/Doctrine/Tests/Models/CMS/CmsUserTyped.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);

namespace Doctrine\Tests\Models\CMS;

use DateInterval;
use DateTime;
use DateTimeImmutable;

/**
* @Entity
* @Table(name="cms_users_typed")
*/
class CmsUserTyped
{
/**
* @Id @Column
* @GeneratedValue
*/
public int $id;
/**
* @Column(length=50)
*/
public ?string $status;

/**
* @Column(length=255, unique=true)
*/
public string $username;

/**
* @Column(type="string", length=255)
*/
public $name;

/**
* @Column
*/
public DateInterval $dateInterval;

/**
* @Column
*/
public DateTime $dateTime;

/**
* @Column
*/
public DateTimeImmutable $dateTimeImmutable;

/**
* @Column
*/
public array $array;

/**
* @Column
*/
public bool $boolean;

/**
* @Column
*/
public float $float;

/**
* @OneToOne(cascade={"persist"}, orphanRemoval=true)
* @JoinColumn
*/
public CmsEmail $email;
}
74 changes: 74 additions & 0 deletions tests/Doctrine/Tests/ORM/Mapping/ClassMetadataTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
use Doctrine\Tests\OrmTestCase;
use DoctrineGlobal_Article;

use const PHP_VERSION_ID;

require_once __DIR__ . '/../../Models/Global/GlobalNamespaceModel.php';

class ClassMetadataTest extends OrmTestCase
Expand Down Expand Up @@ -92,6 +94,78 @@ public function testFieldIsNullable()
$this->assertFalse($cm->isNullable('name'), "By default a field should not be nullable.");
}

public function testFieldIsNullableByType()
{
if (PHP_VERSION_ID < 70400) {
$this->markTestSkipped('requies PHP 7.4');
}

$cm = new ClassMetadata(CMS\CmsUserTyped::class);
$cm->initializeReflection(new RuntimeReflectionService());

// Explicit Nullable
$cm->mapField(['fieldName' => 'status', 'length' => 50]);
$this->assertTrue($cm->isNullable('status'));

// Explicit Not Nullable
$cm->mapField(['fieldName' => 'username', 'length' => 50]);
$this->assertFalse($cm->isNullable('username'));

// Implicit Not Nullable
$cm->mapField(['fieldName' => 'name', 'type' => 'string', 'length' => 50]);
$this->assertFalse($cm->isNullable('name'), "By default a field should not be nullable.");

// Join table Nullable
$cm->mapOneToOne(['fieldName' => 'email', 'joinColumns' => [[]]]);
$this->assertFalse($cm->getAssociationMapping('email')['joinColumns'][0]['nullable']);
}

public function testFieldTypeFromReflection()
{
if (PHP_VERSION_ID < 70400) {
$this->markTestSkipped('requies PHP 7.4');
}

$cm = new ClassMetadata(CMS\CmsUserTyped::class);
$cm->initializeReflection(new RuntimeReflectionService());

// Integer
$cm->mapField(['fieldName' => 'id']);
$this->assertEquals('integer', $cm->getTypeOfField('id'));

// String
$cm->mapField(['fieldName' => 'username', 'length' => 50]);
$this->assertEquals('string', $cm->getTypeOfField('username'));

// Default string fallback
$cm->mapField(['fieldName' => 'name', 'type' => 'string', 'length' => 50]);
$this->assertEquals('string', $cm->getTypeOfField('name'), "By default a field should be string.");

// String
$cm->mapField(['fieldName' => 'dateInterval']);
$this->assertEquals('dateinterval', $cm->getTypeOfField('dateInterval'));

// String
$cm->mapField(['fieldName' => 'dateTime']);
$this->assertEquals('datetime', $cm->getTypeOfField('dateTime'));

// String
$cm->mapField(['fieldName' => 'dateTimeImmutable']);
$this->assertEquals('datetime_immutable', $cm->getTypeOfField('dateTimeImmutable'));

// String
$cm->mapField(['fieldName' => 'array']);
$this->assertEquals('json', $cm->getTypeOfField('array'));

// String
$cm->mapField(['fieldName' => 'boolean']);
$this->assertEquals('boolean', $cm->getTypeOfField('boolean'));

// String
$cm->mapField(['fieldName' => 'float']);
$this->assertEquals('float', $cm->getTypeOfField('float'));
}

/**
* @group DDC-115
*/
Expand Down

0 comments on commit c776207

Please sign in to comment.