Skip to content

Commit d1ea68b

Browse files
committed
Improve detection of the type from a PHP variable
1 parent f22d8ec commit d1ea68b

File tree

8 files changed

+332
-36
lines changed

8 files changed

+332
-36
lines changed

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

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ In order to create a new mapping type you need to subclass
99
``Doctrine\ODM\MongoDB\Types\Type`` and implement/override
1010
the methods.
1111

12+
Date Example: Mapping DateTimeImmutable with Timezone
13+
-----------------------------------------------------
14+
1215
The following example defines a custom type that stores ``DateTimeInterface``
1316
instances as an embedded document containing a BSON date and accompanying
1417
timezone string. Those same embedded documents are then be translated back into
@@ -32,6 +35,7 @@ a ``DateTimeImmutable`` when the data is read from the database.
3235
// This trait provides default closureToPHP used during data hydration
3336
use ClosureToPHP;
3437
38+
/** @param array{utc: UTCDateTime, tz: string} $value */
3539
public function convertToPHPValue($value): DateTimeImmutable
3640
{
3741
if (!isset($value['utc'], $value['tz'])) {
@@ -46,6 +50,7 @@ a ``DateTimeImmutable`` when the data is read from the database.
4650
return DateTimeImmutable::createFromMutable($dateTime);
4751
}
4852
53+
/** @return array{utc: UTCDateTime, tz: string} */
4954
public function convertToDatabaseValue($value): array
5055
{
5156
if (!$value instanceof DateTimeImmutable) {
@@ -115,5 +120,114 @@ specify a unique name for the mapping type and map that to the corresponding
115120
116121
<field field-name="field" type="date_with_timezone" />
117122
123+
Custom Type Example: Mapping a UUID Class
124+
-----------------------------------------
125+
126+
You can create a custom mapping type for your own value objects or classes. For
127+
example, to map a UUID value object using the `ramsey/uuid library`_, you can
128+
implement a type that converts between your class and the BSON Binary UUID format.
129+
130+
This approach works for any custom class by adapting the conversion logic to your needs.
131+
132+
Example Implementation (using ``Ramsey\Uuid\Uuid``)::
133+
134+
.. code-block:: php
135+
136+
<?php
137+
138+
namespace My\Project\Types;
139+
140+
use Doctrine\ODM\MongoDB\Types\ClosureToPHP;
141+
use Doctrine\ODM\MongoDB\Types\Type;
142+
use InvalidArgumentException;
143+
use MongoDB\BSON\Binary;
144+
use Ramsey\Uuid\Uuid;
145+
use Ramsey\Uuid\UuidInterface;
146+
147+
final class UuidType extends Type
148+
{
149+
// This trait provides default closureToPHP used during data hydration
150+
use ClosureToPHP;
151+
152+
public function convertToPHPValue(mixed $value): ?Uuid
153+
{
154+
if (null === $value) {
155+
return null;
156+
}
157+
158+
if ($value instanceof Uuid) {
159+
return $value;
160+
}
161+
162+
if ($value instanceof Binary) {
163+
return Uuid::fromBytes($value->getData());
164+
}
165+
166+
if (is_string($value) && Uuid::isValid($value)) {
167+
return Uuid::fromString($value);
168+
}
169+
170+
throw new InvalidArgumentException(
171+
sprintf(
172+
'Could not convert database value "%s" from "%s" to %s',
173+
$value,
174+
get_debug_type($value),
175+
UuidInterface::class
176+
)
177+
);
178+
}
179+
180+
public function convertToDatabaseValue(mixed $value): ?Binary
181+
{
182+
if (null === $value || [] === $value) {
183+
return null;
184+
}
185+
186+
if ($value instanceof Binary) {
187+
return $value;
188+
}
189+
190+
if (is_string($value) && Uuid::isValid($value)) {
191+
$value = Uuid::fromString($value)->getBytes();
192+
}
193+
194+
if ($value instanceof Uuid) {
195+
return new Binary($value->getBytes(), Binary::TYPE_UUID);
196+
}
197+
198+
throw new InvalidArgumentException(
199+
sprintf(
200+
'Could not convert database value "%s" from "%s" to %s',
201+
$value,
202+
get_debug_type($value),
203+
Binary::class
204+
)
205+
);
206+
}
207+
}
208+
209+
Register the type in your bootstrap code::
210+
211+
.. code-block:: php
212+
213+
Type::addType(Ramsey\Uuid\Uuid::class, My\Project\Types\UuidType::class);
214+
215+
Usage Example::
216+
217+
.. code-block:: php
218+
219+
#[Field(type: Ramsey\Uuid\Uuid::class)]
220+
public Ramsey\Uuid\Uuid $id;
221+
222+
By using the |FQCN| of the value object class as the type name, the type is
223+
automatically used when encountering a property of that class. This means you
224+
can omit the ``type`` option when defining the field mapping::
225+
226+
.. code-block:: php
227+
228+
#[Field]
229+
public Ramsey\Uuid\Uuid $id;
230+
231+
.. _`ramsey/uuid library`: https://github.com/ramsey/uuid
118232
.. |FQCN| raw:: html
119233
<abbr title="Fully-Qualified Class Name">FQCN</abbr>

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2786,9 +2786,19 @@ private function isTypedProperty(string $name): bool
27862786
*/
27872787
private function validateAndCompleteTypedFieldMapping(array $mapping): array
27882788
{
2789+
if (isset($mapping['type'])) {
2790+
return $mapping;
2791+
}
2792+
27892793
$type = $this->reflClass->getProperty($mapping['fieldName'])->getType();
27902794

2791-
if (! $type instanceof ReflectionNamedType || isset($mapping['type'])) {
2795+
if (! $type instanceof ReflectionNamedType) {
2796+
return $mapping;
2797+
}
2798+
2799+
if (! $type->isBuiltin() && Type::hasType($type->getName())) {
2800+
$mapping['type'] = $type->getName();
2801+
27922802
return $mapping;
27932803
}
27942804

@@ -2799,6 +2809,7 @@ private function validateAndCompleteTypedFieldMapping(array $mapping): array
27992809
throw MappingException::nonBackedEnumMapped($this->name, $mapping['fieldName'], $reflection->getName());
28002810
}
28012811

2812+
// Use the backing type of the enum for the mapping type
28022813
$type = $reflection->getBackingType();
28032814
assert($type instanceof ReflectionNamedType);
28042815
$mapping['enumType'] = $reflection->getName();
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\ODM\MongoDB\Types;
6+
7+
use InvalidArgumentException;
8+
9+
use function sprintf;
10+
11+
final class InvalidTypeException extends InvalidArgumentException
12+
{
13+
public static function invalidTypeName(string $name): self
14+
{
15+
return new self(sprintf('Invalid type specified: "%s"', $name));
16+
}
17+
}

lib/Doctrine/ODM/MongoDB/Types/Type.php

Lines changed: 21 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,16 @@
44

55
namespace Doctrine\ODM\MongoDB\Types;
66

7+
use DateTimeImmutable;
78
use DateTimeInterface;
89
use Doctrine\ODM\MongoDB\Mapping\MappingException;
910
use Doctrine\ODM\MongoDB\Types;
10-
use InvalidArgumentException;
11-
use MongoDB\BSON\ObjectId;
1211
use Symfony\Component\Uid\Uuid;
1312

1413
use function end;
1514
use function explode;
1615
use function gettype;
1716
use function is_object;
18-
use function sprintf;
1917
use function str_replace;
2018

2119
/**
@@ -143,52 +141,52 @@ public static function registerType(string $name, string $class): void
143141
/**
144142
* Get a Type instance.
145143
*
146-
* @throws InvalidArgumentException
144+
* @throws InvalidTypeException
147145
*/
148146
public static function getType(string $type): Type
149147
{
150148
if (! isset(self::$typesMap[$type])) {
151-
throw new InvalidArgumentException(sprintf('Invalid type specified "%s".', $type));
149+
throw InvalidTypeException::invalidTypeName($type);
152150
}
153151

154-
if (! isset(self::$typeObjects[$type])) {
155-
$className = self::$typesMap[$type];
156-
self::$typeObjects[$type] = new $className();
157-
}
158-
159-
return self::$typeObjects[$type];
152+
return self::$typeObjects[$type] ??= new (self::$typesMap[$type]);
160153
}
161154

162155
/**
163156
* Get a Type instance based on the type of the passed php variable.
164157
*
165158
* @param mixed $variable
166-
*
167-
* @throws InvalidArgumentException
168159
*/
169160
public static function getTypeFromPHPVariable($variable): ?Type
170161
{
171162
if (is_object($variable)) {
172-
if ($variable instanceof DateTimeInterface) {
173-
return self::getType(self::DATE);
163+
if ($variable instanceof DateTimeImmutable) {
164+
return self::getType(self::DATE_IMMUTABLE);
174165
}
175166

176-
if ($variable instanceof ObjectId) {
177-
return self::getType(self::ID);
167+
if ($variable instanceof DateTimeInterface) {
168+
return self::getType(self::DATE);
178169
}
179170

180171
if ($variable instanceof Uuid) {
181172
return self::getType(self::UUID);
182173
}
183-
} else {
184-
$type = gettype($variable);
185-
switch ($type) {
186-
case 'integer':
187-
return self::getType('int');
174+
175+
// Try the variable class as type name
176+
if (self::hasType($variable::class)) {
177+
return self::getType($variable::class);
188178
}
179+
180+
return null;
189181
}
190182

191-
return null;
183+
return match (gettype($variable)) {
184+
'integer' => self::getType(self::INT),
185+
'boolean' => self::getType(self::BOOL),
186+
'double' => self::getType(self::FLOAT),
187+
'string' => self::getType(self::STRING),
188+
default => null,
189+
};
192190
}
193191

194192
/**

0 commit comments

Comments
 (0)