Skip to content

Commit d106a5a

Browse files
committed
Enable automatic type detection from the property type when type name is the FQCN of the value object
1 parent 6a6e234 commit d106a5a

File tree

4 files changed

+135
-12
lines changed

4 files changed

+135
-12
lines changed

docs/en/reference/custom-mapping-types.rst

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -217,11 +217,15 @@ Usage Example::
217217
#[Field(type: Ramsey\Uuid\Uuid::class)]
218218
public Ramsey\Uuid\Uuid $id;
219219
220-
.. note::
221-
The type name should be the name of the value object class (e.g., ``Ramsey\Uuid\Uuid``
222-
for our custom ``UuidType``) to enable automatic transformation of the values
223-
of this class in the aggregation builder.
220+
By using the |FQCN| of the value object class as the type name, the type is
221+
automatically used when encountering a property of that class. This means you
222+
can omit the ``type`` option when defining the field mapping::
223+
224+
.. code-block:: php
225+
226+
#[Field]
227+
public Ramsey\Uuid\Uuid $id;
224228
225229
.. _`ramsey/uuid library`: https://github.com/ramsey/uuid
226230
.. |FQCN| raw:: html
227-
<abbr title="Fully-Qualified Class Name">FQCN</abbr>
231+
<abbr title="Fully-Qualified Class Name">FQCN</abbr>

lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2750,9 +2750,13 @@ private function isTypedProperty(string $name): bool
27502750
*/
27512751
private function validateAndCompleteTypedFieldMapping(array $mapping): array
27522752
{
2753+
if (isset($mapping['type'])) {
2754+
return $mapping;
2755+
}
2756+
27532757
$type = $this->reflClass->getProperty($mapping['fieldName'])->getType();
27542758

2755-
if (! $type instanceof ReflectionNamedType || isset($mapping['type'])) {
2759+
if (! $type instanceof ReflectionNamedType) {
27562760
return $mapping;
27572761
}
27582762

@@ -2766,6 +2770,14 @@ private function validateAndCompleteTypedFieldMapping(array $mapping): array
27662770
$type = $reflection->getBackingType();
27672771
assert($type instanceof ReflectionNamedType);
27682772
$mapping['enumType'] = $reflection->getName();
2773+
2774+
return $mapping;
2775+
}
2776+
2777+
if (! $type->isBuiltin() && Type::hasType($type->getName())) {
2778+
$mapping['type'] = $type->getName();
2779+
2780+
return $mapping;
27692781
}
27702782

27712783
switch ($type->getName()) {

tests/Doctrine/ODM/MongoDB/Tests/Functional/CustomTypeTest.php

Lines changed: 92 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,34 @@
1010
use Doctrine\ODM\MongoDB\Types\ClosureToPHP;
1111
use Doctrine\ODM\MongoDB\Types\Type;
1212
use Exception;
13+
use PHPUnit\Framework\Attributes\After;
14+
use PHPUnit\Framework\Attributes\Before;
15+
use ReflectionProperty;
1316

1417
use function array_map;
1518
use function array_values;
19+
use function assert;
1620
use function is_array;
1721

1822
class CustomTypeTest extends BaseTestCase
1923
{
20-
public static function setUpBeforeClass(): void
24+
/** @var string[] */
25+
private array $originalTypeMap = [];
26+
27+
#[Before]
28+
public function backupTypeMap(): void
2129
{
30+
$this->originalTypeMap = (new ReflectionProperty(Type::class, 'typesMap'))->getValue();
31+
2232
Type::addType('date_collection', DateCollectionType::class);
33+
Type::addType(Language::class, LanguageType::class);
34+
}
35+
36+
#[After]
37+
public function restoreTypeMap(): void
38+
{
39+
(new ReflectionProperty(Type::class, 'typesMap'))->setValue($this->originalTypeMap);
40+
unset($this->originalTypeMap);
2341
}
2442

2543
public function testCustomTypeValueConversions(): void
@@ -46,6 +64,36 @@ public function testConvertToDatabaseValueExpectsArray(): void
4664
$this->expectException(CustomTypeException::class);
4765
$this->dm->flush();
4866
}
67+
68+
public function testCustomTypeDetection(): void
69+
{
70+
$field = $this->dm->getClassMetadata(Country::class)->getFieldMapping('lang');
71+
self::assertSame(Language::class, $field['type'], 'The custom type should be detected on the field');
72+
73+
$country = new Country();
74+
$country->lang = new Language('French', 'fr');
75+
76+
$this->dm->persist($country);
77+
$this->dm->flush();
78+
$this->dm->clear();
79+
80+
$country = $this->dm->find(Country::class, $country->id);
81+
82+
self::assertNotNull($country);
83+
self::assertInstanceOf(Language::class, $country->lang);
84+
self::assertSame('French', $country->lang->name);
85+
self::assertSame('fr', $country->lang->code);
86+
}
87+
88+
public function testTypeFromPHPVariable(): void
89+
{
90+
$lang = new Language('French', 'fr');
91+
$type = Type::getTypeFromPHPVariable($lang);
92+
self::assertInstanceOf(LanguageType::class, $type);
93+
94+
$databaseValue = Type::convertPHPToDatabaseValue($lang);
95+
self::assertSame(['name' => 'French', 'code' => 'fr'], $databaseValue);
96+
}
4997
}
5098

5199
class DateCollectionType extends Type
@@ -106,11 +154,52 @@ class CustomTypeException extends Exception
106154
#[ODM\Document]
107155
class Country
108156
{
109-
/** @var string|null */
110157
#[ODM\Id]
111-
public $id;
158+
public ?string $id;
112159

113160
/** @var DateTime[]|DateTime|null */
114161
#[ODM\Field(type: 'date_collection')]
115162
public $nationalHolidays;
163+
164+
/** The field type is detected from the property type */
165+
#[ODM\Field(/* type: Language::class */)]
166+
public Language $lang;
167+
}
168+
169+
class Language
170+
{
171+
public function __construct(
172+
public string $name,
173+
public string $code,
174+
) {
175+
}
176+
}
177+
178+
class LanguageType extends Type
179+
{
180+
use ClosureToPHP;
181+
182+
/** @return array{name:string,code:string}|null */
183+
public function convertToDatabaseValue($value): ?array
184+
{
185+
if ($value === null) {
186+
return null;
187+
}
188+
189+
assert($value instanceof Language);
190+
191+
return ['name' => $value->name, 'code' => $value->code];
192+
}
193+
194+
/** @param array{name:string,code:string}|null $value */
195+
public function convertToPHPValue($value): ?Language
196+
{
197+
if ($value === null) {
198+
return null;
199+
}
200+
201+
assert(is_array($value) && isset($value['name'], $value['code']));
202+
203+
return new Language($value['name'], $value['code']);
204+
}
116205
}

tests/Doctrine/ODM/MongoDB/Tests/Functional/Ticket/GH2789Test.php

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,35 @@
99
use Doctrine\ODM\MongoDB\Types\Type;
1010
use Doctrine\ODM\MongoDB\Types\Versionable;
1111
use MongoDB\BSON\Binary;
12-
use PHPUnit\Framework\Attributes\BackupGlobals;
12+
use PHPUnit\Framework\Attributes\After;
13+
use PHPUnit\Framework\Attributes\Before;
14+
use ReflectionProperty;
1315

1416
use function assert;
1517
use function is_int;
1618

17-
#[BackupGlobals(true)]
1819
class GH2789Test extends BaseTestCase
1920
{
20-
public function testVersionWithCustomType(): void
21+
/** @var string[] */
22+
private array $originalTypeMap = [];
23+
24+
#[Before]
25+
public function backupTypeMap(): void
2126
{
27+
$this->originalTypeMap = (new ReflectionProperty(Type::class, 'typesMap'))->getValue();
28+
2229
Type::addType(GH2789CustomType::class, GH2789CustomType::class);
30+
}
31+
32+
#[After]
33+
public function restoreTypeMap(): void
34+
{
35+
(new ReflectionProperty(Type::class, 'typesMap'))->setValue($this->originalTypeMap);
36+
unset($this->originalTypeMap);
37+
}
38+
39+
public function testVersionWithCustomType(): void
40+
{
2341
$doc = new GH2789VersionedUuid('original message');
2442

2543
$this->dm->persist($doc);

0 commit comments

Comments
 (0)