Skip to content
Draft
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
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
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
*/
final class AutoGenerator extends AbstractIdGenerator
{
Expand Down
21 changes: 21 additions & 0 deletions lib/Doctrine/ODM/MongoDB/Id/ObjectIdGenerator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace Doctrine\ODM\MongoDB\Id;

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

/**
* AutoGenerator generates a native 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();
}
}
40 changes: 27 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 @@ -942,6 +945,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 +2404,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 +2807,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,
));
}
}
60 changes: 60 additions & 0 deletions lib/Doctrine/ODM/MongoDB/Types/BinaryUuidType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

declare(strict_types=1);

namespace Doctrine\ODM\MongoDB\Types;

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

class BinaryUuidType extends Type
{
public function convertToDatabaseValue(mixed $value): ?Binary
{
if ($value === null) {
return null;
}

if ($value instanceof Binary) {
return $value;
}

if (! $value instanceof Uuid) {
$value = Uuid::fromString($value);
}

return new Binary($value->toBinary(), Binary::TYPE_UUID);
}

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

if (! $value instanceof Binary || $value->getType() !== Binary::TYPE_UUID) {
throw new Exception('Invalid data received for Uuid');
}

return Uuid::fromString($value->getData());
}

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 '$return = $value instanceof \Symfony\Component\Uid\Uuid ? $value : \Symfony\Component\Uid\Uuid::fromString($value->getData());';
}
}
11 changes: 9 additions & 2 deletions lib/Doctrine/ODM/MongoDB/Types/Type.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Doctrine\ODM\MongoDB\Types;
use InvalidArgumentException;
use MongoDB\BSON\ObjectId;
use Symfony\Component\Uid\Uuid;

use function end;
use function explode;
Expand Down Expand Up @@ -45,6 +46,7 @@ abstract class Type
public const OBJECTID = 'object_id';
public const RAW = 'raw';
public const DECIMAL128 = 'decimal128';
public const UUID = 'uuid';

/** @deprecated const was deprecated in doctrine/mongodb-odm 2.1 and will be removed in 3.0. Use Type::INT instead */
public const INTID = 'int_id';
Expand Down Expand Up @@ -86,6 +88,7 @@ abstract class Type
self::OBJECTID => Types\ObjectIdType::class,
self::RAW => Types\RawType::class,
self::DECIMAL128 => Types\Decimal128Type::class,
self::UUID => Types\BinaryUuidType::class,
];

/** Prevent instantiation and force use of the factory method. */
Expand Down Expand Up @@ -167,11 +170,15 @@ public static function getTypeFromPHPVariable($variable): ?Type
{
if (is_object($variable)) {
if ($variable instanceof DateTimeInterface) {
return self::getType('date');
return self::getType(self::DATE);
}

if ($variable instanceof ObjectId) {
return self::getType('id');
return self::getType(self::ID);
}

if ($variable instanceof Uuid) {
return self::getType(self::UUID);
}
} else {
$type = gettype($variable);
Expand Down
2 changes: 1 addition & 1 deletion lib/Doctrine/ODM/MongoDB/UnitOfWork.php
Original file line number Diff line number Diff line change
Expand Up @@ -1139,7 +1139,7 @@ private function persistNew(ClassMetadata $class, object $document): void
));
}

if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_AUTO && $idValue !== null && ! preg_match('#^[0-9a-f]{24}$#', (string) $idValue)) {
if ($idValue === null && $class->getIdentifier()['type'] === Type::ID && $class->generatorType === ClassMetadata::GENERATOR_TYPE_AUTO && ! preg_match('#^[0-9a-f]{24}$#', (string) $idValue)) {
throw new InvalidArgumentException(sprintf(
'%s uses AUTO identifier generation strategy but provided identifier is not a valid ObjectId.',
$document::class,
Expand Down
18 changes: 18 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -882,6 +882,12 @@ parameters:
count: 1
path: lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataFactory.php

-
message: '#^Method Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadataFactory\:\:generateAutoIdGenerator\(\) has parameter \$class with generic class Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata but does not specify its types\: T$#'
identifier: missingType.generics
count: 1
path: lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataFactory.php

-
message: '#^Method Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadataFactory\:\:initializeReflection\(\) has parameter \$class with generic class Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata but does not specify its types\: T$#'
identifier: missingType.generics
Expand Down Expand Up @@ -2106,6 +2112,18 @@ parameters:
count: 1
path: tests/Doctrine/ODM/MongoDB/Tests/Tools/ResolveTargetDocumentListenerTest.php

-
message: '#^Call to static method PHPUnit\\Framework\\Assert\:\:assertSame\(\) with arguments MongoDB\\BSON\\Binary, null and ''Binary UUIDs are…'' will always evaluate to false\.$#'
identifier: staticMethod.impossibleType
count: 1
path: tests/Doctrine/ODM/MongoDB/Tests/Types/BinaryUuidTypeTest.php

-
message: '#^Call to static method PHPUnit\\Framework\\Assert\:\:assertSame\(\) with arguments Symfony\\Component\\Uid\\UuidV4, null and ''Uuid objects are…'' will always evaluate to false\.$#'
identifier: staticMethod.impossibleType
count: 1
path: tests/Doctrine/ODM/MongoDB/Tests/Types/BinaryUuidTypeTest.php

-
message: '#^Property Doctrine\\ODM\\MongoDB\\Tests\\ArrayTest\:\:\$id is unused\.$#'
identifier: property.unused
Expand Down
Loading
Loading