Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use typed properties for default metadata #2411

Merged
merged 1 commit into from
Mar 8, 2022
Merged
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: 15 additions & 0 deletions UPGRADE-2.4.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# UPGRADE FROM 2.3 to 2.4

## Typed properties as default mapping metadata

When using typed properties on Document classes, Doctrine will use these types to set defaults mapping types.

If you have defined some properties like:

```php
#[Field]
private int $myProp;
```

This property will be stored in DB as `string` but casted back to `int`. Please note that at this
time, due to backward compatibility reasons, nullable type does not imply `nullable` mapping.
15 changes: 10 additions & 5 deletions docs/en/reference/annotations-reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,8 @@ Optional attributes:
information.
-
``collectionClass`` - A |FQCN| of class that implements ``Collection``
interface and is used to hold documents. Doctrine's ``ArrayCollection`` is
interface and is used to hold documents. When typed properties
are used it is inherited from PHP type, otherwise Doctrine's ``ArrayCollection`` is
used by default.
-
``notSaved`` - The property is loaded if it exists in the database; however,
Expand Down Expand Up @@ -257,7 +258,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 +358,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 @@ -961,7 +964,8 @@ Optional attributes:
information.
-
``collectionClass`` - A |FQCN| of class that implements ``Collection``
interface and is used to hold documents. Doctrine's ``ArrayCollection`` is
interface and is used to hold documents. When typed properties
are used it is inherited from PHP type, otherwise Doctrine's ``ArrayCollection`` is
used by default
-
``prime`` - A list of references contained in the target document that will
Expand Down Expand Up @@ -1002,7 +1006,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
19 changes: 19 additions & 0 deletions docs/en/reference/basic-mapping.rst
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,25 @@ This list explains some of the less obvious mapping types:
suitable you should either use an embedded document or use formats provided
by the MongoDB driver (e.g. ``\MongoDB\BSON\UTCDateTime`` instead of ``\DateTime``).

.. _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``: ``string``

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd add similar note about nullable as in upgrade docs as the behaviour is not fully intuitional

Please note that at this time, due to backward compatibility reasons, nullable type does not imply `nullable` mapping.

Property Mapping
----------------

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
107 changes: 107 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['type'] === self::MANY)) {
$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,83 @@ 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'])) {
return $mapping;
}

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 (! $type instanceof ReflectionNamedType) {
return $mapping;
}

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

if (
! isset($mapping['collectionClass'])
&& $mapping['type'] === self::MANY
&& class_exists($type->getName())
) {
$mapping['collectionClass'] = $type->getName();
}

return $mapping;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@
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\CustomCollection;
use Documents74\TypedEmbeddedDocument;
use Documents74\UserTyped;
use InvalidArgumentException;

use function key;
Expand Down Expand Up @@ -194,6 +198,28 @@ 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'));

$this->assertSame(CustomCollection::class, $class->getAssociationCollectionClass('embedMany'));
$this->assertSame(CustomCollection::class, $class->getAssociationCollectionClass('referenceMany'));
IonBazan marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* @param ClassMetadata<AbstractMappingDriverUser> $class
*
Expand Down
51 changes: 51 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,9 @@
use Documents\User;
use Documents\UserName;
use Documents\UserRepository;
use Documents74\CustomCollection;
use Documents74\TypedEmbeddedDocument;
use Documents74\UserTyped;
use Generator;
use InvalidArgumentException;
use ProxyManager\Proxy\GhostObjectInterface;
Expand Down Expand Up @@ -136,6 +139,54 @@ 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'));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

int seems to be missing compared to the list of what is autocompleted :)


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

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

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

$cm->mapManyEmbedded(['fieldName' => 'embedMany']);
self::assertEquals(CustomCollection::class, $cm->getAssociationCollectionClass('embedMany'));

$cm->mapManyReference(['fieldName' => 'referenceMany']);
self::assertEquals(CustomCollection::class, $cm->getAssociationCollectionClass('referenceMany'));
}

/**
* @group DDC-115
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?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"/>
<field name="int"/>

<embed-one field="embedOne" />
<reference-one field="referenceOne" />

<embed-many field="embedMany" />
<reference-many field="referenceMany" />
</document>
</doctrine-mongo-mapping>
Loading