Skip to content
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
5 changes: 5 additions & 0 deletions UPGRADE-3.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ The `Doctrine\ODM\MongoDB\Id\AbstractIdGenerator` class has been removed. Custom
ID generators must implement the `Doctrine\ODM\MongoDB\Id\IdGenerator`
interface.

The `Doctrine\ODM\MongoDB\Id\UuidGenerator` class has been removed. Use a custom
generator to generate string UUIDs. For more efficient storage of UUIDs, use the
`Doctrine\ODM\MongoDB\Types\BinaryUuidType` type in combination with the
`Doctrine\ODM\MongoDB\Id\SymfonyUuidGenerator` generator.

## Metadata
The `Doctrine\ODM\MongoDB\Mapping\ClassMetadata` class has been marked final and
will no longer be extendable.
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@
"phpstan/phpstan-phpunit": "^2.0",
"phpunit/phpunit": "^10.4",
"squizlabs/php_codesniffer": "^3.5",
"symfony/cache": "^5.4 || ^6.0 || ^7.0"
"symfony/cache": "^5.4 || ^6.0 || ^7.0",
"symfony/uid": "^5.4 || ^6.0 || ^7.0"
},
"conflict": {
"doctrine/annotations": "<1.12 || >=3.0"
Expand Down
17 changes: 15 additions & 2 deletions docs/en/reference/basic-mapping.rst
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ Here is a quick overview of the built-in mapping types:
- ``raw``
- ``string``
- ``timestamp``
- ``uuid``

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

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

.. note::

Expand Down Expand Up @@ -206,6 +208,7 @@ follows:
- ``float``: ``float``
- ``int``: ``int``
- ``string``: ``string``
- ``Symfony\Component\Uid\Uuid``: ``uuid``

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

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

When using the ``AUTO`` strategy in combination with a UUID identifier, the generator can create UUIDs of type 1, type 4,
and type 7 automatically. For all other UUID types, assign the identifier manually in combination with the ``NONE``
strategy.

.. note::

The ``UUID`` generator is deprecated, as it stores UUIDs as strings. It is recommended to use the ``AUTO`` strategy
with a ``uuid`` type identifier field instead. If you need to keep generating string UUIDs, you can use the
``CUSTOM`` strategy with your own generator.

Here is an example how to manually set a string identifier for your documents:

.. configuration-block::
Expand Down
2 changes: 2 additions & 0 deletions lib/Doctrine/ODM/MongoDB/Id/AutoGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

/**
* AutoGenerator generates a native ObjectId
*
* @deprecated use ObjectIdGenerator instead
*/
final class AutoGenerator extends AbstractIdGenerator
{
Expand Down
17 changes: 17 additions & 0 deletions lib/Doctrine/ODM/MongoDB/Id/ObjectIdGenerator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace Doctrine\ODM\MongoDB\Id;

use Doctrine\ODM\MongoDB\DocumentManager;
use MongoDB\BSON\ObjectId;

/** @internal */
final class ObjectIdGenerator extends AbstractIdGenerator
{
public function generate(DocumentManager $dm, object $document): ObjectId
{
return new ObjectId();
}
}
39 changes: 39 additions & 0 deletions lib/Doctrine/ODM/MongoDB/Id/SymfonyUuidGenerator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

declare(strict_types=1);

namespace Doctrine\ODM\MongoDB\Id;

use Doctrine\ODM\MongoDB\DocumentManager;
use InvalidArgumentException;
use Symfony\Component\Uid\Uuid;
use Symfony\Component\Uid\UuidV1;
use Symfony\Component\Uid\UuidV4;
use Symfony\Component\Uid\UuidV7;

use function array_values;
use function implode;
use function in_array;
use function sprintf;

/** @internal */
final class SymfonyUuidGenerator extends AbstractIdGenerator
{
private const SUPPORTED_TYPES = [
1 => UuidV1::class,
4 => UuidV4::class,
7 => UuidV7::class,
];

public function __construct(private readonly string $class)
{
if (! in_array($this->class, self::SUPPORTED_TYPES, true)) {
throw new InvalidArgumentException(sprintf('Invalid UUID type "%s". Expected one of: %s.', $this->class, implode(', ', array_values(self::SUPPORTED_TYPES))));
}
}

public function generate(DocumentManager $dm, object $document): Uuid
{
return new $this->class();
}
}
4 changes: 1 addition & 3 deletions lib/Doctrine/ODM/MongoDB/Id/UuidGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,7 @@
use function strlen;
use function substr;

/**
* Generates UUIDs.
*/
/** @deprecated without replacement. Use a custom generator or switch to binary UUIDs. */
final class UuidGenerator extends AbstractIdGenerator
{
/**
Expand Down
42 changes: 29 additions & 13 deletions lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@
use ReflectionEnum;
use ReflectionNamedType;
use ReflectionProperty;
use Symfony\Component\Uid\UuidV1;
use Symfony\Component\Uid\UuidV4;
use Symfony\Component\Uid\UuidV7;

use function array_column;
use function array_filter;
Expand Down Expand Up @@ -300,6 +303,8 @@

/**
* UUID means Doctrine will generate a uuid for us.
*
* @deprecated without replacement. Use a custom generator or switch to binary UUIDs.
*/
public const GENERATOR_TYPE_UUID = 3;

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

/**
* Gets the mapping of the identifier field
*
* @phpstan-return FieldMapping
*/
public function getIdentifierMapping(): array
{
return $this->fieldMappings[$this->identifier];
}

/**
* Since MongoDB only allows exactly one identifier field
* this will always return an array with only one value
Expand Down Expand Up @@ -2391,22 +2406,18 @@ public function mapField(array $mapping): array
}

$this->generatorOptions = $mapping['options'] ?? [];
switch ($this->generatorType) {
case self::GENERATOR_TYPE_AUTO:
$mapping['type'] = 'id';
break;
default:
if (! empty($this->generatorOptions['type'])) {
$mapping['type'] = (string) $this->generatorOptions['type'];
} elseif (empty($mapping['type'])) {
$mapping['type'] = $this->generatorType === self::GENERATOR_TYPE_INCREMENT ? Type::INT : Type::CUSTOMID;
}
if ($this->generatorType !== self::GENERATOR_TYPE_AUTO) {
if (! empty($this->generatorOptions['type'])) {
$mapping['type'] = (string) $this->generatorOptions['type'];
} elseif (empty($mapping['type'])) {
$mapping['type'] = $this->generatorType === self::GENERATOR_TYPE_INCREMENT ? Type::INT : Type::CUSTOMID;
}
} elseif ($mapping['type'] !== Type::UUID) {
$mapping['type'] = Type::ID;
}

unset($this->generatorOptions['type']);
}

if (! isset($mapping['type'])) {
} elseif (! isset($mapping['type'])) {
// Default to string
$mapping['type'] = Type::STRING;
}
Expand Down Expand Up @@ -2798,6 +2809,11 @@ private function validateAndCompleteTypedFieldMapping(array $mapping): array
}

switch ($type->getName()) {
case UuidV1::class:
case UuidV4::class:
case UuidV7::class:
$mapping['type'] = Type::UUID;
break;
case DateTime::class:
$mapping['type'] = Type::DATE;
break;
Expand Down
32 changes: 29 additions & 3 deletions lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,17 @@
use Doctrine\ODM\MongoDB\Event\OnClassMetadataNotFoundEventArgs;
use Doctrine\ODM\MongoDB\Events;
use Doctrine\ODM\MongoDB\Id\AlnumGenerator;
use Doctrine\ODM\MongoDB\Id\AutoGenerator;
use Doctrine\ODM\MongoDB\Id\IdGenerator;
use Doctrine\ODM\MongoDB\Id\IncrementGenerator;
use Doctrine\ODM\MongoDB\Id\ObjectIdGenerator;
use Doctrine\ODM\MongoDB\Id\SymfonyUuidGenerator;
use Doctrine\ODM\MongoDB\Id\UuidGenerator;
use Doctrine\Persistence\Mapping\AbstractClassMetadataFactory;
use Doctrine\Persistence\Mapping\ClassMetadata as ClassMetadataInterface;
use Doctrine\Persistence\Mapping\Driver\MappingDriver;
use Doctrine\Persistence\Mapping\ReflectionService;
use ReflectionException;
use ReflectionNamedType;

use function assert;
use function get_class_methods;
Expand Down Expand Up @@ -186,7 +188,7 @@ protected function doLoadMetadata($class, $parent, $rootEntityFound, array $nonS
if ($parent->idGenerator) {
$class->setIdGenerator($parent->idGenerator);
}
} else {
} elseif ($class->identifier) {
$this->completeIdGeneratorMapping($class);
}

Expand Down Expand Up @@ -230,12 +232,36 @@ protected function newClassMetadataInstance($className): ClassMetadata
return new ClassMetadata($className);
}

private function generateAutoIdGenerator(ClassMetadata $class): void
{
$identifierMapping = $class->getIdentifierMapping();
switch ($identifierMapping['type']) {
case 'id':
case 'objectId':
$class->setIdGenerator(new ObjectIdGenerator());
break;
case 'uuid':
$reflectionProperty = $class->getReflectionProperty($identifierMapping['fieldName']);
if (! $reflectionProperty->getType() instanceof ReflectionNamedType) {
throw MappingException::autoIdGeneratorNeedsType($class->name, $identifierMapping['fieldName']);
}

$class->setIdGenerator(new SymfonyUuidGenerator($reflectionProperty->getType()->getName()));
break;
default:
throw MappingException::unsupportedTypeForAutoGenerator(
$class->name,
$identifierMapping['type'],
);
}
}

private function completeIdGeneratorMapping(ClassMetadata $class): void
{
$idGenOptions = $class->generatorOptions;
switch ($class->generatorType) {
case ClassMetadata::GENERATOR_TYPE_AUTO:
$class->setIdGenerator(new AutoGenerator());
$this->generateAutoIdGenerator($class);
break;
case ClassMetadata::GENERATOR_TYPE_INCREMENT:
$incrementGenerator = new IncrementGenerator();
Expand Down
18 changes: 18 additions & 0 deletions lib/Doctrine/ODM/MongoDB/Mapping/MappingException.php
Original file line number Diff line number Diff line change
Expand Up @@ -319,4 +319,22 @@ public static function rootDocumentCannotBeEncrypted(string $className): self
$className,
));
}

public static function unsupportedTypeForAutoGenerator(string $className, string $type): self
{
return new self(sprintf(
'The type "%s" can not be used for auto ID generation in class "%s".',
$type,
$className,
));
}

public static function autoIdGeneratorNeedsType(string $className, string $identifierFieldName): self
{
return new self(sprintf(
'The auto ID generator for class "%s" requires the identifier field "%s" to have a type.',
$className,
$identifierFieldName,
));
}
}
77 changes: 77 additions & 0 deletions lib/Doctrine/ODM/MongoDB/Types/BinaryUuidType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

declare(strict_types=1);

namespace Doctrine\ODM\MongoDB\Types;

use InvalidArgumentException;
use MongoDB\BSON\Binary;
use Symfony\Component\Uid\Uuid;

use function get_debug_type;
use function is_string;
use function sprintf;

class BinaryUuidType extends Type
{
public function convertToDatabaseValue(mixed $value): ?Binary
{
return match (true) {
$value === null => null,
$value instanceof Binary => $value,
$value instanceof Uuid => new Binary($value->toBinary(), Binary::TYPE_UUID),
is_string($value) => new Binary(Uuid::fromString($value)->toBinary(), Binary::TYPE_UUID),
default => throw new InvalidArgumentException(sprintf('Invalid data type %s received for UUID', get_debug_type($value))),
};
}

public function convertToPHPValue(mixed $value): Uuid
{
if ($value instanceof Uuid) {
return $value;
}

if (! $value instanceof Binary) {
throw new InvalidArgumentException(sprintf('Invalid data of type "%s" received for Uuid', get_debug_type($value)));
}

if ($value->getType() !== Binary::TYPE_UUID) {
throw new InvalidArgumentException(sprintf('Invalid binary data of type %d received for Uuid', $value->getType()));
}

return Uuid::fromBinary($value->getData());
Copy link
Member

Choose a reason for hiding this comment

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

I checked that this returns instances of UuidVx.
If there is a miss-match between the UUID version of the value in the database, and the document property type, the user will get a type error, which is perfect.

}

public function closureToMongo(): string
{
return <<<'PHP'
$return = match (true) {
$value === null => null,
$value instanceof \MongoDB\BSON\Binary => $value,
$value instanceof \Symfony\Component\Uid\Uuid => new \MongoDB\BSON\Binary($value->toBinary(), \MongoDB\BSON\Binary::TYPE_UUID),
is_string($value) => new \MongoDB\BSON\Binary(\Symfony\Component\Uid\Uuid::fromString($value)->toBinary(), \MongoDB\BSON\Binary::TYPE_UUID),
default => throw new \InvalidArgumentException(sprintf('Invalid data type %s received for UUID', get_debug_type($value))),
};
PHP;
}

public function closureToPHP(): string
{
return <<<'PHP'
if ($value instanceof \Symfony\Component\Uid\Uuid) {
$return = $value;
return;
}

if (! $value instanceof \MongoDB\BSON\Binary) {
throw new \InvalidArgumentException(sprintf('Invalid data of type "%s" received for Uuid', get_debug_type($value)));
}

if ($value->getType() !== \MongoDB\BSON\Binary::TYPE_UUID) {
throw new \InvalidArgumentException(sprintf('Invalid binary data of type %d received for Uuid', $value->getType()));
}

$return = \Symfony\Component\Uid\Uuid::fromBinary($value->getData());
PHP;
}
}
Loading