Skip to content

Commit c813a57

Browse files
authored
Support symfony UUID (#2826)
* Add new type for Symfony UUIDs * Add UUID type to type class * Don't transform PHP UUIDs again * Support generating Symfony UUID objects in AUTO generator * Deprecate string-based UUID generator * Add documentation around UUIDs * Apply Copilot review feedback * Unify logic in binary uuid type * Apply code review feedback * Undo type conditional change to fix errors * Add documentation around UUID generator deprecation * Link to Symfony UID component * Explain deprecation of UUID generator in docs * Simplify ID generator docblocks * Fix ObjectId check when using auto generation * Use InvalidArgumentException in type
1 parent 2a9118a commit c813a57

17 files changed

+428
-25
lines changed

UPGRADE-3.0.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ The `Doctrine\ODM\MongoDB\Id\AbstractIdGenerator` class has been removed. Custom
1414
ID generators must implement the `Doctrine\ODM\MongoDB\Id\IdGenerator`
1515
interface.
1616

17+
The `Doctrine\ODM\MongoDB\Id\UuidGenerator` class has been removed. Use a custom
18+
generator to generate string UUIDs. For more efficient storage of UUIDs, use the
19+
`Doctrine\ODM\MongoDB\Types\BinaryUuidType` type in combination with the
20+
`Doctrine\ODM\MongoDB\Id\SymfonyUuidGenerator` generator.
21+
1722
## Metadata
1823
The `Doctrine\ODM\MongoDB\Mapping\ClassMetadata` class has been marked final and
1924
will no longer be extendable.

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@
4949
"phpstan/phpstan-phpunit": "^2.0",
5050
"phpunit/phpunit": "^10.4",
5151
"squizlabs/php_codesniffer": "^3.5",
52-
"symfony/cache": "^5.4 || ^6.0 || ^7.0"
52+
"symfony/cache": "^5.4 || ^6.0 || ^7.0",
53+
"symfony/uid": "^5.4 || ^6.0 || ^7.0"
5354
},
5455
"conflict": {
5556
"doctrine/annotations": "<1.12 || >=3.0"

docs/en/reference/basic-mapping.rst

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ Here is a quick overview of the built-in mapping types:
151151
- ``raw``
152152
- ``string``
153153
- ``timestamp``
154+
- ``uuid``
154155

155156
You can read more about the available MongoDB types on `php.net <https://www.php.net/mongodb.bson>`_.
156157

@@ -178,6 +179,7 @@ This list explains some of the less obvious mapping types:
178179
- ``id``: string to ObjectId by default, but other formats are possible
179180
- ``timestamp``: string to ``MongoDB\BSON\Timestamp``
180181
- ``raw``: any type
182+
- ``uuid``: `Symfony UID <https://symfony.com/doc/current/components/uid.html>`_ to ``MongoDB\BSON\Binary`` instance with a "uuid" type
181183

182184
.. note::
183185

@@ -206,6 +208,7 @@ follows:
206208
- ``float``: ``float``
207209
- ``int``: ``int``
208210
- ``string``: ``string``
211+
- ``Symfony\Component\Uid\Uuid``: ``uuid``
209212

210213
Doctrine can also autoconfigure any backed ``enum`` it encounters: ``type``
211214
will be set to ``string`` or ``int``, depending on the enum's backing type,
@@ -269,13 +272,23 @@ Here is an example:
269272
You can configure custom ID strategies if you don't want to use the default
270273
object ID. The available strategies are:
271274

272-
- ``AUTO`` - Uses the native generated ObjectId.
275+
- ``AUTO`` - Automatically generates an ObjectId or Symfony UUID depending on the identifier type.
273276
- ``ALNUM`` - Generates an alpha-numeric string (based on an incrementing value).
274277
- ``CUSTOM`` - Defers generation to an implementation of ``IdGenerator`` specified in the ``class`` option.
275278
- ``INCREMENT`` - Uses another collection to auto increment an integer identifier.
276-
- ``UUID`` - Generates a UUID identifier.
279+
- ``UUID`` - Generates a UUID identifier (deprecated).
277280
- ``NONE`` - Do not generate any identifier. ID must be manually set.
278281

282+
When using the ``AUTO`` strategy in combination with a UUID identifier, the generator can create UUIDs of type 1, type 4,
283+
and type 7 automatically. For all other UUID types, assign the identifier manually in combination with the ``NONE``
284+
strategy.
285+
286+
.. note::
287+
288+
The ``UUID`` generator is deprecated, as it stores UUIDs as strings. It is recommended to use the ``AUTO`` strategy
289+
with a ``uuid`` type identifier field instead. If you need to keep generating string UUIDs, you can use the
290+
``CUSTOM`` strategy with your own generator.
291+
279292
Here is an example how to manually set a string identifier for your documents:
280293

281294
.. configuration-block::

lib/Doctrine/ODM/MongoDB/Id/AutoGenerator.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
/**
1111
* AutoGenerator generates a native ObjectId
12+
*
13+
* @deprecated use ObjectIdGenerator instead
1214
*/
1315
final class AutoGenerator extends AbstractIdGenerator
1416
{
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\Id;
6+
7+
use Doctrine\ODM\MongoDB\DocumentManager;
8+
use MongoDB\BSON\ObjectId;
9+
10+
/** @internal */
11+
final class ObjectIdGenerator extends AbstractIdGenerator
12+
{
13+
public function generate(DocumentManager $dm, object $document): ObjectId
14+
{
15+
return new ObjectId();
16+
}
17+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\ODM\MongoDB\Id;
6+
7+
use Doctrine\ODM\MongoDB\DocumentManager;
8+
use InvalidArgumentException;
9+
use Symfony\Component\Uid\Uuid;
10+
use Symfony\Component\Uid\UuidV1;
11+
use Symfony\Component\Uid\UuidV4;
12+
use Symfony\Component\Uid\UuidV7;
13+
14+
use function array_values;
15+
use function implode;
16+
use function in_array;
17+
use function sprintf;
18+
19+
/** @internal */
20+
final class SymfonyUuidGenerator extends AbstractIdGenerator
21+
{
22+
private const SUPPORTED_TYPES = [
23+
1 => UuidV1::class,
24+
4 => UuidV4::class,
25+
7 => UuidV7::class,
26+
];
27+
28+
public function __construct(private readonly string $class)
29+
{
30+
if (! in_array($this->class, self::SUPPORTED_TYPES, true)) {
31+
throw new InvalidArgumentException(sprintf('Invalid UUID type "%s". Expected one of: %s.', $this->class, implode(', ', array_values(self::SUPPORTED_TYPES))));
32+
}
33+
}
34+
35+
public function generate(DocumentManager $dm, object $document): Uuid
36+
{
37+
return new $this->class();
38+
}
39+
}

lib/Doctrine/ODM/MongoDB/Id/UuidGenerator.php

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,7 @@
1818
use function strlen;
1919
use function substr;
2020

21-
/**
22-
* Generates UUIDs.
23-
*/
21+
/** @deprecated without replacement. Use a custom generator or switch to binary UUIDs. */
2422
final class UuidGenerator extends AbstractIdGenerator
2523
{
2624
/**

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

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@
3434
use ReflectionEnum;
3535
use ReflectionNamedType;
3636
use ReflectionProperty;
37+
use Symfony\Component\Uid\UuidV1;
38+
use Symfony\Component\Uid\UuidV4;
39+
use Symfony\Component\Uid\UuidV7;
3740

3841
use function array_column;
3942
use function array_filter;
@@ -300,6 +303,8 @@
300303

301304
/**
302305
* UUID means Doctrine will generate a uuid for us.
306+
*
307+
* @deprecated without replacement. Use a custom generator or switch to binary UUIDs.
303308
*/
304309
public const GENERATOR_TYPE_UUID = 3;
305310

@@ -942,6 +947,16 @@ public function getIdentifier(): array
942947
return [$this->identifier];
943948
}
944949

950+
/**
951+
* Gets the mapping of the identifier field
952+
*
953+
* @phpstan-return FieldMapping
954+
*/
955+
public function getIdentifierMapping(): array
956+
{
957+
return $this->fieldMappings[$this->identifier];
958+
}
959+
945960
/**
946961
* Since MongoDB only allows exactly one identifier field
947962
* this will always return an array with only one value
@@ -2391,22 +2406,18 @@ public function mapField(array $mapping): array
23912406
}
23922407

23932408
$this->generatorOptions = $mapping['options'] ?? [];
2394-
switch ($this->generatorType) {
2395-
case self::GENERATOR_TYPE_AUTO:
2396-
$mapping['type'] = 'id';
2397-
break;
2398-
default:
2399-
if (! empty($this->generatorOptions['type'])) {
2400-
$mapping['type'] = (string) $this->generatorOptions['type'];
2401-
} elseif (empty($mapping['type'])) {
2402-
$mapping['type'] = $this->generatorType === self::GENERATOR_TYPE_INCREMENT ? Type::INT : Type::CUSTOMID;
2403-
}
2409+
if ($this->generatorType !== self::GENERATOR_TYPE_AUTO) {
2410+
if (! empty($this->generatorOptions['type'])) {
2411+
$mapping['type'] = (string) $this->generatorOptions['type'];
2412+
} elseif (empty($mapping['type'])) {
2413+
$mapping['type'] = $this->generatorType === self::GENERATOR_TYPE_INCREMENT ? Type::INT : Type::CUSTOMID;
2414+
}
2415+
} elseif ($mapping['type'] !== Type::UUID) {
2416+
$mapping['type'] = Type::ID;
24042417
}
24052418

24062419
unset($this->generatorOptions['type']);
2407-
}
2408-
2409-
if (! isset($mapping['type'])) {
2420+
} elseif (! isset($mapping['type'])) {
24102421
// Default to string
24112422
$mapping['type'] = Type::STRING;
24122423
}
@@ -2798,6 +2809,11 @@ private function validateAndCompleteTypedFieldMapping(array $mapping): array
27982809
}
27992810

28002811
switch ($type->getName()) {
2812+
case UuidV1::class:
2813+
case UuidV4::class:
2814+
case UuidV7::class:
2815+
$mapping['type'] = Type::UUID;
2816+
break;
28012817
case DateTime::class:
28022818
$mapping['type'] = Type::DATE;
28032819
break;

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

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,17 @@
1212
use Doctrine\ODM\MongoDB\Event\OnClassMetadataNotFoundEventArgs;
1313
use Doctrine\ODM\MongoDB\Events;
1414
use Doctrine\ODM\MongoDB\Id\AlnumGenerator;
15-
use Doctrine\ODM\MongoDB\Id\AutoGenerator;
1615
use Doctrine\ODM\MongoDB\Id\IdGenerator;
1716
use Doctrine\ODM\MongoDB\Id\IncrementGenerator;
17+
use Doctrine\ODM\MongoDB\Id\ObjectIdGenerator;
18+
use Doctrine\ODM\MongoDB\Id\SymfonyUuidGenerator;
1819
use Doctrine\ODM\MongoDB\Id\UuidGenerator;
1920
use Doctrine\Persistence\Mapping\AbstractClassMetadataFactory;
2021
use Doctrine\Persistence\Mapping\ClassMetadata as ClassMetadataInterface;
2122
use Doctrine\Persistence\Mapping\Driver\MappingDriver;
2223
use Doctrine\Persistence\Mapping\ReflectionService;
2324
use ReflectionException;
25+
use ReflectionNamedType;
2426

2527
use function assert;
2628
use function get_class_methods;
@@ -186,7 +188,7 @@ protected function doLoadMetadata($class, $parent, $rootEntityFound, array $nonS
186188
if ($parent->idGenerator) {
187189
$class->setIdGenerator($parent->idGenerator);
188190
}
189-
} else {
191+
} elseif ($class->identifier) {
190192
$this->completeIdGeneratorMapping($class);
191193
}
192194

@@ -230,12 +232,36 @@ protected function newClassMetadataInstance($className): ClassMetadata
230232
return new ClassMetadata($className);
231233
}
232234

235+
private function generateAutoIdGenerator(ClassMetadata $class): void
236+
{
237+
$identifierMapping = $class->getIdentifierMapping();
238+
switch ($identifierMapping['type']) {
239+
case 'id':
240+
case 'objectId':
241+
$class->setIdGenerator(new ObjectIdGenerator());
242+
break;
243+
case 'uuid':
244+
$reflectionProperty = $class->getReflectionProperty($identifierMapping['fieldName']);
245+
if (! $reflectionProperty->getType() instanceof ReflectionNamedType) {
246+
throw MappingException::autoIdGeneratorNeedsType($class->name, $identifierMapping['fieldName']);
247+
}
248+
249+
$class->setIdGenerator(new SymfonyUuidGenerator($reflectionProperty->getType()->getName()));
250+
break;
251+
default:
252+
throw MappingException::unsupportedTypeForAutoGenerator(
253+
$class->name,
254+
$identifierMapping['type'],
255+
);
256+
}
257+
}
258+
233259
private function completeIdGeneratorMapping(ClassMetadata $class): void
234260
{
235261
$idGenOptions = $class->generatorOptions;
236262
switch ($class->generatorType) {
237263
case ClassMetadata::GENERATOR_TYPE_AUTO:
238-
$class->setIdGenerator(new AutoGenerator());
264+
$this->generateAutoIdGenerator($class);
239265
break;
240266
case ClassMetadata::GENERATOR_TYPE_INCREMENT:
241267
$incrementGenerator = new IncrementGenerator();

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,4 +319,22 @@ public static function rootDocumentCannotBeEncrypted(string $className): self
319319
$className,
320320
));
321321
}
322+
323+
public static function unsupportedTypeForAutoGenerator(string $className, string $type): self
324+
{
325+
return new self(sprintf(
326+
'The type "%s" can not be used for auto ID generation in class "%s".',
327+
$type,
328+
$className,
329+
));
330+
}
331+
332+
public static function autoIdGeneratorNeedsType(string $className, string $identifierFieldName): self
333+
{
334+
return new self(sprintf(
335+
'The auto ID generator for class "%s" requires the identifier field "%s" to have a type.',
336+
$className,
337+
$identifierFieldName,
338+
));
339+
}
322340
}

0 commit comments

Comments
 (0)