Skip to content

Commit

Permalink
Use typed properties for default metadata
Browse files Browse the repository at this point in the history
  • Loading branch information
franmomu committed Feb 20, 2022
1 parent 9d22a8f commit 5cb2d81
Show file tree
Hide file tree
Showing 9 changed files with 298 additions and 5 deletions.
5 changes: 4 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@
}
},
"config": {
"sort-packages": true
"sort-packages": true,
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true
}
}
}
9 changes: 6 additions & 3 deletions docs/en/reference/annotations-reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,8 @@ following excerpt from the MongoDB documentation:
Optional attributes:

-
``targetDocument`` - A |FQCN| of the target document.
``targetDocument`` - A |FQCN| of the target document. When typed properties
are used it is inherited from PHP type.
-
``discriminatorField`` - The database field name to store the discriminator
value within the embedded document.
Expand Down Expand Up @@ -356,7 +357,8 @@ Optional attributes:
-
``type`` - Name of the ODM type, which will determine the value's
representation in PHP and BSON (i.e. MongoDB). See
:ref:`doctrine_mapping_types` for a list of types. Defaults to "string".
:ref:`doctrine_mapping_types` for a list of types. Defaults to "string" or
:ref:`Type from PHP property type <reference-php-mapping-types>`.
-
``name`` - By default, the property name is used for the field name in
MongoDB; however, this option may be used to specify a database field name.
Expand Down Expand Up @@ -1002,7 +1004,8 @@ Optional attributes:

-
``targetDocument`` - A |FQCN| of the target document. A ``targetDocument``
is required when using ``storeAs: id``.
is required when using ``storeAs: id``. When typed properties are used
it is inherited from PHP type.
-
``storeAs`` - Indicates how to store the reference. ``id`` stores the
identifier, ``ref`` an embedded object containing the ``id`` field and
Expand Down
17 changes: 17 additions & 0 deletions docs/en/reference/basic-mapping.rst
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,23 @@ to use with the ``setDefaultDB`` method:
$config->setDefaultDB('my_db');
.. _reference-php-mapping-types:

PHP Types Mapping
_________________

Since version 2.4 Doctrine can determine usable defaults from property types
on document classes. Doctrine will map PHP types to ``type`` attribute as
follows:

- ``DateTime``: ``date``
- ``DateTimeImmutable``: ``date_immutable``
- ``array``: ``hash``
- ``bool``: ``bool``
- ``float``: ``float``
- ``int``: ``int``
- ``string`` or any other type: ``string``

.. _doctrine_mapping_types:

Doctrine Mapping Types
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ abstract class AbstractField implements Annotation
*/
public function __construct(
?string $name = null,
?string $type = 'string',
?string $type = null,
bool $nullable = false,
array $options = [],
?string $strategy = null,
Expand Down
97 changes: 97 additions & 0 deletions lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
namespace Doctrine\ODM\MongoDB\Mapping;

use BadMethodCallException;
use DateTime;
use DateTimeImmutable;
use Doctrine\Instantiator\Instantiator;
use Doctrine\Instantiator\InstantiatorInterface;
use Doctrine\ODM\MongoDB\Id\IdGenerator;
Expand All @@ -20,6 +22,7 @@
use LogicException;
use ProxyManager\Proxy\GhostObjectInterface;
use ReflectionClass;
use ReflectionNamedType;
use ReflectionProperty;

use function array_filter;
Expand All @@ -43,6 +46,8 @@
use function strtoupper;
use function trigger_deprecation;

use const PHP_VERSION_ID;

/**
* A <tt>ClassMetadata</tt> instance holds all the object-document mapping metadata
* of a document and it's references.
Expand Down Expand Up @@ -1525,6 +1530,11 @@ public function mapOneEmbedded(array $mapping): void
{
$mapping['embedded'] = true;
$mapping['type'] = self::ONE;

if ($this->isTypedProperty($mapping['fieldName'])) {
$mapping = $this->validateAndCompleteTypedAssociationMapping($mapping);
}

$this->mapField($mapping);
}

Expand All @@ -1549,6 +1559,11 @@ public function mapOneReference(array $mapping): void
{
$mapping['reference'] = true;
$mapping['type'] = self::ONE;

if ($this->isTypedProperty($mapping['fieldName'])) {
$mapping = $this->validateAndCompleteTypedAssociationMapping($mapping);
}

$this->mapField($mapping);
}

Expand Down Expand Up @@ -2195,6 +2210,19 @@ public function mapField(array $mapping): array
unset($this->generatorOptions['type']);
}

if ($this->isTypedProperty($mapping['fieldName'])) {
$mapping = $this->validateAndCompleteTypedFieldMapping($mapping);

if (isset($mapping['type']) && $mapping['type'] === self::ONE) {
$mapping = $this->validateAndCompleteTypedAssociationMapping($mapping);
}
}

if (! isset($mapping['type'])) {
// Default to string
$mapping['type'] = Type::STRING;
}

if (! isset($mapping['nullable'])) {
$mapping['nullable'] = false;
}
Expand Down Expand Up @@ -2505,4 +2533,73 @@ private function checkDuplicateMapping(array $mapping): void
throw MappingException::duplicateDatabaseFieldName($this->getName(), $mapping['fieldName'], $mapping['name'], $fieldName);
}
}

private function isTypedProperty(string $name): bool
{
return PHP_VERSION_ID >= 70400
&& $this->reflClass->hasProperty($name)
&& $this->reflClass->getProperty($name)->hasType();
}

/**
* Validates & completes the given field mapping based on typed property.
*
* @psalm-param FieldMappingConfig $mapping
*
* @return FieldMappingConfig
*/
private function validateAndCompleteTypedFieldMapping(array $mapping): array
{
$type = $this->reflClass->getProperty($mapping['fieldName'])->getType();

if ($type instanceof ReflectionNamedType && ! isset($mapping['type'])) {
switch ($type->getName()) {
case DateTime::class:
$mapping['type'] = Type::DATE;
break;
case DateTimeImmutable::class:
$mapping['type'] = Type::DATE_IMMUTABLE;
break;
case 'array':
$mapping['type'] = Type::HASH;
break;
case 'bool':
$mapping['type'] = Type::BOOL;
break;
case 'float':
$mapping['type'] = Type::FLOAT;
break;
case 'int':
$mapping['type'] = Type::INT;
break;
case 'string':
$mapping['type'] = Type::STRING;
break;
}
}

return $mapping;
}

/**
* Validates & completes the basic mapping information based on typed property.
*
* @psalm-param FieldMappingConfig $mapping
*
* @return FieldMappingConfig
*/
private function validateAndCompleteTypedAssociationMapping(array $mapping): array
{
$type = $this->reflClass->getProperty($mapping['fieldName'])->getType();

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

return $mapping;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@
use Doctrine\ODM\MongoDB\Repository\DocumentRepository;
use Doctrine\ODM\MongoDB\Repository\ViewRepository;
use Doctrine\ODM\MongoDB\Tests\BaseTest;
use Doctrine\ODM\MongoDB\Types\Type;
use Doctrine\Persistence\Mapping\Driver\MappingDriver;
use Documents74\TypedEmbeddedDocument;
use Documents74\UserTyped;
use InvalidArgumentException;

use function key;
Expand Down Expand Up @@ -194,6 +197,25 @@ public function testIdentifier(ClassMetadata $class): ClassMetadata
return $class;
}

/**
* @requires PHP >= 7.4
*/
public function testFieldTypeFromReflection(): void
{
$class = $this->dm->getClassMetadata(UserTyped::class);

$this->assertSame(Type::ID, $class->getTypeOfField('id'));
$this->assertSame(Type::STRING, $class->getTypeOfField('username'));
$this->assertSame(Type::DATE, $class->getTypeOfField('dateTime'));
$this->assertSame(Type::DATE_IMMUTABLE, $class->getTypeOfField('dateTimeImmutable'));
$this->assertSame(Type::HASH, $class->getTypeOfField('array'));
$this->assertSame(Type::BOOL, $class->getTypeOfField('boolean'));
$this->assertSame(Type::FLOAT, $class->getTypeOfField('float'));

$this->assertSame(TypedEmbeddedDocument::class, $class->getAssociationTargetClass('embedOne'));
$this->assertSame(UserTyped::class, $class->getAssociationTargetClass('referenceOne'));
}

/**
* @param ClassMetadata<AbstractMappingDriverUser> $class
*
Expand Down
40 changes: 40 additions & 0 deletions tests/Doctrine/ODM/MongoDB/Tests/Mapping/ClassMetadataTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
use Documents\User;
use Documents\UserName;
use Documents\UserRepository;
use Documents74\TypedEmbeddedDocument;
use Documents74\UserTyped;
use Generator;
use InvalidArgumentException;
use ProxyManager\Proxy\GhostObjectInterface;
Expand Down Expand Up @@ -136,6 +138,44 @@ public function testFieldIsNullable(): void
$this->assertFalse($cm->isNullable('name'), 'By default a field should not be nullable.');
}

/**
* @requires PHP >= 7.4
*/
public function testFieldTypeFromReflection(): void
{
$cm = new ClassMetadata(UserTyped::class);

// String
$cm->mapField(['fieldName' => 'username', 'length' => 50]);
self::assertEquals(Type::STRING, $cm->getTypeOfField('username'));

// DateTime object
$cm->mapField(['fieldName' => 'dateTime']);
self::assertEquals(Type::DATE, $cm->getTypeOfField('dateTime'));

// DateTimeImmutable object
$cm->mapField(['fieldName' => 'dateTimeImmutable']);
self::assertEquals(Type::DATE_IMMUTABLE, $cm->getTypeOfField('dateTimeImmutable'));

// array as hash
$cm->mapField(['fieldName' => 'array']);
self::assertEquals(Type::HASH, $cm->getTypeOfField('array'));

// bool
$cm->mapField(['fieldName' => 'boolean']);
self::assertEquals(Type::BOOL, $cm->getTypeOfField('boolean'));

// float
$cm->mapField(['fieldName' => 'float']);
self::assertEquals(Type::FLOAT, $cm->getTypeOfField('float'));

$cm->mapOneEmbedded(['fieldName' => 'embedOne']);
self::assertEquals(TypedEmbeddedDocument::class, $cm->getAssociationTargetClass('embedOne'));

$cm->mapOneReference(['fieldName' => 'referenceOne']);
self::assertEquals(UserTyped::class, $cm->getAssociationTargetClass('referenceOne'));
}

/**
* @group DDC-115
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>

<doctrine-mongo-mapping xmlns="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping
http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping.xsd">

<document name="Documents74\UserTyped">
<id field-name="id" />

<field name="username"/>
<field name="dateTime"/>
<field name="dateTimeImmutable"/>
<field name="array"/>
<field name="boolean"/>
<field name="float"/>

<embed-one field="embedOne" />
<reference-one field="referenceOne" />
</document>
</doctrine-mongo-mapping>
Loading

0 comments on commit 5cb2d81

Please sign in to comment.