Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
c4fb9d5
Replace Type static functions with an instance of TypeRegistry
GromNaN Dec 5, 2025
c2b7acc
Trigger deprecations in legacy Type static methods
GromNaN Dec 6, 2025
2e4d772
Inject the TypeRegistry into ClassMetadata
GromNaN Dec 10, 2025
60ada27
Enable injection of Type instance into the TypeRegistry
GromNaN Dec 10, 2025
17ebe98
Use a shared instance of TypeRegistry by default when none is injected
GromNaN Dec 10, 2025
f828199
Update docs
GromNaN Dec 10, 2025
07c80d1
Update the documentation
GromNaN Dec 12, 2025
fdeb1b0
Forbids the injection of a type per class name if it has a constructor
GromNaN Dec 12, 2025
b1af7a1
Rename TypeRegistry::guessTypeFromValue
GromNaN Dec 12, 2025
bc45067
Validate the registered class in TypeRegistry
GromNaN Dec 12, 2025
c7c948e
Uptimize Date compaison in UOW::computeOrRecomputeChangeSet
GromNaN Dec 12, 2025
cdfd2b9
Move TypeRegistry to the Configuration class
GromNaN Dec 13, 2025
3540c7d
Create TypeRegistry when the DM is not used
GromNaN Dec 13, 2025
b12763c
Revert date comparison to use field type from ClassMetadata
GromNaN Dec 15, 2025
a812ab8
Make TypeRegistry::getMap non-internal, so that it can be overridden …
GromNaN Dec 15, 2025
9bb562c
Add upgrade instructions
GromNaN Jan 12, 2026
2b6ef1d
Fix CS
GromNaN Mar 28, 2026
8381a06
Fix shard key field name bug and move upgrade instructions to 2.17
GromNaN Mar 28, 2026
8f2aa7f
Add test for shard key with custom DB field name
GromNaN Mar 28, 2026
4e4cd91
Document that ClassMetadata must be loaded through ClassMetadataFactory
GromNaN Mar 28, 2026
fc4d6e5
Fix TypeRegistry::register() to allow types with inherited no-arg con…
GromNaN Mar 28, 2026
358c27f
Add test for Configuration::setTypeRegistry() immutability
GromNaN Mar 28, 2026
7b9d4e5
Fix version in deprecation messages
GromNaN Mar 29, 2026
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
20 changes: 20 additions & 0 deletions UPGRADE-2.17.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# UPGRADE FROM 2.16 to 2.17

## `TypeRegistry` replaces `Type` static methods

A new `Doctrine\ODM\MongoDB\Types\TypeRegistry` class has been introduced
to manage custom types. The static methods of `Doctrine\ODM\MongoDB\Types\Type`
are deprecated and will be removed in MongoDB ODM 3.0:
- `Type::getType($name)` → `TypeRegistry::get($name)`
- `Type::hasType($name)` → `TypeRegistry::has($name)`
- `Type::addType($name, $class)` → `TypeRegistry::register($name, $class)`
- `Type::getTypesMap()` → `TypeRegistry::getMap()`
- `Type::registerType($name, $class)` → `TypeRegistry::register($name, $class)`
- `Type::overrideType($name, $class)` → `TypeRegistry::register($name, $class)`
- `Type::getTypeFromPHPVariable($value)` → `TypeRegistry::guessTypeFromValue($value)`
- `Type::convertPHPToDatabaseValue($value)` → `TypeRegistry::convertToDatabaseValue($value)`

You can set and get the `TypeRegistry` instance from the `Doctrine\ODM\MongoDB\Configuration`
using `getTypeRegistry()` and `setTypeRegistry()`.

To access the type of a mapped field, use the `ClassMetadata::getFieldType()` method.
5 changes: 5 additions & 0 deletions docs/en/reference/basic-mapping.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ document mapping metadata:
stored in the metadata cache. Therefore all drivers perform equally well at
runtime.

``ClassMetadata`` instances must always be loaded through
``ClassMetadataFactory``, which restores runtime dependencies (such as the
``TypeRegistry``) after deserialization. Deserializing a ``ClassMetadata``
instance directly is not supported.

Introduction to Attributes
--------------------------

Expand Down
22 changes: 14 additions & 8 deletions docs/en/reference/custom-mapping-types.rst
Original file line number Diff line number Diff line change
Expand Up @@ -79,24 +79,29 @@ Restrictions to keep in mind:
method that did not change in the request.

When you have implemented the type you still need to let Doctrine
know about it:
know about it. You can create a ``TypeRegistry`` and inject it into the
``DocumentManager``:

.. code-block:: php

<?php

// in bootstrapping code

use Doctrine\ODM\MongoDB\Types\Type;
use Doctrine\ODM\MongoDB\DocumentManager;
use Doctrine\ODM\MongoDB\Types\TypeRegistry;

$typeRegistry = new TypeRegistry();

// Adds a type. This results in an exception if type with given name is already registered
Type::addType('date_with_timezone', \My\Project\Types\DateTimeWithTimezoneType::class);
$typeRegistry->register('date_with_timezone', new \My\Project\Types\DateTimeWithTimezoneType());

// Overrides a type. This results in an exception if type with given name is not registered
Type::overrideType('date_immutable', \My\Project\Types\DateTimeWithTimezoneType::class);
// Overrides a type. This replaces any existing type with given name
$typeRegistry->register('date_immutable', new \My\Project\Types\DateTimeWithTimezoneType();

// Registers a type without checking whether it was already registered
Type::registerType('date_immutable', \My\Project\Types\DateTimeWithTimezoneType::class);
// Initialize DocumentManager with the TypeRegistry
$config = new Configuration();
$config->setTypeRegistry($typeRegistry);

As can be seen above, when registering the custom types in the configuration you
specify a unique name for the mapping type and map that to the corresponding
Expand Down Expand Up @@ -182,7 +187,8 @@ Register the type in your bootstrap code::

.. code-block:: php

Type::addType(Money::class, App\MongoDB\Types\MoneyType::class);
$typeRegistry = new TypeRegistry();
$typeRegistry->register(Money::class, new App\MongoDB\Types\MoneyType());

By using the |FQCN| of the value object class as the type name, the type is
automatically used when encountering a property of that class. This means you
Expand Down
12 changes: 12 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -1265,3 +1265,15 @@ parameters:
identifier: staticMethod.impossibleType
count: 1
path: tests/Tests/Types/BinaryUuidTypeTest.php

-
message: '#^Call to static method PHPUnit\\Framework\\Assert\:\:assertInstanceOf\(\) with ''Doctrine\\\\ODM\\\\MongoDB\\\\Types\\\\FloatType'' and Doctrine\\ODM\\MongoDB\\Types\\StringType will always evaluate to false\.$#'
identifier: staticMethod.impossibleType
count: 1
path: tests/Tests/Types/TypeTest.php

-
message: '#^Call to static method PHPUnit\\Framework\\Assert\:\:assertInstanceOf\(\) with ''Doctrine\\\\ODM\\\\MongoDB\\\\Types\\\\IntType'' and \*NEVER\* will always evaluate to false\.$#'
identifier: staticMethod.impossibleType
count: 1
path: tests/Tests/Types/TypeTest.php
3 changes: 1 addition & 2 deletions src/Aggregation/Expr.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
use Doctrine\ODM\MongoDB\DocumentManager;
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
use Doctrine\ODM\MongoDB\Persisters\DocumentPersister;
use Doctrine\ODM\MongoDB\Types\Type;
use LogicException;

use function array_filter;
Expand Down Expand Up @@ -1122,7 +1121,7 @@ private function prepareArgument($expression)
}

// Convert PHP types to MongoDB types for everything else
return Type::convertPHPToDatabaseValue($expression);
return $this->dm->getConfiguration()->getTypeRegistry()->convertToDatabaseValue($expression);
}

private function getDocumentPersister(): DocumentPersister
Expand Down
3 changes: 1 addition & 2 deletions src/Aggregation/Stage/AbstractBucket.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
use Doctrine\ODM\MongoDB\DocumentManager;
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
use Doctrine\ODM\MongoDB\Persisters\DocumentPersister;
use Doctrine\ODM\MongoDB\Types\Type;

use function array_map;
use function is_array;
Expand Down Expand Up @@ -86,7 +85,7 @@ private function convertExpression($expression)
return '$' . $this->getDocumentPersister()->prepareFieldName(substr($expression, 1));
}

return Type::convertPHPToDatabaseValue(Expr::convertExpression($expression));
return $this->dm->getConfiguration()->getTypeRegistry()->convertToDatabaseValue($expression);
}

private function getDocumentPersister(): DocumentPersister
Expand Down
3 changes: 1 addition & 2 deletions src/Aggregation/Stage/AbstractReplace.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
use Doctrine\ODM\MongoDB\DocumentManager;
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
use Doctrine\ODM\MongoDB\Persisters\DocumentPersister;
use Doctrine\ODM\MongoDB\Types\Type;

use function array_map;
use function is_array;
Expand Down Expand Up @@ -50,6 +49,6 @@ private function convertExpression($expression)
return '$' . $this->getDocumentPersister()->prepareFieldName(substr($expression, 1));
}

return Type::convertPHPToDatabaseValue(Expr::convertExpression($expression));
return $this->dm->getConfiguration()->getTypeRegistry()->convertToDatabaseValue(Expr::convertExpression($expression));
}
}
3 changes: 1 addition & 2 deletions src/Aggregation/Stage/GraphLookup.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
use Doctrine\ODM\MongoDB\Mapping\MappingException;
use Doctrine\ODM\MongoDB\Persisters\DocumentPersister;
use Doctrine\ODM\MongoDB\Types\Type;
use Doctrine\Persistence\Mapping\MappingException as BaseMappingException;
use LogicException;

Expand Down Expand Up @@ -264,7 +263,7 @@ private function convertExpression($expression)
return '$' . $this->getDocumentPersister($this->class)->prepareFieldName(substr($expression, 1));
}

return Type::convertPHPToDatabaseValue(Expr::convertExpression($expression));
return $this->dm->getConfiguration()->getTypeRegistry()->convertToDatabaseValue(Expr::convertExpression($expression));
}

private function convertTargetFieldName(string $fieldName): string
Expand Down
3 changes: 1 addition & 2 deletions src/Aggregation/Stage/VectorSearch.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
use Doctrine\ODM\MongoDB\Aggregation\Stage;
use Doctrine\ODM\MongoDB\Persisters\DocumentPersister;
use Doctrine\ODM\MongoDB\Query\Expr;
use Doctrine\ODM\MongoDB\Types\Type;
use InvalidArgumentException;
use MongoDB\BSON\Binary;
use MongoDB\BSON\Decimal128;
Expand Down Expand Up @@ -82,7 +81,7 @@ public function getExpression(): array
}

if ($this->queryVector !== null) {
$params['queryVector'] = Type::getType($this->persister->getClassMetadata()->fieldMappings[$this->path ?? '']['type'] ?? Type::RAW)->convertToDatabaseValue($this->queryVector);
$params['queryVector'] = $this->persister->convertToDatabaseValue($this->path ?? '', $this->queryVector, $this->persister->getClassMetadata());
}

return [$this->getStageName() => $params];
Expand Down
17 changes: 17 additions & 0 deletions src/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use Doctrine\ODM\MongoDB\Repository\DocumentRepository;
use Doctrine\ODM\MongoDB\Repository\GridFSRepository;
use Doctrine\ODM\MongoDB\Repository\RepositoryFactory;
use Doctrine\ODM\MongoDB\Types\TypeRegistry;
use Doctrine\Persistence\Mapping\Driver\MappingDriver;
use Doctrine\Persistence\ObjectRepository;
use InvalidArgumentException;
Expand Down Expand Up @@ -154,6 +155,22 @@ class Configuration

private static string $version;

private ?TypeRegistry $typeRegistry = null;

public function setTypeRegistry(TypeRegistry $typeRegistry): void
{
if ($this->typeRegistry !== null) {
throw new LogicException('TypeRegistry is already set and cannot be changed.');
}

$this->typeRegistry = $typeRegistry;
}

public function getTypeRegistry(): TypeRegistry
{
return $this->typeRegistry ?? TypeRegistry::getSharedInstance();
}

/**
* Provides the driver options to be used when creating the MongoDB client.
*
Expand Down
23 changes: 20 additions & 3 deletions src/Hydrator/HydratorFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
use Doctrine\ODM\MongoDB\Events;
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
use Doctrine\ODM\MongoDB\Proxy\InternalProxy;
use Doctrine\ODM\MongoDB\Types\Type;
use Doctrine\ODM\MongoDB\UnitOfWork;
use ProxyManager\Proxy\GhostObjectInterface;

Expand All @@ -28,9 +27,12 @@
use function rename;
use function rtrim;
use function sprintf;
use function str_contains;
use function str_replace;
use function substr;
use function trigger_deprecation;
use function uniqid;
use function var_export;

use const DIRECTORY_SEPARATOR;
use const PHP_VERSION_ID;
Expand Down Expand Up @@ -196,7 +198,7 @@ private function generateHydratorClass(ClassMetadata $class, string $hydratorCla
,
$mapping['name'],
$mapping['fieldName'],
Type::getType($mapping['type'])->closureToPHP(),
$this->closureToPHP($class, $mapping['fieldName']),
);
} elseif (! isset($mapping['association'])) {
$code .= sprintf(
Expand All @@ -219,7 +221,7 @@ private function generateHydratorClass(ClassMetadata $class, string $hydratorCla
,
$mapping['name'],
$mapping['fieldName'],
Type::getType($mapping['type'])->closureToPHP(),
$this->closureToPHP($class, $mapping['fieldName']),
);
} elseif ($mapping['association'] === ClassMetadata::REFERENCE_ONE && $mapping['isOwningSide']) {
$code .= sprintf(
Expand Down Expand Up @@ -476,4 +478,19 @@ public function hydrate(object $document, array $data, array $hints = []): array

return $data;
}

private function closureToPHP(ClassMetadata $class, string $fieldName): string
{
$code = $class->getFieldType($fieldName)->closureToPHP();

if (str_contains($code, '$typeIdentifier')) {
trigger_deprecation('doctrine/mongodb-odm', '2.16', 'Using $typeIdentifier in Type::closureToPHP() is deprecated and will be removed in Doctrine ODM 3.0');
}

return str_replace(
'$fieldName',
var_export($fieldName, true),
$code,
);
}
}
51 changes: 41 additions & 10 deletions src/Mapping/ClassMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
use Doctrine\ODM\MongoDB\Mapping\PropertyAccessors\PropertyAccessorFactory;
use Doctrine\ODM\MongoDB\Proxy\InternalProxy;
use Doctrine\ODM\MongoDB\Types\Incrementable;
use Doctrine\ODM\MongoDB\Types\InvalidTypeException;
use Doctrine\ODM\MongoDB\Types\Type;
use Doctrine\ODM\MongoDB\Types\TypeRegistry;
use Doctrine\ODM\MongoDB\Types\Versionable;
use Doctrine\ODM\MongoDB\Utility\CollectionHelper;
use Doctrine\Persistence\Mapping\ClassMetadata as BaseClassMetadata;
Expand Down Expand Up @@ -867,6 +869,8 @@
/** @var class-string|null */
private ?string $rootClass;

private TypeRegistry $typeRegistry;

/**
* Initializes a new ClassMetadata instance that will hold the object-document mapping
* metadata of the class with the given name.
Expand Down Expand Up @@ -926,6 +930,24 @@ public static function getReferenceFieldName(string $storeAs, string $pathPrefix
return ($pathPrefix ? $pathPrefix . '.' : '') . self::getReferencePrefix($storeAs) . 'id';
}

/**
* Inject the TypeRegistry instance, used for field transformation and type detection.
*/
public function setTypeRegistry(TypeRegistry $types): void
{
$this->typeRegistry = $types;
}

private function getTypeRegistry(): TypeRegistry
{
if (! isset($this->typeRegistry)) {
$this->typeRegistry = TypeRegistry::getSharedInstance();
trigger_deprecation('doctrine/mongodb-odm', '2.17', 'Using ClassMetadata without a TypeRegistry is deprecated. Inject the TypeRegistry instance from the DocumentManager via $classMetadata->setTypeRegistry($configuration->getTypeRegistry()).');
}

return $this->typeRegistry;
}

public function getReflectionClass(): ReflectionClass
{
return $this->reflClass;
Expand Down Expand Up @@ -1711,7 +1733,7 @@ private function applyStorageStrategy(array &$mapping): void
default:
$defaultStrategy = self::STORAGE_STRATEGY_SET;
$allowedStrategies = [self::STORAGE_STRATEGY_SET];
$type = Type::getType($mapping['type']);
$type = $this->getTypeRegistry()->get($mapping['type']);
if ($type instanceof Incrementable) {
$allowedStrategies[] = self::STORAGE_STRATEGY_INCREMENT;
}
Expand Down Expand Up @@ -1920,9 +1942,7 @@ public function setIdGenerator(IdGenerator $generator): void
*/
public function getPHPIdentifierValue($id)
{
$idType = $this->fieldMappings[$this->identifier]['type'];

return Type::getType($idType)->convertToPHPValue($id);
return $this->getFieldType($this->identifier)->convertToPHPValue($id);
}

/**
Expand All @@ -1934,9 +1954,7 @@ public function getPHPIdentifierValue($id)
*/
public function getDatabaseIdentifierValue($id)
{
$idType = $this->fieldMappings[$this->identifier]['type'];

return Type::getType($idType)->convertToDatabaseValue($id);
return $this->getFieldType($this->identifier)->convertToDatabaseValue($id);
}

/**
Expand Down Expand Up @@ -2038,6 +2056,19 @@ public function getFieldMapping(string $fieldName): array
return $this->fieldMappings[$fieldName];
}

public function getFieldType(string $fieldName): Type
{
if (! isset($this->fieldMappings[$fieldName]['type'])) {
return $this->getTypeRegistry()->get(Type::RAW);
}

try {
return $this->getTypeRegistry()->get($this->fieldMappings[$fieldName]['type']);
} catch (InvalidTypeException) {
throw MappingException::invalidTypeForField($this->name, $fieldName, $this->fieldMappings[$fieldName]['type']);
}
}

/**
* Gets mappings of fields holding embedded document(s).
*
Expand Down Expand Up @@ -2203,7 +2234,7 @@ public function isIdGeneratorNone(): bool
*/
public function setVersionMapping(array &$mapping): void
{
if (! Type::getType($mapping['type']) instanceof Versionable) {
if (! $this->getTypeRegistry()->get($mapping['type']) instanceof Versionable) {
throw LockException::invalidVersionFieldType($mapping['type']);
}

Expand Down Expand Up @@ -2466,7 +2497,7 @@ public function mapField(array $mapping): array

if (isset($mapping['encrypt']['queryType'])) {
// The encrypted range query options min and max must be converted to the database type
$type = Type::getType($mapping['type']);
$type = $this->getTypeRegistry()->get($mapping['type']);
foreach (['min', 'max'] as $option) {
if (isset($mapping['encrypt'][$option])) {
$mapping['encrypt'][$option] = $type->convertToDatabaseValue($mapping['encrypt'][$option]);
Expand Down Expand Up @@ -2880,7 +2911,7 @@ private function validateAndCompleteTypedFieldMapping(array $mapping): array
return $mapping;
}

if (! $type->isBuiltin() && Type::hasType($type->getName())) {
if (! $type->isBuiltin() && $this->getTypeRegistry()->has($type->getName())) {
$mapping['type'] = $type->getName();

return $mapping;
Expand Down
Loading
Loading