diff --git a/UPGRADE-2.17.md b/UPGRADE-2.17.md new file mode 100644 index 000000000..5558450b9 --- /dev/null +++ b/UPGRADE-2.17.md @@ -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. diff --git a/docs/en/reference/basic-mapping.rst b/docs/en/reference/basic-mapping.rst index 9f1553edd..34d80c4db 100644 --- a/docs/en/reference/basic-mapping.rst +++ b/docs/en/reference/basic-mapping.rst @@ -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 -------------------------- diff --git a/docs/en/reference/custom-mapping-types.rst b/docs/en/reference/custom-mapping-types.rst index cd47aa757..661c6fd9a 100644 --- a/docs/en/reference/custom-mapping-types.rst +++ b/docs/en/reference/custom-mapping-types.rst @@ -79,7 +79,8 @@ 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 @@ -87,16 +88,20 @@ know about it: // 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 @@ -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 diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 1325869ca..1f117a908 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -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 diff --git a/src/Aggregation/Expr.php b/src/Aggregation/Expr.php index 20f642d92..36ffa8e3e 100644 --- a/src/Aggregation/Expr.php +++ b/src/Aggregation/Expr.php @@ -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; @@ -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 diff --git a/src/Aggregation/Stage/AbstractBucket.php b/src/Aggregation/Stage/AbstractBucket.php index 478914f65..b15260079 100644 --- a/src/Aggregation/Stage/AbstractBucket.php +++ b/src/Aggregation/Stage/AbstractBucket.php @@ -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; @@ -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 diff --git a/src/Aggregation/Stage/AbstractReplace.php b/src/Aggregation/Stage/AbstractReplace.php index 589ff2ae4..8f267da2f 100644 --- a/src/Aggregation/Stage/AbstractReplace.php +++ b/src/Aggregation/Stage/AbstractReplace.php @@ -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; @@ -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)); } } diff --git a/src/Aggregation/Stage/GraphLookup.php b/src/Aggregation/Stage/GraphLookup.php index 59d39e241..4ad4d26f1 100644 --- a/src/Aggregation/Stage/GraphLookup.php +++ b/src/Aggregation/Stage/GraphLookup.php @@ -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; @@ -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 diff --git a/src/Aggregation/Stage/VectorSearch.php b/src/Aggregation/Stage/VectorSearch.php index 8fedc2da1..3f67db1d7 100644 --- a/src/Aggregation/Stage/VectorSearch.php +++ b/src/Aggregation/Stage/VectorSearch.php @@ -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; @@ -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]; diff --git a/src/Configuration.php b/src/Configuration.php index 47ba337ad..badaa5587 100644 --- a/src/Configuration.php +++ b/src/Configuration.php @@ -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; @@ -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. * diff --git a/src/Hydrator/HydratorFactory.php b/src/Hydrator/HydratorFactory.php index 37a8644ce..98c22b773 100644 --- a/src/Hydrator/HydratorFactory.php +++ b/src/Hydrator/HydratorFactory.php @@ -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; @@ -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; @@ -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( @@ -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( @@ -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, + ); + } } diff --git a/src/Mapping/ClassMetadata.php b/src/Mapping/ClassMetadata.php index 4cac3cc53..d8e0f9440 100644 --- a/src/Mapping/ClassMetadata.php +++ b/src/Mapping/ClassMetadata.php @@ -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; @@ -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. @@ -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; @@ -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; } @@ -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); } /** @@ -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); } /** @@ -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). * @@ -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']); } @@ -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]); @@ -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; diff --git a/src/Mapping/ClassMetadataFactory.php b/src/Mapping/ClassMetadataFactory.php index 6f02bde63..d6a2be03d 100644 --- a/src/Mapping/ClassMetadataFactory.php +++ b/src/Mapping/ClassMetadataFactory.php @@ -119,6 +119,8 @@ protected function getDriver(): MappingDriver protected function wakeupReflection(ClassMetadataInterface $class, ReflectionService $reflService): void { + $class->setTypeRegistry($this->config->getTypeRegistry()); + if (PHP_VERSION_ID < 80400) { return; } @@ -134,6 +136,7 @@ protected function wakeupReflection(ClassMetadataInterface $class, ReflectionSer protected function initializeReflection(ClassMetadataInterface $class, ReflectionService $reflService): void { + $class->setTypeRegistry($this->config->getTypeRegistry()); } protected function isEntity(ClassMetadataInterface $class): bool diff --git a/src/Mapping/MappingException.php b/src/Mapping/MappingException.php index 7fb906665..436348420 100644 --- a/src/Mapping/MappingException.php +++ b/src/Mapping/MappingException.php @@ -27,6 +27,11 @@ public static function typeNotFound(string $name): self return new self(sprintf('Type to be overwritten %s does not exist.', $name)); } + public static function invalidTypeForField(string $className, string $fieldName, string $type): self + { + return new self(sprintf("Type '%s' is not valid for field '%s' in class '%s'.", $type, $fieldName, $className)); + } + public static function typeRequirementsNotFulfilled(string $className, string $fieldName, string $type, string $reason): self { return new self(sprintf("Can not use '%s' type for field '%s' in class '%s' as its requirements are not met: %s.", $fieldName, $className, $type, $reason)); diff --git a/src/Persisters/DocumentPersister.php b/src/Persisters/DocumentPersister.php index 2841ee807..5cfae719f 100644 --- a/src/Persisters/DocumentPersister.php +++ b/src/Persisters/DocumentPersister.php @@ -28,7 +28,6 @@ use Doctrine\ODM\MongoDB\UnitOfWork; use Doctrine\ODM\MongoDB\Utility\CollectionHelper; use Doctrine\Persistence\Mapping\MappingException; -use InvalidArgumentException; use MongoDB\BSON\ObjectId; use MongoDB\Collection; use MongoDB\Driver\CursorInterface; @@ -210,7 +209,7 @@ public function executeInserts(array $options = []): void if ($this->class->isVersioned) { $versionMapping = $this->class->fieldMappings[$this->class->versionField]; $nextVersion = $this->class->propertyAccessors[$this->class->versionField]->getValue($document); - $type = Type::getType($versionMapping['type']); + $type = $this->class->getFieldType($this->class->versionField); assert($type instanceof Versionable); if ($nextVersion === null) { $nextVersion = $type->getNextVersion(null); @@ -288,7 +287,7 @@ private function executeUpsert(object $document, array $options): void if ($this->class->isVersioned) { $versionMapping = $this->class->fieldMappings[$this->class->versionField]; $nextVersion = $this->class->propertyAccessors[$this->class->versionField]->getValue($document); - $type = Type::getType($versionMapping['type']); + $type = $this->class->getFieldType($this->class->versionField); assert($type instanceof Versionable); if ($nextVersion === null) { $nextVersion = $type->getNextVersion(null); @@ -373,7 +372,7 @@ public function update(object $document, array $options = []): void if ($this->class->isVersioned) { $versionMapping = $this->class->fieldMappings[$this->class->versionField]; $currentVersion = $this->class->propertyAccessors[$this->class->versionField]->getValue($document); - $type = Type::getType($versionMapping['type']); + $type = $this->class->getFieldType($this->class->versionField); assert($type instanceof Versionable); $nextVersion = $type->getNextVersion($currentVersion); $update['$set'][$versionMapping['name']] = $type->convertToDatabaseValue($nextVersion); @@ -576,8 +575,7 @@ private function getShardKeyQuery(object $document): array $shardKeyQueryPart[$keyValue[0]] = $keyValue[1]; } } else { - $value = Type::getType($mapping['type'])->convertToDatabaseValue($data[$mapping['fieldName']]); - $shardKeyQueryPart[$key] = $value; + $shardKeyQueryPart[$key] = $this->class->getFieldType($mapping['fieldName'])->convertToDatabaseValue($data[$mapping['fieldName']]); } } @@ -1085,7 +1083,7 @@ public function prepareQueryOrNewObj(array $query, bool $isNewObj = false): arra * * @return mixed */ - private function convertToDatabaseValue(string $fieldName, $value, ?ClassMetadata $class = null) + public function convertToDatabaseValue(string $fieldName, $value, ?ClassMetadata $class = null) { if (is_array($value)) { foreach ($value as $k => $v) { @@ -1104,7 +1102,7 @@ private function convertToDatabaseValue(string $fieldName, $value, ?ClassMetadat $value = $value->value; } - return Type::convertPHPToDatabaseValue($value); + return $this->dm->getConfiguration()->getTypeRegistry()->convertToDatabaseValue($value); } $mapping = $class->fieldMappings[$fieldName]; @@ -1114,12 +1112,6 @@ private function convertToDatabaseValue(string $fieldName, $value, ?ClassMetadat return $value; } - if (! Type::hasType($typeName)) { - throw new InvalidArgumentException( - sprintf('Mapping type "%s" does not exist', $typeName), - ); - } - if ($value instanceof BackedEnum && isset($mapping['enumType'])) { $value = $value->value; } @@ -1128,10 +1120,7 @@ private function convertToDatabaseValue(string $fieldName, $value, ?ClassMetadat return $value; } - $type = Type::getType($typeName); - $value = $type->convertToDatabaseValue($value); - - return $value; + return $class->getFieldType($mapping['fieldName'])->convertToDatabaseValue($value); } private function prepareQueryReference(mixed $value, ClassMetadata $class): mixed @@ -1392,7 +1381,7 @@ private function prepareQueryExpression(array $expression, ClassMetadata $class) /** * Checks whether the value has DBRef fields. * - * This method doesn't check if the the value is a complete DBRef object, + * This method doesn't check if the value is a complete DBRef object, * although it should return true for a DBRef. Rather, we're checking that * the value has one or more fields for a DBref. In practice, this could be * $elemMatch criteria for matching a DBRef. @@ -1409,7 +1398,7 @@ private function hasDBRefFields($value): bool $value = get_object_vars($value); } - foreach ($value as $key => $value) { + foreach ($value as $key => $v) { if ($key === '$ref' || $key === '$id' || $key === '$db') { return true; } diff --git a/src/Persisters/PersistenceBuilder.php b/src/Persisters/PersistenceBuilder.php index 130269412..acf2f9e57 100644 --- a/src/Persisters/PersistenceBuilder.php +++ b/src/Persisters/PersistenceBuilder.php @@ -11,7 +11,6 @@ use Doctrine\ODM\MongoDB\Mapping\MappingException; use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionInterface; use Doctrine\ODM\MongoDB\Types\Incrementable; -use Doctrine\ODM\MongoDB\Types\Type; use Doctrine\ODM\MongoDB\UnitOfWork; use Doctrine\ODM\MongoDB\Utility\CollectionHelper; use InvalidArgumentException; @@ -64,7 +63,7 @@ public function prepareInsertData($document) $changeset = $this->uow->getDocumentChangeSet($document); $insertData = []; - foreach ($class->fieldMappings as $mapping) { + foreach ($class->fieldMappings as $fieldName => $mapping) { $new = $changeset[$mapping['fieldName']][1] ?? null; if ($new === null) { @@ -77,7 +76,7 @@ public function prepareInsertData($document) // @Field, @String, @Date, etc. if (! isset($mapping['association'])) { - $insertData[$mapping['name']] = Type::getType($mapping['type'])->convertToDatabaseValue($new); + $insertData[$mapping['name']] = $class->getFieldType($fieldName)->convertToDatabaseValue($new); // @ReferenceOne } elseif ($mapping['association'] === ClassMetadata::REFERENCE_ONE) { @@ -154,12 +153,12 @@ public function prepareUpdateData($document) if (! isset($mapping['association'])) { if (isset($mapping['strategy']) && $mapping['strategy'] === ClassMetadata::STORAGE_STRATEGY_INCREMENT) { $operator = '$inc'; - $type = Type::getType($mapping['type']); + $type = $class->getFieldType($fieldName); assert($type instanceof Incrementable); $value = $type->convertToDatabaseValue($type->diff($old, $new)); } else { $operator = '$set'; - $value = Type::getType($mapping['type'])->convertToDatabaseValue($new); + $value = $class->getFieldType($fieldName)->convertToDatabaseValue($new); } $updateData[$operator][$mapping['name']] = $value; @@ -258,12 +257,12 @@ public function prepareUpsertData($document) if (! isset($mapping['association'])) { if (empty($mapping['id']) && isset($mapping['strategy']) && $mapping['strategy'] === ClassMetadata::STORAGE_STRATEGY_INCREMENT) { $operator = '$inc'; - $type = Type::getType($mapping['type']); + $type = $class->getFieldType($fieldName); assert($type instanceof Incrementable); $value = $type->convertToDatabaseValue($type->diff($old, $new)); } else { $operator = '$set'; - $value = Type::getType($mapping['type'])->convertToDatabaseValue($new); + $value = $class->getFieldType($fieldName)->convertToDatabaseValue($new); } $updateData[$operator][$mapping['name']] = $value; @@ -360,7 +359,7 @@ public function prepareEmbeddedDocumentValue(array $embeddedMapping, $embeddedDo $embeddedDocumentValue = []; $class = $this->dm->getClassMetadata($embeddedDocument::class); - foreach ($class->fieldMappings as $mapping) { + foreach ($class->fieldMappings as $fieldName => $mapping) { // Skip notSaved fields if (! empty($mapping['notSaved'])) { continue; @@ -374,7 +373,7 @@ public function prepareEmbeddedDocumentValue(array $embeddedMapping, $embeddedDo switch ($mapping['association'] ?? null) { // @Field, @String, @Date, etc. case null: - $value = Type::getType($mapping['type'])->convertToDatabaseValue($rawValue); + $value = $class->getFieldType($fieldName)->convertToDatabaseValue($rawValue); break; case ClassMetadata::EMBED_ONE: diff --git a/src/Types/ClosureToPHP.php b/src/Types/ClosureToPHP.php index ef9bbfd1c..f631bc9e3 100644 --- a/src/Types/ClosureToPHP.php +++ b/src/Types/ClosureToPHP.php @@ -4,16 +4,12 @@ namespace Doctrine\ODM\MongoDB\Types; -use function sprintf; - /** This trait will be deprecated in 3.0 as this behavior will be used by default */ trait ClosureToPHP { /** @return string Redirects to the method convertToPHPValue from child class */ final public function closureToPHP(): string { - return sprintf(' - $type = \%s::getType($typeIdentifier); - $return = $type->convertToPHPValue($value);', Type::class); + return '$return = $this->class->getFieldType($fieldName)->convertToPHPValue($value);'; } } diff --git a/src/Types/Type.php b/src/Types/Type.php index 9e97d365d..61d60d87f 100644 --- a/src/Types/Type.php +++ b/src/Types/Type.php @@ -4,16 +4,10 @@ namespace Doctrine\ODM\MongoDB\Types; -use DateTimeImmutable; -use DateTimeInterface; use Doctrine\ODM\MongoDB\Mapping\MappingException; -use Doctrine\ODM\MongoDB\Types; -use Symfony\Component\Uid\Uuid; use function end; use function explode; -use function gettype; -use function is_object; use function str_replace; use function trigger_deprecation; @@ -59,48 +53,6 @@ abstract class Type /** @deprecated const was deprecated in doctrine/mongodb-odm 2.1 and will be removed in 3.0. Use Type::BOOL instead */ public const BOOLEAN = 'boolean'; - /** @var Type[] Map of already instantiated type objects. One instance per type (flyweight). */ - private static array $typeObjects = []; - - /** @var array The map of supported doctrine mapping types. */ - private static array $typesMap = [ - self::ID => Types\IdType::class, - self::INTID => Types\IntIdType::class, - self::CUSTOMID => Types\CustomIdType::class, - self::BOOL => Types\BooleanType::class, - self::BOOLEAN => Types\BooleanType::class, - self::INT => Types\IntType::class, - self::INTEGER => Types\IntType::class, - self::INT64 => Types\Int64Type::class, - self::FLOAT => Types\FloatType::class, - self::STRING => Types\StringType::class, - self::DATE => Types\DateType::class, - self::DATE_IMMUTABLE => Types\DateImmutableType::class, - self::KEY => Types\KeyType::class, - self::TIMESTAMP => Types\TimestampType::class, - self::BINDATA => Types\BinDataType::class, - self::BINDATAFUNC => Types\BinDataFuncType::class, - self::BINDATABYTEARRAY => Types\BinDataByteArrayType::class, - self::BINDATAUUID => Types\BinDataUUIDType::class, - self::BINDATAUUIDRFC4122 => Types\BinDataUUIDRFC4122Type::class, - self::BINDATAMD5 => Types\BinDataMD5Type::class, - self::BINDATACUSTOM => Types\BinDataCustomType::class, - self::HASH => Types\HashType::class, - self::COLLECTION => Types\CollectionType::class, - self::OBJECTID => Types\ObjectIdType::class, - self::RAW => Types\RawType::class, - self::DECIMAL128 => Types\Decimal128Type::class, - self::UUID => Types\BinaryUuidType::class, - self::VECTOR_FLOAT32 => Types\VectorFloat32Type::class, - self::VECTOR_INT8 => Types\VectorInt8Type::class, - self::VECTOR_PACKED_BIT => Types\VectorPackedBitType::class, - ]; - - /** Prevent instantiation and force use of the factory method. */ - final private function __construct() - { - } - /** * Converts a value from its PHP representation to its database representation * of this type. @@ -155,133 +107,125 @@ public function closureToPHP(): string /** * Register a new type in the type map. + * + * @deprecated Use {@see TypeRegistry::register()} instead */ public static function registerType(string $name, string $class): void { - self::$typesMap[$name] = $class; + trigger_deprecation('doctrine/mongodb-odm', '2.17', 'Type::registerType() is deprecated, use $typeRegistry->register() instead.'); + + TypeRegistry::getSharedInstance()->register($name, $class); } /** * Get a Type instance. * + * @deprecated Use {@see TypeRegistry::get()} instead + * * @throws InvalidTypeException */ public static function getType(string $type): Type { - if (! isset(self::$typesMap[$type])) { - throw InvalidTypeException::invalidTypeName($type); - } + trigger_deprecation('doctrine/mongodb-odm', '2.17', 'Type::getType() is deprecated, use $typeRegistry->get() instead.'); - return self::$typeObjects[$type] ??= new (self::$typesMap[$type]); + return TypeRegistry::getSharedInstance()->get($type); } /** * Get a Type instance based on the type of the passed php variable. * + * @deprecated Use {@see TypeRegistry::guessTypeFromValue()} instead + * * @param mixed $variable */ public static function getTypeFromPHPVariable($variable): ?Type { - if (is_object($variable)) { - if ($variable instanceof DateTimeImmutable) { - return self::getType(self::DATE_IMMUTABLE); - } - - if ($variable instanceof DateTimeInterface) { - return self::getType(self::DATE); - } - - if ($variable instanceof Uuid) { - return self::getType(self::UUID); - } - - // Try the variable class as type name - if (self::hasType($variable::class)) { - return self::getType($variable::class); - } - - return null; - } + trigger_deprecation('doctrine/mongodb-odm', '2.17', 'Type::getTypeFromPHPVariable() is deprecated without replacement.'); - return match (gettype($variable)) { - 'integer' => self::getType(self::INT), - 'boolean' => self::getType(self::BOOL), - 'double' => self::getType(self::FLOAT), - 'string' => self::getType(self::STRING), - default => null, - }; + return TypeRegistry::getSharedInstance()->guessTypeFromValue($variable); } /** + * @deprecated Use {@see TypeRegistry::convertToDatabaseValue()} instead + * * @param mixed $value * * @return mixed */ public static function convertPHPToDatabaseValue($value) { - $type = self::getTypeFromPHPVariable($value); - if ($type !== null) { - return $type->convertToDatabaseValue($value); - } + trigger_deprecation('doctrine/mongodb-odm', '2.17', 'Type::convertPHPToDatabaseValue() is deprecated, use $typeRegistry->convertToDatabaseValue() instead.'); - return $value; + return TypeRegistry::getSharedInstance()->convertToDatabaseValue($value); } /** * Adds a custom type to the type map. * + * @deprecated Use {@see TypeRegistry::register()} instead + * * @param class-string $className * * @throws MappingException - * - * @static */ public static function addType(string $name, string $className): void { - if (isset(self::$typesMap[$name])) { + trigger_deprecation('doctrine/mongodb-odm', '2.17', 'Type::addType() is deprecated, use $typeRegistry->register() instead.'); + + $registry = TypeRegistry::getSharedInstance(); + if ($registry->has($name)) { throw MappingException::typeExists($name); } - self::$typesMap[$name] = $className; + $registry->register($name, $className); } /** * Checks if exists support for a type. * - * @static + * @deprecated Use {@see TypeRegistry::has()} instead */ public static function hasType(string $name): bool { - return isset(self::$typesMap[$name]); + trigger_deprecation('doctrine/mongodb-odm', '2.17', 'Type::hasType() is deprecated, use $typeRegistry->has() instead.'); + + return TypeRegistry::getSharedInstance()->has($name); } /** * Overrides an already defined type to use a different implementation. * + * @deprecated Use {@see TypeRegistry::register()} instead + * * @param class-string $className * * @throws MappingException - * - * @static */ public static function overrideType(string $name, string $className): void { - if (! isset(self::$typesMap[$name])) { + trigger_deprecation('doctrine/mongodb-odm', '2.17', 'Type::overrideType() is deprecated, use $typeRegistry->register() instead.'); + + $registry = TypeRegistry::getSharedInstance(); + if (! $registry->has($name)) { throw MappingException::typeNotFound($name); } - self::$typesMap[$name] = $className; + $registry->register($name, $className); } /** * Get the types array map which holds all registered types and the corresponding * type class * + * @deprecated Will be removed in 3.0 + * * @phpstan-return array */ public static function getTypesMap(): array { - return self::$typesMap; + trigger_deprecation('doctrine/mongodb-odm', '2.17', 'Type::getTypesMap() is deprecated and will be removed in 3.0. Use TypeRegistry methods instead.'); + + return TypeRegistry::getSharedInstance()->getMap(); } public function __toString(): string diff --git a/src/Types/TypeRegistry.php b/src/Types/TypeRegistry.php new file mode 100644 index 000000000..be56e2700 --- /dev/null +++ b/src/Types/TypeRegistry.php @@ -0,0 +1,179 @@ +> The map of supported mapping types. */ + private array $typesMap = [ + Type::ID => IdType::class, + Type::INTID => IntIdType::class, + Type::CUSTOMID => CustomIdType::class, + Type::BOOL => BooleanType::class, + Type::BOOLEAN => BooleanType::class, + Type::INT => IntType::class, + Type::INTEGER => IntType::class, + Type::INT64 => Int64Type::class, + Type::FLOAT => FloatType::class, + Type::STRING => StringType::class, + Type::DATE => DateType::class, + Type::DATE_IMMUTABLE => DateImmutableType::class, + Type::KEY => KeyType::class, + Type::TIMESTAMP => TimestampType::class, + Type::BINDATA => BinDataType::class, + Type::BINDATAFUNC => BinDataFuncType::class, + Type::BINDATABYTEARRAY => BinDataByteArrayType::class, + Type::BINDATAUUID => BinDataUUIDType::class, + Type::BINDATAUUIDRFC4122 => BinDataUUIDRFC4122Type::class, + Type::BINDATAMD5 => BinDataMD5Type::class, + Type::BINDATACUSTOM => BinDataCustomType::class, + Type::HASH => HashType::class, + Type::COLLECTION => CollectionType::class, + Type::OBJECTID => ObjectIdType::class, + Type::RAW => RawType::class, + Type::DECIMAL128 => Decimal128Type::class, + Type::UUID => BinaryUuidType::class, + Type::VECTOR_FLOAT32 => VectorFloat32Type::class, + Type::VECTOR_INT8 => VectorInt8Type::class, + Type::VECTOR_PACKED_BIT => VectorPackedBitType::class, + ]; + + /** @var array Cache of instantiated Type objects */ + private array $typeObjects = []; + + private static ?TypeRegistry $sharedInstance = null; + + /** + * Register a new type in the type map. + * + * The name of the type can be a PHP class name used for automatic type detection + * + * @param class-string|Type $type + */ + public function register(string $name, string|Type $type): void + { + if ($type instanceof Type) { + $this->typesMap[$name] = $type::class; + $this->typeObjects[$name] = $type; + } else { + if (! is_subclass_of($type, Type::class)) { + throw new InvalidArgumentException(sprintf('Type class "%s" must be a subclass of "%s".', $type, Type::class)); + } + + try { + $instance = new $type(); + } catch (ArgumentCountError) { // @phpstan-ignore catch.neverThrown + throw new InvalidArgumentException(sprintf('Type class "%s" must not have a constructor with required parameters to be registered by class name. Register an instance of the class instead.', $type)); + } + + $this->typesMap[$name] = $type; + $this->typeObjects[$name] = $instance; + } + } + + /** + * Checks if exists support for a type. + */ + public function has(string $name): bool + { + return isset($this->typesMap[$name]); + } + + /** + * Get a Type instance. + * + * @throws InvalidTypeException + */ + public function get(string $name): Type + { + if (! isset($this->typesMap[$name])) { + throw InvalidTypeException::invalidTypeName($name); + } + + return $this->typeObjects[$name] ??= new $this->typesMap[$name](); + } + + /** + * Determine the database representation of a value based on its PHP type. + */ + public function convertToDatabaseValue(mixed $value): mixed + { + $type = $this->guessTypeFromValue($value); + + if ($type === null) { + return $value; + } + + return $type->convertToDatabaseValue($value); + } + + /** + * Get a Type instance based on the type of the passed PHP variable. + * + * @internal + */ + public function guessTypeFromValue(mixed $variable): ?Type + { + if (is_object($variable)) { + if ($variable instanceof DateTimeImmutable) { + return $this->get(Type::DATE_IMMUTABLE); + } + + if ($variable instanceof DateTimeInterface) { + return $this->get(Type::DATE); + } + + if ($variable instanceof Uuid) { + return $this->get(Type::UUID); + } + + // Try the variable class as a type name + if ($this->has($variable::class)) { + return $this->get($variable::class); + } + + return null; + } + + return match (gettype($variable)) { + 'integer' => $this->get(Type::INT), + 'boolean' => $this->get(Type::BOOL), + 'double' => $this->get(Type::FLOAT), + 'string' => $this->get(Type::STRING), + default => null, + }; + } + + /** + * Get the type array map which holds all registered types and the corresponding + * type class + * + * @return array> + */ + public function getMap(): array + { + return $this->typesMap; + } + + /** @internal Do not use this method. */ + public static function getSharedInstance(): TypeRegistry + { + return self::$sharedInstance ??= new self(); + } +} diff --git a/src/UnitOfWork.php b/src/UnitOfWork.php index 47fb4233a..433165faf 100644 --- a/src/UnitOfWork.php +++ b/src/UnitOfWork.php @@ -16,7 +16,6 @@ use Doctrine\ODM\MongoDB\Persisters\PersistenceBuilder; use Doctrine\ODM\MongoDB\Proxy\InternalProxy; use Doctrine\ODM\MongoDB\Query\Query; -use Doctrine\ODM\MongoDB\Types\DateType; use Doctrine\ODM\MongoDB\Types\Type; use Doctrine\ODM\MongoDB\Utility\CollectionHelper; use Doctrine\ODM\MongoDB\Utility\LifecycleEventManager; @@ -832,12 +831,11 @@ private function computeOrRecomputeChangeSet(ClassMetadata $class, object $docum // skip equivalent date values if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'date') { - $dateType = Type::getType('date'); - assert($dateType instanceof DateType); + $dateType = $class->getFieldType($propName); $dbOrgValue = $dateType->convertToDatabaseValue($orgValue); $dbActualValue = $dateType->convertToDatabaseValue($actualValue); - // We rely on loose comparison to compare every field (including microseconds) + // We rely on loose comparison to compare every field // phpcs:ignore SlevomatCodingStandard.Operators.DisallowEqualOperators.DisallowedEqualOperator if ($dbOrgValue == $dbActualValue) { continue; diff --git a/tests/Documentation/CustomMapping/CustomMappingTest.php b/tests/Documentation/CustomMapping/CustomMappingTest.php index 54b515b16..795e0f6f5 100644 --- a/tests/Documentation/CustomMapping/CustomMappingTest.php +++ b/tests/Documentation/CustomMapping/CustomMappingTest.php @@ -7,23 +7,13 @@ use DateTimeImmutable; use DateTimeZone; use Doctrine\ODM\MongoDB\Tests\BaseTestCase; -use Doctrine\ODM\MongoDB\Types\Type; -use PHPUnit\Framework\Attributes\After; -use ReflectionProperty; class CustomMappingTest extends BaseTestCase { - #[After] - public function restoreTypeMap(): void - { - $r = new ReflectionProperty(Type::class, 'typesMap'); - $r->setValue(null, $r->getDefaultValue()); - } - public function testTest(): void { - Type::addType('date_with_timezone', DateTimeWithTimezoneType::class); - Type::overrideType('date_immutable', DateTimeWithTimezoneType::class); + $this->config->getTypeRegistry()->register('date_with_timezone', new DateTimeWithTimezoneType()); + $this->config->getTypeRegistry()->register('date_immutable', new DateTimeWithTimezoneType()); $thing = new Thing(); $thing->date = new DateTimeImmutable('2021-01-01 00:00:00', new DateTimeZone('Africa/Tripoli')); diff --git a/tests/Tests/BaseTestCase.php b/tests/Tests/BaseTestCase.php index 835c6c0e9..361aaa432 100644 --- a/tests/Tests/BaseTestCase.php +++ b/tests/Tests/BaseTestCase.php @@ -10,6 +10,7 @@ use Doctrine\ODM\MongoDB\Proxy\Factory\NativeLazyObjectFactory; use Doctrine\ODM\MongoDB\Proxy\InternalProxy; use Doctrine\ODM\MongoDB\Tests\Query\Filter\Filter; +use Doctrine\ODM\MongoDB\Types\TypeRegistry; use Doctrine\ODM\MongoDB\UnitOfWork; use Doctrine\Persistence\Mapping\Driver\FileClassLocator; use Doctrine\Persistence\Mapping\Driver\MappingDriver; @@ -21,6 +22,7 @@ use MongoDB\Model\DatabaseInfo; use PHPUnit\Framework\TestCase; use ProxyManager\Proxy\LazyLoadingInterface; +use ReflectionProperty; use function array_key_exists; use function array_map; @@ -45,18 +47,22 @@ abstract class BaseTestCase extends TestCase { protected static ?bool $supportsTransactions; protected static bool $allowsTransactions = true; + protected Configuration $config; protected ?DocumentManager $dm; protected UnitOfWork $uow; private bool $disableFailPoints = false; protected function setUp(): void { - $this->dm = static::createTestDocumentManager(); - $this->uow = $this->dm->getUnitOfWork(); + $this->dm = static::createTestDocumentManager(); + $this->config = $this->dm->getConfiguration(); + $this->uow = $this->dm->getUnitOfWork(); } protected function tearDown(): void { + (new ReflectionProperty(TypeRegistry::class, 'sharedInstance'))->setValue(null, null); + if (! $this->dm) { return; } diff --git a/tests/Tests/ConfigurationTest.php b/tests/Tests/ConfigurationTest.php index 87c675741..259e96c00 100644 --- a/tests/Tests/ConfigurationTest.php +++ b/tests/Tests/ConfigurationTest.php @@ -9,6 +9,7 @@ use Doctrine\ODM\MongoDB\ConfigurationException; use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionFactory; use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionGenerator; +use Doctrine\ODM\MongoDB\Types\TypeRegistry; use LogicException; use MongoDB\Driver\Manager; use PHPUnit\Framework\Attributes\IgnoreDeprecations; @@ -257,4 +258,23 @@ public function testKmsProviderTypeMustBeString(): void // @phpstan-ignore argument.type $c->setKmsProvider(['type' => ['not', 'a', 'string']]); } + + public function testTypRegistry(): void + { + $c = new Configuration(); + self::assertSame($c->getTypeRegistry(), TypeRegistry::getSharedInstance()); + + $typeRegistry = new TypeRegistry(); + $c->setTypeRegistry($typeRegistry); + self::assertSame($typeRegistry, $c->getTypeRegistry()); + } + + public function testTypeRegistryCannotBeReplacedOnceSet(): void + { + $c = new Configuration(); + $c->setTypeRegistry(new TypeRegistry()); + + self::expectException(LogicException::class); + $c->setTypeRegistry(new TypeRegistry()); + } } diff --git a/tests/Tests/Functional/CustomTypeTest.php b/tests/Tests/Functional/CustomTypeTest.php index 6543998b8..b62102b0f 100644 --- a/tests/Tests/Functional/CustomTypeTest.php +++ b/tests/Tests/Functional/CustomTypeTest.php @@ -10,9 +10,9 @@ use Doctrine\ODM\MongoDB\Tests\CaptureDeprecationMessages; use Doctrine\ODM\MongoDB\Types\ClosureToPHP; use Doctrine\ODM\MongoDB\Types\Type; +use Doctrine\ODM\MongoDB\Types\TypeRegistry; use Exception; -use PHPUnit\Framework\Attributes\After; -use ReflectionProperty; +use InvalidArgumentException; use function array_map; use function array_values; @@ -23,20 +23,15 @@ class CustomTypeTest extends BaseTestCase { use CaptureDeprecationMessages; + private TypeRegistry $registry; + public function setUp(): void { parent::setUp(); - Type::addType('date_collection', DateCollectionType::class); - Type::addType(Language::class, LanguageType::class); - Type::addType('custom_type_without_closure_to_php', CustomTypeWithoutClosureToPHP::class); - } - - #[After] - public function restoreTypeMap(): void - { - $r = new ReflectionProperty(Type::class, 'typesMap'); - $r->setValue(null, $r->getDefaultValue()); + $this->registry = $this->config->getTypeRegistry(); + $this->registry->register('date_collection', new DateCollectionType()); + $this->registry->register(Language::class, LanguageType::class); } public function testCustomTypeValueConversions(): void @@ -84,25 +79,33 @@ public function testCustomTypeDetection(): void self::assertSame('fr', $country->lang->code); } - public function testTypeFromPHPVariable(): void + public function testConvertToDatabaseValue(): void { $lang = new Language('French', 'fr'); - $type = Type::getTypeFromPHPVariable($lang); + $type = $this->registry->guessTypeFromValue($lang); self::assertInstanceOf(LanguageType::class, $type); - $databaseValue = Type::convertPHPToDatabaseValue($lang); + $databaseValue = $this->registry->convertToDatabaseValue($lang); self::assertSame(['name' => 'French', 'code' => 'fr'], $databaseValue); } public function testNotOverridingClosureToPHPIsDeprecated(): void { - $type = Type::getType('custom_type_without_closure_to_php'); + $type = new CustomTypeWithoutClosureToPHP(); $code = $this->captureDeprecationMessages(static fn () => $type->closureToPHP(), $deprecations); self::assertSame('$return = $value;', $code); self::assertSame(['Since doctrine/mongodb-odm 2.16: The method Type::closureToPHP() will change its default implementation in 3.0 to use convertToPHPValue(). Override this method if you need custom behavior before upgrading to 3.0 or use the trait ClosureToPHP to get the upcoming behavior now.'], $deprecations); } + + public function testConstructorNotAllowedInCustomTypeRegisteredByClassName(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Type class "Doctrine\ODM\MongoDB\Tests\Functional\CustomTypeWithConstructor" must not have a constructor to be registered by class name. Register an instance of the class instead.'); + + $this->registry->register('custom_with_constructor', CustomTypeWithConstructor::class); + } } class DateCollectionType extends Type @@ -122,7 +125,8 @@ public function convertToDatabaseValue($value) throw new CustomTypeException('Array expected.'); } - $converter = Type::getType('date'); + $registry = new TypeRegistry(); + $converter = $registry->get('date'); $value = array_map(static fn ($date) => $converter->convertToDatabaseValue($date), array_values($value)); @@ -139,7 +143,8 @@ public function convertToPHPValue($value) throw new CustomTypeException('Array expected.'); } - $converter = Type::getType('date'); + $registry = new TypeRegistry(); + $converter = $registry->get('date'); $value = array_map(static fn ($date) => $converter->convertToPHPValue($date), array_values($value)); @@ -216,3 +221,10 @@ public function convertToPHPValue($value): ?Language class CustomTypeWithoutClosureToPHP extends Type { } + +class CustomTypeWithConstructor extends Type +{ + public function __construct() + { + } +} diff --git a/tests/Tests/Functional/DocumentPersisterTest.php b/tests/Tests/Functional/DocumentPersisterTest.php index 885d63f7d..3f19aa692 100644 --- a/tests/Tests/Functional/DocumentPersisterTest.php +++ b/tests/Tests/Functional/DocumentPersisterTest.php @@ -223,8 +223,7 @@ public function testPrepareQueryOrNewObjWithCustomTypedId(array $expected, array { $class = DocumentPersisterTestDocumentWithCustomId::class; $documentPersister = $this->uow->getDocumentPersister($class); - - Type::registerType('DocumentPersisterCustomId', DocumentPersisterCustomIdType::class); + $this->config->getTypeRegistry()->register('DocumentPersisterCustomId', new DocumentPersisterCustomIdType()); self::assertEquals( $expected, @@ -235,7 +234,7 @@ public function testPrepareQueryOrNewObjWithCustomTypedId(array $expected, array #[DataProvider('queryProviderForDocumentWithReferenceToDocumentWithCustomTypedId')] public function testPrepareQueryOrNewObjWithReferenceToDocumentWithCustomTypedId(Closure $getTestCase): void { - Type::registerType('DocumentPersisterCustomId', DocumentPersisterCustomIdType::class); + $this->config->getTypeRegistry()->register('DocumentPersisterCustomId', new DocumentPersisterCustomIdType()); $class = DocumentPersisterTestDocumentWithReferenceToDocumentWithCustomId::class; $documentPersister = $this->uow->getDocumentPersister($class); diff --git a/tests/Tests/Functional/Ticket/GH2789Test.php b/tests/Tests/Functional/Ticket/GH2789Test.php index 92ef106f8..9590efd97 100644 --- a/tests/Tests/Functional/Ticket/GH2789Test.php +++ b/tests/Tests/Functional/Ticket/GH2789Test.php @@ -9,24 +9,15 @@ use Doctrine\ODM\MongoDB\Types\Type; use Doctrine\ODM\MongoDB\Types\Versionable; use MongoDB\BSON\Binary; -use PHPUnit\Framework\Attributes\After; -use ReflectionProperty; use function assert; use function is_int; class GH2789Test extends BaseTestCase { - #[After] - public function restoreTypeMap(): void - { - $r = new ReflectionProperty(Type::class, 'typesMap'); - $r->setValue(null, $r->getDefaultValue()); - } - public function testVersionWithCustomType(): void { - Type::addType(GH2789CustomType::class, GH2789CustomType::class); + $this->config->getTypeRegistry()->register(GH2789CustomType::class, new GH2789CustomType()); $doc = new GH2789VersionedUuid('original message'); diff --git a/tests/Tests/Mapping/AnnotationDriverTest.php b/tests/Tests/Mapping/AnnotationDriverTest.php index 3bad2754a..1e05b9029 100644 --- a/tests/Tests/Mapping/AnnotationDriverTest.php +++ b/tests/Tests/Mapping/AnnotationDriverTest.php @@ -9,21 +9,20 @@ use Doctrine\ODM\MongoDB\Mapping\Annotations\Document; use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; use Doctrine\ODM\MongoDB\Mapping\Driver\AnnotationDriver; +use Doctrine\ODM\MongoDB\Tests\CaptureDeprecationMessages; +use Doctrine\ODM\MongoDB\Types\TypeRegistry; use Doctrine\Persistence\Mapping\Driver\FileClassLocator; use Doctrine\Persistence\Mapping\Driver\MappingDriver; use PHPUnit\Framework\Attributes\RequiresMethod; -use function call_user_func; use function class_exists; -use function restore_error_handler; -use function set_error_handler; use function sprintf; -use const E_USER_DEPRECATED; - #[RequiresMethod(AnnotationReader::class, '__construct')] class AnnotationDriverTest extends AbstractAnnotationDriverTestCase { + use CaptureDeprecationMessages; + protected static function loadDriver(array $paths = []): MappingDriver { if (class_exists(FileClassLocator::class)) { @@ -37,6 +36,7 @@ public function testIndexesClassAnnotationEmitsDeprecationMessage(): void { $driver = static::loadDriver(); $classMetadata = new ClassMetadata(DeprecatedIndexesClassAnnotation::class); + $classMetadata->setTypeRegistry(new TypeRegistry()); $this->captureDeprecationMessages( static fn () => $driver->loadMetadataForClass($classMetadata->name, $classMetadata), @@ -56,6 +56,7 @@ public function testIndexesOptionOfDocumentClassAnnotationEmitsDeprecationMessag { $driver = static::loadDriver(); $classMetadata = new ClassMetadata(DeprecatedDocumentClassAnnotationIndexesOption::class); + $classMetadata->setTypeRegistry(new TypeRegistry()); $this->captureDeprecationMessages( static fn () => $driver->loadMetadataForClass($classMetadata->name, $classMetadata), @@ -75,6 +76,7 @@ public function testIndexesPropertyAnnotationEmitsDeprecationMessage(): void { $driver = static::loadDriver(); $classMetadata = new ClassMetadata(DeprecatedIndexesPropertyAnnotation::class); + $classMetadata->setTypeRegistry(new TypeRegistry()); $this->captureDeprecationMessages( static fn () => $driver->loadMetadataForClass($classMetadata->name, $classMetadata), @@ -89,30 +91,6 @@ public function testIndexesPropertyAnnotationEmitsDeprecationMessage(): void self::assertTrue(isset($indexes[0]['keys']['foo'])); self::assertEquals(1, $indexes[0]['keys']['foo']); } - - /** - * @param list $errors - * - * @param-out list $errors - */ - private function captureDeprecationMessages(callable $callable, ?array &$errors): mixed - { - /* TODO: this method can be replaced with expectUserDeprecationMessage() in PHPUnit 11+. - * See: https://docs.phpunit.de/en/11.1/error-handling.html#expecting-deprecations-e-user-deprecated */ - $errors = []; - - set_error_handler(static function (int $errno, string $errstr) use (&$errors): bool { - $errors[] = $errstr; - - return false; - }, E_USER_DEPRECATED); - - try { - return call_user_func($callable); - } finally { - restore_error_handler(); - } - } } /** diff --git a/tests/Tests/Mapping/ClassMetadataTest.php b/tests/Tests/Mapping/ClassMetadataTest.php index 612ad8c52..7c7258e7f 100644 --- a/tests/Tests/Mapping/ClassMetadataTest.php +++ b/tests/Tests/Mapping/ClassMetadataTest.php @@ -17,6 +17,7 @@ use Doctrine\ODM\MongoDB\Tests\CaptureDeprecationMessages; use Doctrine\ODM\MongoDB\Tests\ClassMetadataTestUtil; use Doctrine\ODM\MongoDB\Types\Type; +use Doctrine\ODM\MongoDB\Types\TypeRegistry; use Doctrine\ODM\MongoDB\Utility\CollectionHelper; use DoctrineGlobal_Article; use DoctrineGlobal_User; @@ -57,6 +58,7 @@ class ClassMetadataTest extends BaseTestCase public function testClassMetadataInstanceSerialization(): void { $cm = new ClassMetadata(CmsUser::class); + $cm->setTypeRegistry(new TypeRegistry()); // Test initial state self::assertInstanceOf(LegacyReflectionFields::class, $cm->getReflectionProperties()); @@ -146,10 +148,12 @@ public function testExtendingClassMetadata(): void public function testOwningSideAndInverseSide(): void { $cm = new ClassMetadata(User::class); + $cm->setTypeRegistry(new TypeRegistry()); $cm->mapOneReference(['fieldName' => 'account', 'targetDocument' => Account::class, 'inversedBy' => 'user']); self::assertTrue($cm->fieldMappings['account']['isOwningSide']); $cm = new ClassMetadata(Account::class); + $cm->setTypeRegistry(new TypeRegistry()); $cm->mapOneReference(['fieldName' => 'user', 'targetDocument' => Account::class, 'mappedBy' => 'account']); self::assertTrue($cm->fieldMappings['user']['isInverseSide']); } @@ -157,6 +161,7 @@ public function testOwningSideAndInverseSide(): void public function testFieldIsNullable(): void { $cm = new ClassMetadata(CmsUser::class); + $cm->setTypeRegistry(new TypeRegistry()); // Explicit Nullable $cm->mapField(['fieldName' => 'status', 'nullable' => true, 'type' => 'string']); @@ -174,6 +179,7 @@ public function testFieldIsNullable(): void public function testFieldTypeFromReflection(): void { $cm = new ClassMetadata(UserTyped::class); + $cm->setTypeRegistry(new TypeRegistry()); // String $cm->mapField(['fieldName' => 'username']); @@ -213,6 +219,7 @@ public function testFieldTypeFromReflection(): void public function testEnumTypeFromReflection(): void { $cm = new ClassMetadata(Card::class); + $cm->setTypeRegistry(new TypeRegistry()); $cm->mapField(['fieldName' => 'suit']); self::assertEquals(Type::STRING, $cm->getTypeOfField('suit')); @@ -233,6 +240,7 @@ public function testEnumTypeFromReflection(): void public function testEnumPropertyAccessorSerialization(): void { $cm = new ClassMetadata(Card::class); + $cm->setTypeRegistry(new TypeRegistry()); $cm->mapField(['fieldName' => 'suit']); self::assertInstanceOf(EnumPropertyAccessor::class, $cm->propertyAccessors['suit']); @@ -246,6 +254,7 @@ public function testEnumPropertyAccessorSerialization(): void public function testEnumTypeFromReflectionMustBeBacked(): void { $cm = new ClassMetadata(Card::class); + $cm->setTypeRegistry(new TypeRegistry()); $this->expectException(MappingException::class); $this->expectExceptionMessage( @@ -262,6 +271,7 @@ public function testEnumTypeMustPointToAnEnum(): void }; $cm = new ClassMetadata($object::class); + $cm->setTypeRegistry(new TypeRegistry()); $this->expectException(MappingException::class); $this->expectExceptionMessage( @@ -282,6 +292,7 @@ public function testEnumTypeMustPointToABackedEnum(): void }; $cm = new ClassMetadata($object::class); + $cm->setTypeRegistry(new TypeRegistry()); $this->expectException(MappingException::class); $this->expectExceptionMessage( @@ -299,6 +310,7 @@ public function testMapAssocationInGlobalNamespace(): void require_once __DIR__ . '/Documents/GlobalNamespaceDocument.php'; $cm = new ClassMetadata(DoctrineGlobal_Article::class); + $cm->setTypeRegistry(new TypeRegistry()); $cm->mapManyEmbedded([ 'fieldName' => 'author', 'targetDocument' => DoctrineGlobal_User::class, @@ -310,6 +322,7 @@ public function testMapAssocationInGlobalNamespace(): void public function testMapManyToManyJoinTableDefaults(): void { $cm = new ClassMetadata(CmsUser::class); + $cm->setTypeRegistry(new TypeRegistry()); $cm->mapManyEmbedded( [ 'fieldName' => 'groups', @@ -324,6 +337,7 @@ public function testMapManyToManyJoinTableDefaults(): void public function testGetAssociationTargetClassWithoutTargetDocument(): void { $cm = new ClassMetadata(CmsUser::class); + $cm->setTypeRegistry(new TypeRegistry()); $cm->mapManyEmbedded( [ 'fieldName' => 'groups', @@ -339,6 +353,7 @@ public function testSetDiscriminatorMapInGlobalNamespace(): void require_once __DIR__ . '/Documents/GlobalNamespaceDocument.php'; $cm = new ClassMetadata(DoctrineGlobal_User::class); + $cm->setTypeRegistry(new TypeRegistry()); $cm->setDiscriminatorMap(['descr' => DoctrineGlobal_Article::class, 'foo' => DoctrineGlobal_User::class]); self::assertEquals(DoctrineGlobal_Article::class, $cm->discriminatorMap['descr']); @@ -350,6 +365,7 @@ public function testSetSubClassesInGlobalNamespace(): void require_once __DIR__ . '/Documents/GlobalNamespaceDocument.php'; $cm = new ClassMetadata(DoctrineGlobal_User::class); + $cm->setTypeRegistry(new TypeRegistry()); $cm->setSubclasses([DoctrineGlobal_Article::class]); self::assertEquals(DoctrineGlobal_Article::class, $cm->subClasses[0]); @@ -358,6 +374,7 @@ public function testSetSubClassesInGlobalNamespace(): void public function testDuplicateFieldMapping(): void { $cm = new ClassMetadata(CmsUser::class); + $cm->setTypeRegistry(new TypeRegistry()); $a1 = ['reference' => true, 'type' => 'many', 'fieldName' => 'name', 'targetDocument' => stdClass::class]; $a2 = ['type' => 'string', 'fieldName' => 'name']; @@ -370,6 +387,7 @@ public function testDuplicateFieldMapping(): void public function testDuplicateColumnNameDiscriminatorColumnThrowsMappingException(): void { $cm = new ClassMetadata(CmsUser::class); + $cm->setTypeRegistry(new TypeRegistry()); $cm->mapField(['fieldName' => 'name', 'type' => Type::STRING]); $this->expectException(MappingException::class); @@ -379,6 +397,7 @@ public function testDuplicateColumnNameDiscriminatorColumnThrowsMappingException public function testDuplicateFieldNameDiscriminatorColumn2ThrowsMappingException(): void { $cm = new ClassMetadata(CmsUser::class); + $cm->setTypeRegistry(new TypeRegistry()); $cm->setDiscriminatorField('name'); $this->expectException(MappingException::class); @@ -388,6 +407,7 @@ public function testDuplicateFieldNameDiscriminatorColumn2ThrowsMappingException public function testDuplicateFieldAndAssocationMapping1(): void { $cm = new ClassMetadata(CmsUser::class); + $cm->setTypeRegistry(new TypeRegistry()); $cm->mapField(['fieldName' => 'name', 'type' => Type::STRING]); $cm->mapOneEmbedded(['fieldName' => 'name', 'targetDocument' => CmsUser::class]); @@ -397,6 +417,7 @@ public function testDuplicateFieldAndAssocationMapping1(): void public function testDuplicateFieldAndAssocationMapping2(): void { $cm = new ClassMetadata(CmsUser::class); + $cm->setTypeRegistry(new TypeRegistry()); $cm->mapOneEmbedded(['fieldName' => 'name', 'targetDocument' => CmsUser::class]); $cm->mapField(['fieldName' => 'name', 'type' => 'string']); @@ -406,6 +427,7 @@ public function testDuplicateFieldAndAssocationMapping2(): void public function testMapNotExistingFieldThrowsException(): void { $cm = new ClassMetadata(CmsUser::class); + $cm->setTypeRegistry(new TypeRegistry()); $this->expectException(ReflectionException::class); $cm->mapField(['fieldName' => 'namee', 'type' => 'string']); } @@ -455,6 +477,7 @@ public function testDefaultDiscriminatorField(): void }; $cm = new ClassMetadata($object::class); + $cm->setTypeRegistry(new TypeRegistry()); $cm->mapField([ 'fieldName' => 'assoc', @@ -575,6 +598,7 @@ public function testSetFieldValueWithProxy(): void public function testSetCustomRepositoryClass(): void { $cm = new ClassMetadata(self::class); + $cm->setTypeRegistry(new TypeRegistry()); $cm->setCustomRepositoryClass(Repository::class); @@ -656,6 +680,7 @@ public function testInvokeLifecycleCallbacksShouldAllowProxyOfExactClass(): void public function testSimpleReferenceRequiresTargetDocument(): void { $cm = new ClassMetadata('stdClass'); + $cm->setTypeRegistry(new TypeRegistry()); $this->expectException(MappingException::class); $this->expectExceptionMessage('Target document must be specified for identifier reference: stdClass::assoc'); @@ -670,6 +695,7 @@ public function testSimpleReferenceRequiresTargetDocument(): void public function testSimpleAsStringReferenceRequiresTargetDocument(): void { $cm = new ClassMetadata('stdClass'); + $cm->setTypeRegistry(new TypeRegistry()); $this->expectException(MappingException::class); $this->expectExceptionMessage('Target document must be specified for identifier reference: stdClass::assoc'); @@ -686,6 +712,7 @@ public function testSimpleAsStringReferenceRequiresTargetDocument(): void public function testRepositoryMethodCanNotBeCombinedWithSkipLimitAndSort(string $prop, $value): void { $cm = new ClassMetadata('stdClass'); + $cm->setTypeRegistry(new TypeRegistry()); $this->expectException(MappingException::class); $this->expectExceptionMessage( @@ -712,6 +739,7 @@ public static function provideRepositoryMethodCanNotBeCombinedWithSkipLimitAndSo public function testStoreAsIdReferenceRequiresTargetDocument(): void { $cm = new ClassMetadata('stdClass'); + $cm->setTypeRegistry(new TypeRegistry()); $this->expectException(MappingException::class); $this->expectExceptionMessage('Target document must be specified for identifier reference: stdClass::assoc'); @@ -790,6 +818,7 @@ public static function provideOwningAndInversedRefsNeedTargetDocument(): array public function testAddInheritedAssociationMapping(): void { $cm = new ClassMetadata('stdClass'); + $cm->setTypeRegistry(new TypeRegistry()); $mapping = ClassMetadataTestUtil::getFieldMapping([ 'fieldName' => 'assoc', @@ -808,6 +837,7 @@ public function testAddInheritedAssociationMapping(): void public function testIdFieldsTypeMustNotBeOverridden(): void { $cm = new ClassMetadata('stdClass'); + $cm->setTypeRegistry(new TypeRegistry()); $cm->setIdentifier('id'); $this->expectException(MappingException::class); $this->expectExceptionMessage('stdClass::id was declared an identifier and must stay this way.'); @@ -820,6 +850,7 @@ public function testIdFieldsTypeMustNotBeOverridden(): void public function testReferenceManySortMustNotBeUsedWithNonSetCollectionStrategy(): void { $cm = new ClassMetadata('stdClass'); + $cm->setTypeRegistry(new TypeRegistry()); $this->expectException(MappingException::class); $this->expectExceptionMessage( 'ReferenceMany\'s sort can not be used with addToSet and pushAll strategies, ' . @@ -837,6 +868,7 @@ public function testReferenceManySortMustNotBeUsedWithNonSetCollectionStrategy() public function testSetShardKeyForClassWithoutInheritance(): void { $cm = new ClassMetadata('stdClass'); + $cm->setTypeRegistry(new TypeRegistry()); $cm->setShardKey(['id' => 'asc']); $shardKey = $cm->getShardKey(); @@ -858,6 +890,7 @@ public function testSetShardKeyForClassWithSingleCollectionInheritance(): void public function testSetShardKeyForClassWithSingleCollectionInheritanceWhichAlreadyHasIt(): void { $cm = new ClassMetadata('stdClass'); + $cm->setTypeRegistry(new TypeRegistry()); $cm->setShardKey(['id' => 'asc']); $cm->inheritanceType = ClassMetadata::INHERITANCE_TYPE_SINGLE_COLLECTION; @@ -880,6 +913,7 @@ public function testSetShardKeyForClassWithCollPerClassInheritance(): void public function testIsNotShardedIfThereIsNoShardKey(): void { $cm = new ClassMetadata('stdClass'); + $cm->setTypeRegistry(new TypeRegistry()); self::assertFalse($cm->isSharded()); } @@ -887,6 +921,7 @@ public function testIsNotShardedIfThereIsNoShardKey(): void public function testIsShardedIfThereIsAShardKey(): void { $cm = new ClassMetadata('stdClass'); + $cm->setTypeRegistry(new TypeRegistry()); $cm->setShardKey(['id' => 'asc']); self::assertTrue($cm->isSharded()); @@ -909,6 +944,7 @@ public function testNoIncrementFieldsAllowedInShardKey(): void }; $cm = new ClassMetadata($object::class); + $cm->setTypeRegistry(new TypeRegistry()); $cm->mapField([ 'fieldName' => 'inc', 'type' => 'int', @@ -927,6 +963,7 @@ public function testNoCollectionsInShardKey(): void }; $cm = new ClassMetadata($object::class); + $cm->setTypeRegistry(new TypeRegistry()); $cm->mapField([ 'fieldName' => 'collection', 'type' => 'collection', @@ -944,6 +981,7 @@ public function testNoEmbedManyInShardKey(): void }; $cm = new ClassMetadata($object::class); + $cm->setTypeRegistry(new TypeRegistry()); $cm->mapManyEmbedded(['fieldName' => 'embedMany']); $this->expectException(MappingException::class); $this->expectExceptionMessage('No multikey indexes are allowed in the shard key'); @@ -958,6 +996,7 @@ public function testNoReferenceManyInShardKey(): void }; $cm = new ClassMetadata($object::class); + $cm->setTypeRegistry(new TypeRegistry()); $cm->mapManyEmbedded(['fieldName' => 'referenceMany']); $this->expectException(MappingException::class); $this->expectExceptionMessage('No multikey indexes are allowed in the shard key'); @@ -983,24 +1022,28 @@ public function testArbitraryFieldInGridFSFileThrowsException(): void public function testDefaultValueForValidator(): void { $cm = new ClassMetadata('stdClass'); + $cm->setTypeRegistry(new TypeRegistry()); self::assertNull($cm->getValidator()); } public function testDefaultValueForValidationAction(): void { $cm = new ClassMetadata('stdClass'); + $cm->setTypeRegistry(new TypeRegistry()); self::assertEquals(ClassMetadata::SCHEMA_VALIDATION_ACTION_ERROR, $cm->getValidationAction()); } public function testDefaultValueForValidationLevel(): void { $cm = new ClassMetadata('stdClass'); + $cm->setTypeRegistry(new TypeRegistry()); self::assertEquals(ClassMetadata::SCHEMA_VALIDATION_LEVEL_STRICT, $cm->getValidationLevel()); } public function testEmptySearchIndexDefinition(): void { $cm = new ClassMetadata('stdClass'); + $cm->setTypeRegistry(new TypeRegistry()); $this->expectException(MappingException::class); $this->expectExceptionMessage('stdClass search index "default" must be dynamic or specify a field mapping'); @@ -1012,6 +1055,7 @@ public function testEmptySearchIndexDefinition(): void public function testSearchIndexDefinition(?string $name, string $expectedName, array $definition): void { $cm = new ClassMetadata('stdClass'); + $cm->setTypeRegistry(new TypeRegistry()); $cm->addSearchIndex($definition, $name); self::assertSame([ @@ -1029,6 +1073,7 @@ public function testSearchIndexDefinition(?string $name, string $expectedName, a public function testEmptyVectorSearchIndexDefinition(array $definition): void { $cm = new ClassMetadata('stdClass'); + $cm->setTypeRegistry(new TypeRegistry()); $this->expectException(MappingException::class); $this->expectExceptionMessage('stdClass vector search index "default" must have a vector field'); @@ -1119,6 +1164,17 @@ public function testTimeSeriesMappingWithGranularityAndBucketMaxSpanSeconds(): v self::assertSame(15, $metadata->timeSeriesOptions->bucketMaxSpanSeconds); self::assertSame(20, $metadata->timeSeriesOptions->bucketRoundingSeconds); } + + public function testNotInjectingTypeRegistryIsDeprecated(): void + { + $cm = new ClassMetadata(UserTyped::class); + $this->captureDeprecationMessages(static function () use ($cm) { + $cm->mapField(['name' => 'dateTimeImmutable']); + }, $errors); + + self::assertSame(['Since doctrine/mongodb-odm 2.16: Using ClassMetadata without a TypeRegistry is deprecated. Inject the TypeRegistry instance from the DocumentManager via $classMetadata->setTypeRegistry($configuration->getTypeRegistry()).'], $errors); + self::assertSame('date_immutable', $cm->getFieldMapping('dateTimeImmutable')['type']); + } } /** @template-extends DocumentRepository */ diff --git a/tests/Tests/Mapping/Driver/AbstractDriverTestCase.php b/tests/Tests/Mapping/Driver/AbstractDriverTestCase.php index 1a84114f9..2d2596614 100644 --- a/tests/Tests/Mapping/Driver/AbstractDriverTestCase.php +++ b/tests/Tests/Mapping/Driver/AbstractDriverTestCase.php @@ -7,6 +7,7 @@ use DateTimeImmutable; use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; use Doctrine\ODM\MongoDB\Mapping\EncryptQuery; +use Doctrine\ODM\MongoDB\Types\TypeRegistry; use Doctrine\Persistence\Mapping\Driver\MappingDriver; use Documents\Account; use Documents\Address; @@ -46,6 +47,7 @@ public function tearDown(): void public function testDriver(): void { $classMetadata = new ClassMetadata(User::class); + $classMetadata->setTypeRegistry(new TypeRegistry()); $this->driver->loadMetadataForClass(User::class, $classMetadata); self::assertEquals([ @@ -260,6 +262,7 @@ public function testDriver(): void ); $classMetadata = new ClassMetadata(EmbeddedDocument::class); + $classMetadata->setTypeRegistry(new TypeRegistry()); $this->driver->loadMetadataForClass(EmbeddedDocument::class, $classMetadata); self::assertEquals([ @@ -416,6 +419,7 @@ public function testCollectionPrimers(): void public function testNullableFieldsMapping(): void { $classMetadata = new ClassMetadata(NullableFieldsDocument::class); + $classMetadata->setTypeRegistry(new TypeRegistry()); $this->driver->loadMetadataForClass(NullableFieldsDocument::class, $classMetadata); self::assertEquals([ @@ -535,6 +539,7 @@ public function testNullableFieldsMapping(): void public function testEncryptFieldMapping(): void { $classMetadata = new ClassMetadata(PatientRecord::class); + $classMetadata->setTypeRegistry(new TypeRegistry()); $this->driver->loadMetadataForClass(PatientRecord::class, $classMetadata); self::assertFalse($classMetadata->isEncrypted); @@ -557,6 +562,7 @@ public function testEncryptFieldMapping(): void public function testEncryptEmbeddedDocumentMapping(): void { $classMetadata = new ClassMetadata(ClientCard::class); + $classMetadata->setTypeRegistry(new TypeRegistry()); $this->driver->loadMetadataForClass(ClientCard::class, $classMetadata); self::assertTrue($classMetadata->isEncrypted); @@ -568,6 +574,7 @@ public function testEncryptEmbeddedDocumentMapping(): void public function testEncryptQueryRangeTypes(): void { $classMetadata = new ClassMetadata(RangeTypes::class); + $classMetadata->setTypeRegistry(new TypeRegistry()); $this->driver->loadMetadataForClass(RangeTypes::class, $classMetadata); self::assertEquals([ diff --git a/tests/Tests/Mapping/Driver/XmlDriverTest.php b/tests/Tests/Mapping/Driver/XmlDriverTest.php index b3961d0c4..b404942d6 100644 --- a/tests/Tests/Mapping/Driver/XmlDriverTest.php +++ b/tests/Tests/Mapping/Driver/XmlDriverTest.php @@ -7,6 +7,7 @@ use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; use Doctrine\ODM\MongoDB\Mapping\Driver\XmlDriver; use Doctrine\ODM\MongoDB\Mapping\MappingException; +use Doctrine\ODM\MongoDB\Types\TypeRegistry; use MongoDB\BSON\Document; use TestDocuments\AlsoLoadDocument; use TestDocuments\CustomIdGenerator; @@ -102,6 +103,7 @@ public function testWildcardIndexName(): void public function testAlsoLoadFieldMapping(): void { $classMetadata = new ClassMetadata(AlsoLoadDocument::class); + $classMetadata->setTypeRegistry(new TypeRegistry()); $this->driver->loadMetadataForClass(AlsoLoadDocument::class, $classMetadata); self::assertEquals([ diff --git a/tests/Tests/Persisters/DocumentPersisterGetShardKeyQueryTest.php b/tests/Tests/Persisters/DocumentPersisterGetShardKeyQueryTest.php index 74f9ab2f9..cb6ae66c7 100644 --- a/tests/Tests/Persisters/DocumentPersisterGetShardKeyQueryTest.php +++ b/tests/Tests/Persisters/DocumentPersisterGetShardKeyQueryTest.php @@ -73,6 +73,24 @@ public function testShardById(): void self::assertSame(['_id' => $o->identifier], $shardKeyQuery); } + public function testGetShardKeyQueryWithCustomFieldName(): void + { + $o = new ShardedByScalarWithCustomFieldName(); + $o->myOid = (string) new ObjectId(); + + $persister = $this->uow->getDocumentPersister($o::class); + + $method = new ReflectionMethod($persister, 'getShardKeyQuery'); + $shardKeyQuery = $method->invoke($persister, $o); + + // The shard key uses DB field name 'my_oid'; value must be converted via ObjectIdType, + // not left as a raw string (which would happen if getFieldType received the DB field + // name instead of the PHP property name). + self::assertArrayHasKey('my_oid', $shardKeyQuery); + self::assertInstanceOf(ObjectId::class, $shardKeyQuery['my_oid']); + self::assertSame($o->myOid, (string) $shardKeyQuery['my_oid']); + } + public function testShardByReference(): void { $o = new ShardedByReferenceOne(); @@ -162,3 +180,14 @@ class ShardedByReferenceOne #[ODM\ReferenceOne(targetDocument: User::class)] public $reference; } + +#[ODM\Document] +#[ODM\ShardKey(keys: ['my_oid' => 'asc'])] +class ShardedByScalarWithCustomFieldName +{ + #[ODM\Id] + public ?string $id; + + #[ODM\Field(name: 'my_oid', type: 'object_id')] + public ?string $myOid; +} diff --git a/tests/Tests/Query/BuilderTest.php b/tests/Tests/Query/BuilderTest.php index 202271b62..1e9fb8369 100644 --- a/tests/Tests/Query/BuilderTest.php +++ b/tests/Tests/Query/BuilderTest.php @@ -22,6 +22,7 @@ use IteratorAggregate; use MongoDB\BSON\ObjectId; use MongoDB\BSON\Regex; +use MongoDB\BSON\UTCDateTime; use MongoDB\Driver\ReadPreference; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\MockObject\MockObject; @@ -425,7 +426,7 @@ public function testSetOnInsert(): void $expected = [ '$setOnInsert' => [ - 'createDate' => Type::getType('date')->convertToDatabaseValue($createDate), + 'createDate' => new UTCDateTime($createDate), ], ]; self::assertEquals($expected, $qb->getNewObj()); @@ -440,8 +441,8 @@ public function testDateRange(): void $expected = [ 'createdAt' => [ - '$gte' => Type::getType('date')->convertToDatabaseValue($start), - '$lt' => Type::getType('date')->convertToDatabaseValue($end), + '$gte' => new UTCDateTime($start), + '$lt' => new UTCDateTime($end), ], ]; self::assertEquals($expected, $qb->getQueryArray()); diff --git a/tests/Tests/Types/BinaryUuidTypeTest.php b/tests/Tests/Types/BinaryUuidTypeTest.php index 407ce66ab..9f2f405c3 100644 --- a/tests/Tests/Types/BinaryUuidTypeTest.php +++ b/tests/Tests/Types/BinaryUuidTypeTest.php @@ -4,7 +4,7 @@ namespace Doctrine\ODM\MongoDB\Tests\Types; -use Doctrine\ODM\MongoDB\Types\Type; +use Doctrine\ODM\MongoDB\Types\BinaryUuidType; use InvalidArgumentException; use MongoDB\BSON\Binary; use PHPUnit\Framework\TestCase; @@ -15,7 +15,7 @@ class BinaryUuidTypeTest extends TestCase { public function testConvertToDatabaseValue(): void { - $type = Type::getType(Type::UUID); + $type = new BinaryUuidType(); $uuid = new UuidV4(); $stringUuid = $uuid->toRfc4122(); $binaryUuid = new Binary($uuid->toBinary(), Binary::TYPE_UUID); @@ -28,7 +28,7 @@ public function testConvertToDatabaseValue(): void public function testConvertInvalidUuid(): void { - $type = Type::getType(Type::UUID); + $type = new BinaryUuidType(); $this->expectException(InvalidArgumentException::class); $type->convertToDatabaseValue('invalid'); @@ -36,7 +36,7 @@ public function testConvertInvalidUuid(): void public function testConvertToPHPValue(): void { - $type = Type::getType(Type::UUID); + $type = new BinaryUuidType(); $uuid = new UuidV4(); $binaryUuid = new Binary($uuid->toBinary(), Binary::TYPE_UUID); @@ -46,7 +46,7 @@ public function testConvertToPHPValue(): void public function testConvertInvalidBinaryUuid(): void { - $type = Type::getType(Type::UUID); + $type = new BinaryUuidType(); $this->expectException(InvalidArgumentException::class); $type->convertToPHPValue(new Binary('invalid', Binary::TYPE_UUID)); @@ -54,7 +54,7 @@ public function testConvertInvalidBinaryUuid(): void public function testConvertInvalidBinary(): void { - $type = Type::getType(Type::UUID); + $type = new BinaryUuidType(); $this->expectException(Throwable::class); $type->convertToPHPValue(new Binary('invalid', Binary::TYPE_GENERIC)); @@ -62,7 +62,7 @@ public function testConvertInvalidBinary(): void public function testClosureToMongo(): void { - $type = Type::getType(Type::UUID); + $type = new BinaryUuidType(); $uuid = new UuidV4(); $stringUuid = $uuid->toRfc4122(); $binaryUuid = new Binary($uuid->toBinary(), Binary::TYPE_UUID); @@ -82,7 +82,7 @@ public function testClosureToMongo(): void public function testClosureToPhp(): void { - $type = Type::getType(Type::UUID); + $type = new BinaryUuidType(); $uuid = new UuidV4(); $binaryUuid = new Binary($uuid->toBinary(), Binary::TYPE_UUID); diff --git a/tests/Tests/Types/DateImmutableTypeTest.php b/tests/Tests/Types/DateImmutableTypeTest.php index 713cfb7ea..8e7a0c823 100644 --- a/tests/Tests/Types/DateImmutableTypeTest.php +++ b/tests/Tests/Types/DateImmutableTypeTest.php @@ -8,14 +8,12 @@ use DateTimeImmutable; use DateTimeZone; use Doctrine\ODM\MongoDB\Types\DateImmutableType; -use Doctrine\ODM\MongoDB\Types\Type; use InvalidArgumentException; use MongoDB\BSON\UTCDateTime; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use stdClass; -use function assert; use function date; use function strtotime; @@ -25,9 +23,7 @@ class DateImmutableTypeTest extends TestCase { public function testGetDateTime(): void { - $type = Type::getType(Type::DATE_IMMUTABLE); - assert($type instanceof DateImmutableType); - + $type = new DateImmutableType(); $timestamp = 100000000.001; $dateTime = $type->getDateTime($timestamp); self::assertEquals($timestamp, $dateTime->format('U.u')); @@ -39,7 +35,7 @@ public function testGetDateTime(): void public function testConvertToDatabaseValue(): void { - $type = Type::getType(Type::DATE_IMMUTABLE); + $type = new DateImmutableType(); self::assertNull($type->convertToDatabaseValue(null), 'null is not converted'); @@ -58,7 +54,7 @@ public function testConvertToDatabaseValue(): void public function testConvertDateTime(): void { - $type = Type::getType(Type::DATE); + $type = new DateImmutableType(); $timestamp = 100000000.123; $mongoDate = new UTCDateTime(100000000123); @@ -69,7 +65,7 @@ public function testConvertDateTime(): void public function testConvertOldDate(): void { - $type = Type::getType(Type::DATE_IMMUTABLE); + $type = new DateImmutableType(); $date = new DateTimeImmutable('1900-01-01 00:00:00.123', new DateTimeZone('UTC')); $timestamp = '-2208988800.123'; @@ -80,7 +76,7 @@ public function testConvertOldDate(): void #[DataProvider('provideInvalidDateValues')] public function testConvertToDatabaseValueWithInvalidValues($value): void { - $type = Type::getType(Type::DATE_IMMUTABLE); + $type = new DateImmutableType(); $this->expectException(InvalidArgumentException::class); $type->convertToDatabaseValue($value); } @@ -100,7 +96,7 @@ public static function provideInvalidDateValues(): array #[DataProvider('provideDatabaseToPHPValues')] public function testConvertToPHPValue($input, DateTimeImmutable $output): void { - $type = Type::getType(Type::DATE_IMMUTABLE); + $type = new DateImmutableType(); $return = $type->convertToPHPValue($input); self::assertInstanceOf('DateTimeImmutable', $return); @@ -109,7 +105,7 @@ public function testConvertToPHPValue($input, DateTimeImmutable $output): void public function testConvertToPHPValueDoesNotConvertNull(): void { - $type = Type::getType(Type::DATE_IMMUTABLE); + $type = new DateImmutableType(); self::assertNull($type->convertToPHPValue(null)); } @@ -118,7 +114,7 @@ public function testConvertToPHPValueDoesNotConvertNull(): void #[DataProvider('provideDatabaseToPHPValues')] public function testClosureToPHP($input, DateTimeImmutable $output): void { - $type = Type::getType(Type::DATE_IMMUTABLE); + $type = new DateImmutableType(); $return = (static function ($value) use ($type) { $return = null; @@ -153,7 +149,7 @@ public function test32bit1900Date(): void $this->markTestSkipped('Platform is not 32-bit'); } - $type = Type::getType(Type::DATE_IMMUTABLE); + $type = new DateImmutableType(); $this->expectException(InvalidArgumentException::class); $type->convertToDatabaseValue('1900-01-01'); } @@ -164,7 +160,7 @@ public function test64bit1900Date(): void $this->markTestSkipped('Platform is not 64-bit'); } - $type = Type::getType(Type::DATE_IMMUTABLE); + $type = new DateImmutableType(); $return = $type->convertToDatabaseValue('1900-01-01'); self::assertInstanceOf(UTCDateTime::class, $return); diff --git a/tests/Tests/Types/DateTypeTest.php b/tests/Tests/Types/DateTypeTest.php index 77ae14775..d713f9519 100644 --- a/tests/Tests/Types/DateTypeTest.php +++ b/tests/Tests/Types/DateTypeTest.php @@ -8,14 +8,12 @@ use DateTimeImmutable; use DateTimeZone; use Doctrine\ODM\MongoDB\Types\DateType; -use Doctrine\ODM\MongoDB\Types\Type; use InvalidArgumentException; use MongoDB\BSON\UTCDateTime; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use stdClass; -use function assert; use function date; use function strtotime; @@ -25,8 +23,7 @@ class DateTypeTest extends TestCase { public function testGetDateTime(): void { - $type = Type::getType(Type::DATE); - assert($type instanceof DateType); + $type = new DateType(); $timestamp = 100000000.001; $dateTime = $type->getDateTime($timestamp); @@ -39,7 +36,7 @@ public function testGetDateTime(): void public function testConvertToDatabaseValue(): void { - $type = Type::getType(Type::DATE); + $type = new DateType(); self::assertNull($type->convertToDatabaseValue(null), 'null is not converted'); @@ -58,7 +55,7 @@ public function testConvertToDatabaseValue(): void public function testConvertDateTimeImmutable(): void { - $type = Type::getType(Type::DATE); + $type = new DateType(); $timestamp = 100000000.123; $mongoDate = new UTCDateTime(100000000123); @@ -69,7 +66,7 @@ public function testConvertDateTimeImmutable(): void public function testConvertOldDate(): void { - $type = Type::getType(Type::DATE); + $type = new DateType(); $date = new DateTime('1900-01-01 00:00:00.123', new DateTimeZone('UTC')); $timestamp = '-2208988800.123'; @@ -80,7 +77,7 @@ public function testConvertOldDate(): void #[DataProvider('provideInvalidDateValues')] public function testConvertToDatabaseValueWithInvalidValues($value): void { - $type = Type::getType(Type::DATE); + $type = new DateType(); $this->expectException(InvalidArgumentException::class); $type->convertToDatabaseValue($value); } @@ -100,7 +97,7 @@ public static function provideInvalidDateValues(): array #[DataProvider('provideDatabaseToPHPValues')] public function testConvertToPHPValue($input, DateTime $output): void { - $type = Type::getType(Type::DATE); + $type = new DateType(); $return = $type->convertToPHPValue($input); self::assertInstanceOf('DateTime', $return); @@ -109,7 +106,7 @@ public function testConvertToPHPValue($input, DateTime $output): void public function testConvertToPHPValueDoesNotConvertNull(): void { - $type = Type::getType(Type::DATE); + $type = new DateType(); self::assertNull($type->convertToPHPValue(null)); } @@ -118,7 +115,7 @@ public function testConvertToPHPValueDoesNotConvertNull(): void #[DataProvider('provideDatabaseToPHPValues')] public function testClosureToPHP($input, DateTime $output): void { - $type = Type::getType(Type::DATE); + $type = new DateType(); $return = (static function ($value) use ($type) { $return = null; @@ -153,7 +150,7 @@ public function test32bit1900Date(): void $this->markTestSkipped('Platform is not 32-bit'); } - $type = Type::getType(Type::DATE); + $type = new DateType(); $this->expectException(InvalidArgumentException::class); $type->convertToDatabaseValue('1900-01-01'); } @@ -164,7 +161,7 @@ public function test64bit1900Date(): void $this->markTestSkipped('Platform is not 64-bit'); } - $type = Type::getType(Type::DATE); + $type = new DateType(); $return = $type->convertToDatabaseValue('1900-01-01'); self::assertInstanceOf(UTCDateTime::class, $return); diff --git a/tests/Tests/Types/IdTypeTest.php b/tests/Tests/Types/IdTypeTest.php index ec050bb0f..61626f7d8 100644 --- a/tests/Tests/Types/IdTypeTest.php +++ b/tests/Tests/Types/IdTypeTest.php @@ -4,7 +4,7 @@ namespace Doctrine\ODM\MongoDB\Tests\Types; -use Doctrine\ODM\MongoDB\Types\Type; +use Doctrine\ODM\MongoDB\Types\IdType; use MongoDB\BSON\ObjectId; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; @@ -14,7 +14,7 @@ class IdTypeTest extends TestCase public function testConvertToDatabaseValue(): void { $identifier = new ObjectId(); - $type = Type::getType('id'); + $type = new IdType(); self::assertNull($type->convertToDatabaseValue(null), 'null is not converted'); self::assertSame($identifier, $type->convertToDatabaseValue($identifier), 'ObjectId objects are not converted'); @@ -25,7 +25,7 @@ public function testConvertToDatabaseValue(): void #[DataProvider('provideInvalidObjectIdConstructorArguments')] public function testConvertToDatabaseValueShouldGenerateObjectIds($value): void { - $type = Type::getType('id'); + $type = new IdType(); self::assertInstanceOf(ObjectId::class, $type->convertToDatabaseValue($value)); } diff --git a/tests/Tests/Types/InvalidValueExceptionTest.php b/tests/Tests/Types/InvalidValueExceptionTest.php index 1f1be6f8f..32a316775 100644 --- a/tests/Tests/Types/InvalidValueExceptionTest.php +++ b/tests/Tests/Types/InvalidValueExceptionTest.php @@ -6,44 +6,45 @@ use Doctrine\Common\Collections\ArrayCollection; use Doctrine\ODM\MongoDB\MongoDBException; -use Doctrine\ODM\MongoDB\Types\Type; +use Doctrine\ODM\MongoDB\Types\CollectionType; +use Doctrine\ODM\MongoDB\Types\HashType; use PHPUnit\Framework\TestCase; class InvalidValueExceptionTest extends TestCase { public function testCollectionDoesntAcceptObject(): void { - $t = Type::getType('collection'); + $type = new CollectionType(); $this->expectException(MongoDBException::class); $this->expectExceptionMessage( 'Collection type requires value of type array or null, Doctrine\Common\Collections\ArrayCollection given', ); - $t->convertToDatabaseValue(new ArrayCollection()); + $type->convertToDatabaseValue(new ArrayCollection()); } public function testCollectionDoesntAcceptScalar(): void { - $t = Type::getType('collection'); + $type = new CollectionType(); $this->expectException(MongoDBException::class); $this->expectExceptionMessage('Collection type requires value of type array or null, scalar given'); - $t->convertToDatabaseValue(true); + $type->convertToDatabaseValue(true); } public function testHashDoesntAcceptObject(): void { - $t = Type::getType('hash'); + $type = new HashType(); $this->expectException(MongoDBException::class); $this->expectExceptionMessage( 'Hash type requires value of type array or null, Doctrine\Common\Collections\ArrayCollection given', ); - $t->convertToDatabaseValue(new ArrayCollection()); + $type->convertToDatabaseValue(new ArrayCollection()); } public function testHashDoesntAcceptScalar(): void { - $t = Type::getType('hash'); + $type = new HashType(); $this->expectException(MongoDBException::class); $this->expectExceptionMessage('Hash type requires value of type array or null, scalar given'); - $t->convertToDatabaseValue(true); + $type->convertToDatabaseValue(true); } } diff --git a/tests/Tests/Types/TypeRegistryTest.php b/tests/Tests/Types/TypeRegistryTest.php new file mode 100644 index 000000000..db28dd9b1 --- /dev/null +++ b/tests/Tests/Types/TypeRegistryTest.php @@ -0,0 +1,156 @@ +guessTypeFromValue($variable); + + if ($expectedType === null) { + self::assertNull($type); + } elseif ($type === null) { + self::fail(sprintf('Type is null, expected "%s"', $expectedType)); + } else { + self::assertSame($registry->get($expectedType), $type); + } + } + + public static function provideTypeToGuessFromValue(): Generator + { + yield 'null' => [null, null]; + yield 'bool' => [Type::BOOL, true]; + yield 'int' => [Type::INT, 1]; + yield 'float' => [Type::FLOAT, 3.14]; + yield 'string' => [Type::STRING, 'ohai']; + yield 'DateTime' => [Type::DATE, new DateTime()]; + yield 'DateTimeImmutable' => [Type::DATE_IMMUTABLE, new DateTimeImmutable()]; + yield 'unknown object' => [ + null, + new class () { + }, + ]; + } + + public function testInvalidType(): void + { + $registry = new TypeRegistry(); + + self::expectException(InvalidTypeException::class); + self::expectExceptionMessage('Invalid type specified: "foo"'); + + $registry->get('foo'); + } + + public function testRegister(): void + { + $registry = new TypeRegistry(); + self::assertFalse($registry->has('my_custom_type')); + + $registry->register('my_custom_type', IntType::class); + self::assertTrue($registry->has('my_custom_type')); + self::assertInstanceOf(IntType::class, $registry->get('my_custom_type')); + self::assertSame($registry->get('my_custom_type'), $registry->get('my_custom_type')); + + $registry->register('my_custom_type', RawType::class); + self::assertInstanceOf(RawType::class, $registry->get('my_custom_type')); + + $map = $registry->getMap(); + self::assertSame(RawType::class, $map['my_custom_type']); + } + + public function testRegisterTypeInstance(): void + { + $registry = new TypeRegistry(); + self::assertFalse($registry->has('my_custom_type')); + + $typeInstance = new IntType(); + $registry->register('my_custom_type', $typeInstance); + self::assertTrue($registry->has('my_custom_type')); + self::assertSame($typeInstance, $registry->get('my_custom_type')); + + $map = $registry->getMap(); + self::assertSame(IntType::class, $map['my_custom_type']); + + // Replace it with a type by class name unsets the instance + $registry->register('my_custom_type', FloatType::class); + self::assertInstanceOf(FloatType::class, $registry->get('my_custom_type')); + + $map = $registry->getMap(); + self::assertSame(FloatType::class, $map['my_custom_type']); + } + + public function testConvertToDatabaseValue(): void + { + $registry = new TypeRegistry(); + + self::assertSame(42, $registry->convertToDatabaseValue(42)); + self::assertInstanceOf(UTCDateTime::class, $registry->convertToDatabaseValue(new DateTime())); + + // Not found + $object = new class () { + }; + self::assertSame($object, $registry->convertToDatabaseValue($object)); + } + + public function testSharedInstance(): void + { + self::assertSame(TypeRegistry::getSharedInstance(), TypeRegistry::getSharedInstance()); + } + + public function testRegisterRequiresATypeClassOrInstance(): void + { + $registry = new TypeRegistry(); + self::expectException(InvalidArgumentException::class); + // @phpstan-ignore argument.type + $registry->register('invalid_type', stdClass::class); + } + + public function testRegisterRejectsClassWithRequiredConstructorParameters(): void + { + $registry = new TypeRegistry(); + self::expectException(InvalidArgumentException::class); + self::expectExceptionMessage('must not have a constructor with required parameters'); + $registry->register('my_type', TypeWithRequiredConstructor::class); + } + + public function testRegisterAllowsClassWithInheritedNoArgConstructor(): void + { + $registry = new TypeRegistry(); + $registry->register('my_type', TypeWithInheritedConstructor::class); + self::assertInstanceOf(TypeWithInheritedConstructor::class, $registry->get('my_type')); + } +} + +class TypeWithRequiredConstructor extends IntType +{ + public function __construct(private string $required) + { + } +} + +class TypeWithInheritedConstructor extends IntType +{ +} diff --git a/tests/Tests/Types/TypeTest.php b/tests/Tests/Types/TypeTest.php index e9103aa78..5ca16d6af 100644 --- a/tests/Tests/Types/TypeTest.php +++ b/tests/Tests/Types/TypeTest.php @@ -6,9 +6,17 @@ use DateTime; use DateTimeImmutable; +use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; +use Doctrine\ODM\MongoDB\Mapping\MappingException; use Doctrine\ODM\MongoDB\Tests\BaseTestCase; +use Doctrine\ODM\MongoDB\Tests\CaptureDeprecationMessages; +use Doctrine\ODM\MongoDB\Types\FloatType; +use Doctrine\ODM\MongoDB\Types\IntType; use Doctrine\ODM\MongoDB\Types\InvalidTypeException; +use Doctrine\ODM\MongoDB\Types\StringType; use Doctrine\ODM\MongoDB\Types\Type; +use Doctrine\ODM\MongoDB\Types\TypeRegistry; +use Generator; use MongoDB\BSON\Binary; use MongoDB\BSON\Decimal128; use MongoDB\BSON\Int64; @@ -32,11 +40,26 @@ class TypeTest extends BaseTestCase { + use CaptureDeprecationMessages; + + private TypeRegistry $registry; + + private ClassMetadata $class; + + public function setUp(): void + { + parent::setUp(); + + $this->registry = new TypeRegistry(); + $this->class = new ClassMetadata(TypeTestDocument::class); + $this->class->setTypeRegistry($this->registry); + } + #[DataProvider('provideTypes')] public function testConversion(string $typeName, mixed $phpValue, mixed $bsonValue = null): void { $bsonValue ??= $phpValue; - $type = Type::getType($typeName); + $type = $this->registry->get($typeName); self::assertSameTypeAndValue($phpValue, $type->convertToPHPValue($bsonValue)); self::assertSameTypeAndValue($bsonValue, $type->convertToDatabaseValue($phpValue)); @@ -45,70 +68,72 @@ public function testConversion(string $typeName, mixed $phpValue, mixed $bsonVal #[DataProvider('provideTypes')] public function testConversionWithClosureToPHP(string $typeIdentifier, mixed $expectedValue, mixed $value = null): void { - $value ??= $expectedValue; - $return = $this; + $this->class->mapField([ + 'fieldName' => 'theField', + 'type' => $typeIdentifier, + ]); + + $value ??= $expectedValue; + $fieldName = 'theField'; + $return = $this; eval(Type::getType($typeIdentifier)->closureToPHP()); self::assertSameTypeAndValue($expectedValue, $return); } - public static function provideTypes(): array + public static function provideTypes(): Generator { - return [ - 'id' => [Type::ID, '507f1f77bcf86cd799439011', new ObjectId('507f1f77bcf86cd799439011')], - 'intId' => [Type::INTID, 1], - 'customId' => [Type::CUSTOMID, (object) ['foo' => 'bar']], - 'bool' => [Type::BOOL, true], - 'boolean' => [Type::BOOLEAN, false], - 'int' => [Type::INT, 69], - 'integer' => [Type::INTEGER, 42], - 'int64' => [Type::INT64, 100, new Int64(100)], - 'float' => [Type::FLOAT, 3.14], - 'string' => [Type::STRING, 'ohai'], - 'minKey' => [Type::KEY, 0, new MinKey()], - 'maxKey' => [Type::KEY, 1, new MaxKey()], - 'timestamp' => [Type::TIMESTAMP, $t = time(), new Timestamp(0, $t)], - 'binData' => [Type::BINDATA, 'foobarbaz', new Binary('foobarbaz', Binary::TYPE_GENERIC)], - 'binDataFunc' => [Type::BINDATAFUNC, 'foobarbaz', new Binary('foobarbaz', Binary::TYPE_FUNCTION)], - 'binDataByteArray' => [Type::BINDATABYTEARRAY, 'foobarbaz', new Binary('foobarbaz', Binary::TYPE_OLD_BINARY)], - 'binDataUuid' => [Type::BINDATAUUID, 'testtesttesttest', new Binary('testtesttesttest', Binary::TYPE_OLD_UUID)], - 'binDataUuidRFC4122' => [Type::BINDATAUUIDRFC4122, str_repeat('a', 16), new Binary(str_repeat('a', 16), Binary::TYPE_UUID)], - 'binDataMD5' => [Type::BINDATAMD5, md5('ODM'), new Binary(md5('ODM'), Binary::TYPE_MD5)], - 'binDataCustom' => [Type::BINDATACUSTOM, 'foobarbaz', new Binary('foobarbaz', Binary::TYPE_USER_DEFINED)], - 'hash' => [Type::HASH, ['foo' => 'bar'], (object) ['foo' => 'bar']], - 'collection' => [Type::COLLECTION, ['foo', 'bar']], - 'objectId' => [Type::OBJECTID, '507f1f77bcf86cd799439011', new ObjectId('507f1f77bcf86cd799439011')], - 'raw' => [Type::RAW, (object) ['foo' => 'bar']], - 'decimal128' => [Type::DECIMAL128, '4.20', new Decimal128('4.20')], - 'uuid' => [Type::UUID, new UuidV4('550e8400-e29b-41d4-a716-446655440000'), new Binary(hex2bin('550e8400e29b41d4a716446655440000'), Binary::TYPE_UUID)], - ]; + yield 'id' => [Type::ID, '507f1f77bcf86cd799439011', new ObjectId('507f1f77bcf86cd799439011')]; + yield 'intId' => [Type::INTID, 1]; + yield 'customId' => [Type::CUSTOMID, (object) ['foo' => 'bar']]; + yield 'bool' => [Type::BOOL, true]; + yield 'boolean' => [Type::BOOLEAN, false]; + yield 'int' => [Type::INT, 69]; + yield 'integer' => [Type::INTEGER, 42]; + yield 'int64' => [Type::INT64, 100, new Int64(100)]; + yield 'float' => [Type::FLOAT, 3.14]; + yield 'string' => [Type::STRING, 'ohai']; + yield 'minKey' => [Type::KEY, 0, new MinKey()]; + yield 'maxKey' => [Type::KEY, 1, new MaxKey()]; + yield 'timestamp' => [Type::TIMESTAMP, $t = time(), new Timestamp(0, $t)]; + yield 'binData' => [Type::BINDATA, 'foobarbaz', new Binary('foobarbaz', Binary::TYPE_GENERIC)]; + yield 'binDataFunc' => [Type::BINDATAFUNC, 'foobarbaz', new Binary('foobarbaz', Binary::TYPE_FUNCTION)]; + yield 'binDataByteArray' => [Type::BINDATABYTEARRAY, 'foobarbaz', new Binary('foobarbaz', Binary::TYPE_OLD_BINARY)]; + yield 'binDataUuid' => [Type::BINDATAUUID, 'testtesttesttest', new Binary('testtesttesttest', Binary::TYPE_OLD_UUID)]; + yield 'binDataUuidRFC4122' => [Type::BINDATAUUIDRFC4122, str_repeat('a', 16), new Binary(str_repeat('a', 16), Binary::TYPE_UUID)]; + yield 'binDataMD5' => [Type::BINDATAMD5, md5('ODM'), new Binary(md5('ODM'), Binary::TYPE_MD5)]; + yield 'binDataCustom' => [Type::BINDATACUSTOM, 'foobarbaz', new Binary('foobarbaz', Binary::TYPE_USER_DEFINED)]; + yield 'hash' => [Type::HASH, ['foo' => 'bar'], (object) ['foo' => 'bar']]; + yield 'collection' => [Type::COLLECTION, ['foo', 'bar']]; + yield 'objectId' => [Type::OBJECTID, '507f1f77bcf86cd799439011', new ObjectId('507f1f77bcf86cd799439011')]; + yield 'raw' => [Type::RAW, (object) ['foo' => 'bar']]; + yield 'decimal128' => [Type::DECIMAL128, '4.20', new Decimal128('4.20')]; + yield 'uuid' => [Type::UUID, new UuidV4('550e8400-e29b-41d4-a716-446655440000'), new Binary(hex2bin('550e8400e29b41d4a716446655440000'), Binary::TYPE_UUID)]; } /** @param mixed $test */ #[DataProvider('provideTypesForIdempotent')] - public function testConversionIsIdempotent(Type $type, $test): void + public function testConversionIsIdempotent(string $type, $test): void { - self::assertSameTypeAndValue($test, $type->convertToDatabaseValue($test)); + self::assertSameTypeAndValue($test, $this->registry->get($type)->convertToDatabaseValue($test)); } - public static function provideTypesForIdempotent(): array + public static function provideTypesForIdempotent(): Generator { - return [ - 'id' => [Type::getType(Type::ID), new ObjectId()], - 'date' => [Type::getType(Type::DATE), new UTCDateTime()], - 'dateImmutable' => [Type::getType(Type::DATE_IMMUTABLE), new UTCDateTime()], - 'int64' => [Type::getType(Type::INT64), new Int64(100)], - 'timestamp' => [Type::getType(Type::TIMESTAMP), new Timestamp(0, time())], - 'binData' => [Type::getType(Type::BINDATA), new Binary('foobarbaz', Binary::TYPE_GENERIC)], - 'binDataFunc' => [Type::getType(Type::BINDATAFUNC), new Binary('foobarbaz', Binary::TYPE_FUNCTION)], - 'binDataByteArray' => [Type::getType(Type::BINDATABYTEARRAY), new Binary('foobarbaz', Binary::TYPE_OLD_BINARY)], - 'binDataUuid' => [Type::getType(Type::BINDATAUUID), new Binary('testtesttesttest', Binary::TYPE_OLD_UUID)], - 'binDataUuidRFC4122' => [Type::getType(Type::BINDATAUUIDRFC4122), new Binary(str_repeat('a', 16), Binary::TYPE_UUID)], - 'binDataMD5' => [Type::getType(Type::BINDATAMD5), new Binary(md5('ODM'), Binary::TYPE_MD5)], - 'binDataCustom' => [Type::getType(Type::BINDATACUSTOM), new Binary('foobarbaz', Binary::TYPE_USER_DEFINED)], - 'objectId' => [Type::getType(Type::OBJECTID), new ObjectId()], - 'decimal128' => [Type::getType(Type::DECIMAL128), new Decimal128('4.20')], - ]; + yield 'id' => [Type::ID, new ObjectId()]; + yield 'date' => [Type::DATE, new UTCDateTime()]; + yield 'dateImmutable' => [Type::DATE_IMMUTABLE, new UTCDateTime()]; + yield 'int64' => [Type::INT64, new Int64(100)]; + yield 'timestamp' => [Type::TIMESTAMP, new Timestamp(0, time())]; + yield 'binData' => [Type::BINDATA, new Binary('foobarbaz', Binary::TYPE_GENERIC)]; + yield 'binDataFunc' => [Type::BINDATAFUNC, new Binary('foobarbaz', Binary::TYPE_FUNCTION)]; + yield 'binDataByteArray' => [Type::BINDATABYTEARRAY, new Binary('foobarbaz', Binary::TYPE_OLD_BINARY)]; + yield 'binDataUuid' => [Type::BINDATAUUID, new Binary('testtesttesttest', Binary::TYPE_OLD_UUID)]; + yield 'binDataUuidRFC4122' => [Type::BINDATAUUIDRFC4122, new Binary(str_repeat('a', 16), Binary::TYPE_UUID)]; + yield 'binDataMD5' => [Type::BINDATAMD5, new Binary(md5('ODM'), Binary::TYPE_MD5)]; + yield 'binDataCustom' => [Type::BINDATACUSTOM, new Binary('foobarbaz', Binary::TYPE_USER_DEFINED)]; + yield 'objectId' => [Type::OBJECTID, new ObjectId()]; + yield 'decimal128' => [Type::DECIMAL128, new Decimal128('4.20')]; } public function testConvertDatePreservesMilliseconds(): void @@ -119,7 +144,8 @@ public function testConvertDatePreservesMilliseconds(): void $cleanMicroseconds = (int) $date->format('v') * 1000; $expectedDate->modify($date->format('H:i:s') . '.' . str_pad((string) $cleanMicroseconds, 6, '0', STR_PAD_LEFT)); - $type = Type::getType(Type::DATE); + $registry = new TypeRegistry(); + $type = $registry->get(Type::DATE); self::assertEquals($expectedDate, $type->convertToPHPValue($type->convertToDatabaseValue($date))); } @@ -130,7 +156,7 @@ public function testConvertDateImmutablePreservesMilliseconds(): void $cleanMicroseconds = (int) $date->format('v') * 1000; $expectedDate = $date->modify($date->format('H:i:s') . '.' . str_pad((string) $cleanMicroseconds, 6, '0', STR_PAD_LEFT)); - $type = Type::getType(Type::DATE_IMMUTABLE); + $type = $this->registry->get(Type::DATE_IMMUTABLE); self::assertEquals($expectedDate, $type->convertToPHPValue($type->convertToDatabaseValue($date))); } @@ -142,34 +168,37 @@ public function testConvertImmutableDate(): void } #[DataProvider('provideTypeFromPHPVariable')] - public function testGetTypeFromPHPVariable(?Type $expectedType, mixed $variable): void + public function testGetTypeFromPHPVariable(?string $expectedType, mixed $variable): void { - $type = Type::getTypeFromPHPVariable($variable); + $type = $this->captureDeprecationMessages(static function () use ($variable): ?Type { + return Type::getTypeFromPHPVariable($variable); + }, $errors); if ($expectedType === null) { self::assertNull($type); } elseif ($type === null) { - self::fail(sprintf('Type is null, expected "%s"', $expectedType::class)); + self::fail(sprintf('Type is null, expected "%s"', $expectedType)); } else { + $expectedType = $this->registry->get($expectedType); self::assertInstanceOf($expectedType::class, $type, $type::class); } + + self::assertSame(['Since doctrine/mongodb-odm 2.16: Type::getTypeFromPHPVariable() is deprecated without replacement.'], $errors); } - public static function provideTypeFromPHPVariable(): array + public static function provideTypeFromPHPVariable(): Generator { - return [ - 'null' => [null, null], - 'bool' => [Type::getType(Type::BOOL), true], - 'int' => [Type::getType(Type::INT), 1], - 'float' => [Type::getType(Type::FLOAT), 3.14], - 'string' => [Type::getType(Type::STRING), 'ohai'], - 'DateTime' => [Type::getType(Type::DATE), new DateTime()], - 'DateTimeImmutable' => [Type::getType(Type::DATE_IMMUTABLE), new DateTimeImmutable()], - 'unknown object' => [ - null, - new class () { - }, - ], + yield 'null' => [null, null]; + yield 'bool' => [Type::BOOL, true]; + yield 'int' => [Type::INT, 1]; + yield 'float' => [Type::FLOAT, 3.14]; + yield 'string' => [Type::STRING, 'ohai']; + yield 'DateTime' => [Type::DATE, new DateTime()]; + yield 'DateTimeImmutable' => [Type::DATE_IMMUTABLE, new DateTimeImmutable()]; + yield 'unknown object' => [ + null, + new class () { + }, ]; } @@ -178,7 +207,48 @@ public function testInvalidType(): void self::expectException(InvalidTypeException::class); self::expectExceptionMessage('Invalid type specified: "foo"'); - Type::getType('foo'); + $this->registry->get('foo'); + } + + public function testDeprecatedMethods(): void + { + $this->captureDeprecationMessages(static function () { + self::assertTrue(Type::hasType(Type::STRING)); + self::assertFalse(Type::hasType('non_existent_type')); + self::assertInstanceOf(StringType::class, Type::getType(Type::STRING)); + self::assertInstanceOf(IntType::class, Type::getTypeFromPHPVariable(10)); + self::assertInstanceOf(UTCDateTime::class, Type::convertPHPToDatabaseValue(new DateTime())); + Type::addType('custom_type', StringType::class); + self::assertInstanceOf(StringType::class, Type::getType('custom_type')); + Type::registerType('custom_type', FloatType::class); + self::assertInstanceOf(FloatType::class, Type::getType('custom_type')); + Type::overrideType('custom_type', IntType::class); + self::assertInstanceOf(IntType::class, Type::getType('custom_type')); + self::assertIsArray(Type::getTypesMap()); + + try { + Type::overrideType('non_existent_type', IntType::class); + self::fail('Expected exception not thrown.'); + } catch (MappingException $e) { + self::assertEquals(MappingException::typeNotFound('non_existent_type'), $e); + } + }, $errors); + + self::assertSame([ + 'Since doctrine/mongodb-odm 2.16: Type::hasType() is deprecated, use TypeRegistry::has() instead.', + 'Since doctrine/mongodb-odm 2.16: Type::hasType() is deprecated, use TypeRegistry::has() instead.', + 'Since doctrine/mongodb-odm 2.16: Type::getType() is deprecated, use TypeRegistry::get() instead.', + 'Since doctrine/mongodb-odm 2.16: Type::getTypeFromPHPVariable() is deprecated without replacement.', + 'Since doctrine/mongodb-odm 2.16: Type::convertPHPToDatabaseValue() is deprecated, use TypeRegistry::convertToDatabaseValue() instead.', + 'Since doctrine/mongodb-odm 2.16: Type::addType() is deprecated, use TypeRegistry::register() instead.', + 'Since doctrine/mongodb-odm 2.16: Type::getType() is deprecated, use TypeRegistry::get() instead.', + 'Since doctrine/mongodb-odm 2.16: Type::registerType() is deprecated, use TypeRegistry::register() instead.', + 'Since doctrine/mongodb-odm 2.16: Type::getType() is deprecated, use TypeRegistry::get() instead.', + 'Since doctrine/mongodb-odm 2.16: Type::overrideType() is deprecated, use TypeRegistry::register() instead.', + 'Since doctrine/mongodb-odm 2.16: Type::getType() is deprecated, use TypeRegistry::get() instead.', + 'Since doctrine/mongodb-odm 2.16: Type::getTypesMap() is deprecated and will be removed in 3.0. Use TypeRegistry methods instead.', + 'Since doctrine/mongodb-odm 2.16: Type::overrideType() is deprecated, use TypeRegistry::register() instead.', + ], $errors); } private static function assertSameTypeAndValue(mixed $expected, mixed $actual): void @@ -187,3 +257,13 @@ private static function assertSameTypeAndValue(mixed $expected, mixed $actual): self::assertEquals($expected, $actual); } } + + +class TypeTestDocument +{ + /** @var string */ + public $id; + + /** @var mixed */ + public $theField; +} diff --git a/tests/Tests/Types/VectorTypeTest.php b/tests/Tests/Types/VectorTypeTest.php index 23ab1a7e9..06e180db9 100644 --- a/tests/Tests/Types/VectorTypeTest.php +++ b/tests/Tests/Types/VectorTypeTest.php @@ -4,7 +4,9 @@ namespace Doctrine\ODM\MongoDB\Tests\Types; +use Doctrine\ODM\MongoDB\Types\AbstractVectorType; use Doctrine\ODM\MongoDB\Types\Type; +use Doctrine\ODM\MongoDB\Types\TypeRegistry; use InvalidArgumentException; use MongoDB\BSON\Binary; use MongoDB\BSON\VectorType; @@ -17,17 +19,25 @@ #[RequiresPhpExtension('mongodb', '>= 2.2')] class VectorTypeTest extends TestCase { + public function getType(string $typeName): AbstractVectorType + { + $type = (new TypeRegistry())->get($typeName); + self::assertInstanceOf(AbstractVectorType::class, $type); + + return $type; + } + #[DataProvider('providePhpVectors')] public function testConvertToDatabaseValue(string $name, mixed $value, mixed $expectedValue): void { - $this->assertSameTypeAndValue($expectedValue, Type::getType($name)->convertToDatabaseValue($value)); + $this->assertSameTypeAndValue($expectedValue, $this->getType($name)->convertToDatabaseValue($value)); } #[DataProvider('providePhpVectors')] public function testClosureToDatabase(string $name, mixed $value, mixed $expectedValue): void { $return = $this; - eval(Type::getType($name)->closureToMongo()); + eval($this->getType($name)->closureToMongo()); $this->assertSameTypeAndValue($expectedValue, $return); } @@ -60,14 +70,14 @@ public static function providePhpVectors(): iterable #[DataProvider('provideDatabaseVectors')] public function testConvertToPHPValue(string $name, mixed $value, mixed $expectedValue): void { - $this->assertEquals($expectedValue, Type::getType($name)->convertToPHPValue($value)); + $this->assertEquals($expectedValue, $this->getType($name)->convertToPHPValue($value)); } #[DataProvider('provideDatabaseVectors')] public function testClosureToPHP(string $name, mixed $value, mixed $expectedValue): void { $return = $this; - eval(Type::getType($name)->closureToPHP()); + eval($this->getType($name)->closureToPHP()); $this->assertEquals($expectedValue, $return); } @@ -100,7 +110,7 @@ public static function provideDatabaseVectors(): iterable #[DataProvider('provideDatabaseValueException')] public function testConvertToPHPValueException(mixed $value, string $message): void { - $type = Type::getType(Type::VECTOR_FLOAT32); + $type = $this->getType(Type::VECTOR_FLOAT32); $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage($message); @@ -110,7 +120,7 @@ public function testConvertToPHPValueException(mixed $value, string $message): v #[DataProvider('provideDatabaseValueException')] public function testClosureToPHPValueException(mixed $value, string $message): void { - $type = Type::getType(Type::VECTOR_FLOAT32); + $type = $this->getType(Type::VECTOR_FLOAT32); $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage($message); @@ -128,7 +138,7 @@ public static function provideDatabaseValueException(): iterable #[DataProvider('providePHPValueException')] public function testConvertToDatabaseValueException(mixed $value, string $message): void { - $type = Type::getType(Type::VECTOR_FLOAT32); + $type = $this->getType(Type::VECTOR_FLOAT32); $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage($message); @@ -138,7 +148,7 @@ public function testConvertToDatabaseValueException(mixed $value, string $messag #[DataProvider('providePHPValueException')] public function testClosureToDatabaseException(mixed $value, string $message): void { - $type = Type::getType(Type::VECTOR_FLOAT32); + $type = $this->getType(Type::VECTOR_FLOAT32); $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage($message); diff --git a/tests/Tests/Types/VersionableTest.php b/tests/Tests/Types/VersionableTest.php index ba4876509..098cb737a 100644 --- a/tests/Tests/Types/VersionableTest.php +++ b/tests/Tests/Types/VersionableTest.php @@ -8,6 +8,7 @@ use DateTimeImmutable; use Doctrine\ODM\MongoDB\Tests\BaseTestCase; use Doctrine\ODM\MongoDB\Types\Type; +use Doctrine\ODM\MongoDB\Types\TypeRegistry; use Doctrine\ODM\MongoDB\Types\Versionable; use MongoDB\BSON\ObjectId; @@ -74,7 +75,7 @@ public function testObjectIdNextVersion(): void private function getType(string $name): Versionable { - $type = Type::getType($name); + $type = (new TypeRegistry())->get($name); self::assertInstanceOf(Versionable::class, $type);