From 19dfc9c187b74b2f560d1b701c3f308624b1a05b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Fri, 28 Feb 2025 03:24:13 +0100 Subject: [PATCH] Hydrate models from raw BSON --- .../ODM/MongoDB/Aggregation/Builder.php | 4 ++ lib/Doctrine/ODM/MongoDB/DocumentManager.php | 8 +++ .../ODM/MongoDB/Event/PreLoadEventArgs.php | 33 ++++++++++- .../ODM/MongoDB/Hydrator/HydratorFactory.php | 55 +++++++++++-------- .../MongoDB/Hydrator/HydratorInterface.php | 6 +- .../MongoDB/Iterator/HydratingIterator.php | 8 +-- .../ODM/MongoDB/Mapping/ClassMetadata.php | 7 +++ .../PersistentCollectionInterface.php | 8 +-- .../PersistentCollectionTrait.php | 14 ++--- .../MongoDB/Persisters/DocumentPersister.php | 35 ++++++++---- lib/Doctrine/ODM/MongoDB/Query/Builder.php | 4 ++ lib/Doctrine/ODM/MongoDB/Query/Query.php | 4 +- lib/Doctrine/ODM/MongoDB/Types/HashType.php | 7 +++ lib/Doctrine/ODM/MongoDB/UnitOfWork.php | 16 +++--- .../Tests/Events/PreLoadEventArgsTest.php | 6 +- .../Iterator/HydratingIteratorTest.php | 7 ++- .../Tests/Functional/Ticket/GH1418Test.php | 5 +- .../Tests/Functional/Ticket/GH852Test.php | 1 + .../ODM/MongoDB/Tests/HydratorTest.php | 52 +++++++++--------- .../Tests/PersistentCollectionTest.php | 5 +- 20 files changed, 184 insertions(+), 101 deletions(-) diff --git a/lib/Doctrine/ODM/MongoDB/Aggregation/Builder.php b/lib/Doctrine/ODM/MongoDB/Aggregation/Builder.php index cbc62a3b8f..6034b5c6db 100644 --- a/lib/Doctrine/ODM/MongoDB/Aggregation/Builder.php +++ b/lib/Doctrine/ODM/MongoDB/Aggregation/Builder.php @@ -254,6 +254,10 @@ public function getAggregation(array $options = []): IterableResult { $class = $this->hydrationClass ? $this->dm->getClassMetadata($this->hydrationClass) : null; + if ($this->hydrationClass) { + $options['typeMap'] = DocumentManager::HYDRATION_TYPEMAP; + } + return new Aggregation($this->dm, $class, $this->collection, $this->getPipeline(), $options, $this->rewindable); } diff --git a/lib/Doctrine/ODM/MongoDB/DocumentManager.php b/lib/Doctrine/ODM/MongoDB/DocumentManager.php index b43cf9ecdd..8ff5d589aa 100644 --- a/lib/Doctrine/ODM/MongoDB/DocumentManager.php +++ b/lib/Doctrine/ODM/MongoDB/DocumentManager.php @@ -57,8 +57,16 @@ */ class DocumentManager implements ObjectManager { + /** + * TypeMap by default + */ public const CLIENT_TYPEMAP = ['root' => 'array', 'document' => 'array']; + /** + * TypeMap when result is hydrated + */ + public const HYDRATION_TYPEMAP = ['root' => 'bson', 'document' => 'bson', 'array' => 'bson']; + /** * The Doctrine MongoDB connection instance. */ diff --git a/lib/Doctrine/ODM/MongoDB/Event/PreLoadEventArgs.php b/lib/Doctrine/ODM/MongoDB/Event/PreLoadEventArgs.php index a30f705f29..bb52eed10f 100644 --- a/lib/Doctrine/ODM/MongoDB/Event/PreLoadEventArgs.php +++ b/lib/Doctrine/ODM/MongoDB/Event/PreLoadEventArgs.php @@ -5,6 +5,7 @@ namespace Doctrine\ODM\MongoDB\Event; use Doctrine\ODM\MongoDB\DocumentManager; +use MongoDB\BSON\Document; use MongoDB\Driver\Session; /** @@ -12,23 +13,49 @@ */ final class PreLoadEventArgs extends LifecycleEventArgs { - /** @param array $data */ + private array $deprecatedDataArray; + public function __construct( object $document, DocumentManager $dm, - private array &$data, + private Document &$data, ?Session $session = null, ) { parent::__construct($document, $dm, $session); } + public function getRawData(): Document + { + if (isset($this->deprecatedDataArray)) { + $this->data = Document::fromPHP($this->deprecatedDataArray); + unset($this->deprecatedDataArray); + } + + return $this->data; + } + + public function setRawData(Document $document): void + { + $this->data = $document; + unset($this->deprecatedDataArray); + } + /** * Get the array of data to be loaded and hydrated. * + * @deprecated Use {@see self::getBsonDocument()} and {@see self::setBsonDocument()} + * * @return array */ public function &getData(): array { - return $this->data; + $this->deprecatedDataArray ??= $this->data->toPHP(['root' => 'array']); + + return $this->deprecatedDataArray; + } + + public function __destruct() + { + $this->getRawData(); } } diff --git a/lib/Doctrine/ODM/MongoDB/Hydrator/HydratorFactory.php b/lib/Doctrine/ODM/MongoDB/Hydrator/HydratorFactory.php index 1e8135b6aa..d3a5565c3c 100644 --- a/lib/Doctrine/ODM/MongoDB/Hydrator/HydratorFactory.php +++ b/lib/Doctrine/ODM/MongoDB/Hydrator/HydratorFactory.php @@ -14,9 +14,9 @@ use Doctrine\ODM\MongoDB\Proxy\InternalProxy; use Doctrine\ODM\MongoDB\Types\Type; use Doctrine\ODM\MongoDB\UnitOfWork; +use MongoDB\BSON\Document; use ProxyManager\Proxy\GhostObjectInterface; -use function array_key_exists; use function chmod; use function class_exists; use function dirname; @@ -169,8 +169,11 @@ private function generateHydratorClass(ClassMetadata $class, string $hydratorCla <<has('%1\$s') && \$data->has('$name')) { + // @todo extracting and repacking is not very efficient + \$data = \$data->toPHP(['root' => 'array', 'document' => 'bson', 'array' => 'bson']); \$data['%1\$s'] = \$data['$name']; + \$data = Document::fromPHP(\$data); } EOF @@ -203,8 +206,11 @@ private function generateHydratorClass(ClassMetadata $class, string $hydratorCla <<class->fieldMappings['%2\$s']['nullable']) && array_key_exists('%1\$s', \$data))) { - \$value = \$data['%1\$s']; + if (\$data->has('%1\$s') || (! empty(\$this->class->fieldMappings['%2\$s']['nullable']) && \$data->has('%1\$s'))) { + \$value = \$data->get('%1\$s'); + if (\$value instanceof PackedArray || \$value instanceof Document) { + \$value = \$value->toPHP(DocumentManager::CLIENT_TYPEMAP); + } if (\$value !== null) { \$typeIdentifier = \$this->class->fieldMappings['%2\$s']['type']; %3\$s @@ -226,11 +232,11 @@ private function generateHydratorClass(ClassMetadata $class, string $hydratorCla <<<'EOF' // ReferenceOne - if (isset($data['%1$s']) || (! empty($this->class->fieldMappings['%2$s']['nullable']) && array_key_exists('%1$s', $data))) { - $return = $data['%1$s']; + if ($data->has('%1$s') || (! empty($this->class->fieldMappings['%2$s']['nullable']) && $data->has('%1$s'))) { + $return = $data->get('%1$s'); if ($return !== null) { - if ($this->class->fieldMappings['%2$s']['storeAs'] !== ClassMetadata::REFERENCE_STORE_AS_ID && ! is_array($return)) { - throw HydratorException::associationTypeMismatch('%3$s', '%1$s', 'array', gettype($return)); + if ($this->class->fieldMappings['%2$s']['storeAs'] !== ClassMetadata::REFERENCE_STORE_AS_ID && ! $return instanceof Document) { + throw HydratorException::associationTypeMismatch('%3$s', '%1$s', Document::class, get_debug_type($return)); } $className = $this->dm->getClassNameForAssociation($this->class->fieldMappings['%2$s'], $return); @@ -295,10 +301,10 @@ private function generateHydratorClass(ClassMetadata $class, string $hydratorCla <<<'EOF' // ReferenceMany & EmbedMany - $mongoData = $data['%1$s'] ?? null; + $mongoData = $data->has('%1$s') ? $data->get('%1$s') : null; - if ($mongoData !== null && ! is_array($mongoData)) { - throw HydratorException::associationTypeMismatch('%3$s', '%1$s', 'array', gettype($mongoData)); + if ($mongoData !== null && ! $mongoData instanceof PackedArray && ! $mongoData instanceof Document) { + throw HydratorException::associationTypeMismatch('%3$s', '%1$s', Document::class . '|' . PackedArray::class, get_debug_type($mongoData)); } $return = $this->dm->getConfiguration()->getPersistentCollectionFactory()->create($this->dm, $this->class->fieldMappings['%2$s']); @@ -322,13 +328,13 @@ private function generateHydratorClass(ClassMetadata $class, string $hydratorCla <<<'EOF' // EmbedOne - if (isset($data['%1$s']) || (! empty($this->class->fieldMappings['%2$s']['nullable']) && array_key_exists('%1$s', $data))) { - $return = $data['%1$s']; + if ($data->has('%1$s') || (! empty($this->class->fieldMappings['%2$s']['nullable']) && $data->has('%1$s'))) { + $return = $data->get('%1$s'); if ($return !== null) { $embeddedDocument = $return; - if (! is_array($embeddedDocument)) { - throw HydratorException::associationTypeMismatch('%3$s', '%1$s', 'array', gettype($embeddedDocument)); + if (! $embeddedDocument instanceof Document) { + throw HydratorException::associationTypeMismatch('%3$s', '%1$s', Document::class, get_debug_type($embeddedDocument)); } $className = $this->dm->getClassNameForAssociation($this->class->fieldMappings['%2$s'], $embeddedDocument); @@ -370,9 +376,11 @@ private function generateHydratorClass(ClassMetadata $class, string $hydratorCla use Doctrine\ODM\MongoDB\Hydrator\HydratorInterface; use Doctrine\ODM\MongoDB\Query\Query; use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; +use MongoDB\BSON\Document; +use MongoDB\BSON\PackedArray; use function array_key_exists; -use function gettype; +use function get_debug_type; use function is_array; /** @@ -382,10 +390,11 @@ class $hydratorClassName implements HydratorInterface { public function __construct(private DocumentManager \$dm, private ClassMetadata \$class) {} - public function hydrate(object \$document, array \$data, array \$hints = []): array + public function hydrate(object \$document, Document \$data, array \$hints = []): array { \$hydratedData = []; -%s return \$hydratedData; +%s + return \$hydratedData; } } EOF @@ -420,18 +429,16 @@ public function hydrate(object \$document, array \$data, array \$hints = []): ar /** * Hydrate array of MongoDB document data into the given document object. * - * @param array $data * @phpstan-param Hints $hints Any hints to account for during reconstitution/lookup of the document. * * @return array */ - public function hydrate(object $document, array $data, array $hints = []): array + public function hydrate(object $document, Document $data, array $hints = []): array { $metadata = $this->dm->getClassMetadata($document::class); // Invoke preLoad lifecycle events and listeners if (! empty($metadata->lifecycleCallbacks[Events::preLoad])) { - $args = [new PreLoadEventArgs($document, $this->dm, $data)]; - $metadata->invokeLifecycleCallbacks(Events::preLoad, $document, $args); + $metadata->invokeLifecycleCallbacks(Events::preLoad, $document, [new PreLoadEventArgs($document, $this->dm, $data)]); } $this->evm->dispatchEvent(Events::preLoad, new PreLoadEventArgs($document, $this->dm, $data)); @@ -441,8 +448,8 @@ public function hydrate(object $document, array $data, array $hints = []): array foreach ($metadata->alsoLoadMethods as $method => $fieldNames) { foreach ($fieldNames as $fieldName) { // Invoke the method only once for the first field we find - if (array_key_exists($fieldName, $data)) { - $document->$method($data[$fieldName]); + if ($data->has($fieldName)) { + $document->$method($data->get($fieldName)); continue 2; } } diff --git a/lib/Doctrine/ODM/MongoDB/Hydrator/HydratorInterface.php b/lib/Doctrine/ODM/MongoDB/Hydrator/HydratorInterface.php index f804b783d5..fc69baf912 100644 --- a/lib/Doctrine/ODM/MongoDB/Hydrator/HydratorInterface.php +++ b/lib/Doctrine/ODM/MongoDB/Hydrator/HydratorInterface.php @@ -5,6 +5,7 @@ namespace Doctrine\ODM\MongoDB\Hydrator; use Doctrine\ODM\MongoDB\UnitOfWork; +use MongoDB\BSON\Document; /** * The HydratorInterface defines methods all hydrator need to implement @@ -16,10 +17,7 @@ interface HydratorInterface /** * Hydrate array of MongoDB document data into the given document object. * - * @param array $data * @phpstan-param Hints $hints - * - * @return array */ - public function hydrate(object $document, array $data, array $hints = []): array; + public function hydrate(object $document, Document $data, array $hints = []): array; } diff --git a/lib/Doctrine/ODM/MongoDB/Iterator/HydratingIterator.php b/lib/Doctrine/ODM/MongoDB/Iterator/HydratingIterator.php index 306e15c8dc..b28890e0d8 100644 --- a/lib/Doctrine/ODM/MongoDB/Iterator/HydratingIterator.php +++ b/lib/Doctrine/ODM/MongoDB/Iterator/HydratingIterator.php @@ -8,6 +8,7 @@ use Doctrine\ODM\MongoDB\UnitOfWork; use Iterator; use IteratorIterator; +use MongoDB\BSON\Document; use ReturnTypeWillChange; use RuntimeException; use Traversable; @@ -82,12 +83,7 @@ private function getIterator(): Iterator return $this->iterator; } - /** - * @param array|null $document - * - * @return TDocument|null - */ - private function hydrate(?array $document): ?object + private function hydrate(?Document $document): ?object { return $document !== null ? $this->unitOfWork->getOrCreateDocument($this->class->name, $document, $this->unitOfWorkHints) : null; } diff --git a/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php b/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php index 896faf2408..87405d56aa 100644 --- a/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php +++ b/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php @@ -11,6 +11,7 @@ use Doctrine\Common\Collections\Collection; use Doctrine\Instantiator\Instantiator; use Doctrine\Instantiator\InstantiatorInterface; +use Doctrine\ODM\MongoDB\DocumentManager; use Doctrine\ODM\MongoDB\Id\IdGenerator; use Doctrine\ODM\MongoDB\LockException; use Doctrine\ODM\MongoDB\Mapping\Annotations\TimeSeries; @@ -25,6 +26,8 @@ use Doctrine\Persistence\Reflection\EnumReflectionProperty; use InvalidArgumentException; use LogicException; +use MongoDB\BSON\Document; +use MongoDB\BSON\PackedArray; use ProxyManager\Proxy\GhostObjectInterface; use ReflectionClass; use ReflectionEnum; @@ -1825,6 +1828,10 @@ public function getPHPIdentifierValue($id) { $idType = $this->fieldMappings[$this->identifier]['type']; + if ($id instanceof Document || $id instanceof PackedArray) { + $id = $id->toPHP(DocumentManager::CLIENT_TYPEMAP); + } + return Type::getType($idType)->convertToPHPValue($id); } diff --git a/lib/Doctrine/ODM/MongoDB/PersistentCollection/PersistentCollectionInterface.php b/lib/Doctrine/ODM/MongoDB/PersistentCollection/PersistentCollectionInterface.php index c71b5d17b7..5adce191ba 100644 --- a/lib/Doctrine/ODM/MongoDB/PersistentCollection/PersistentCollectionInterface.php +++ b/lib/Doctrine/ODM/MongoDB/PersistentCollection/PersistentCollectionInterface.php @@ -9,6 +9,8 @@ use Doctrine\ODM\MongoDB\MongoDBException; use Doctrine\ODM\MongoDB\UnitOfWork; use Doctrine\Persistence\Mapping\ClassMetadata; +use MongoDB\BSON\Document; +use MongoDB\BSON\PackedArray; /** * Interface for persistent collection classes. @@ -34,16 +36,14 @@ public function setDocumentManager(DocumentManager $dm); /** * Sets the array of raw mongo data that will be used to initialize this collection. * - * @param mixed[] $mongoData - * * @return void */ - public function setMongoData(array $mongoData); + public function setMongoData(Document|PackedArray $mongoData); /** * Gets the array of raw mongo data that will be used to initialize this collection. * - * @return mixed[] $mongoData + * @return Document|PackedArray|null $mongoData */ public function getMongoData(); diff --git a/lib/Doctrine/ODM/MongoDB/PersistentCollection/PersistentCollectionTrait.php b/lib/Doctrine/ODM/MongoDB/PersistentCollection/PersistentCollectionTrait.php index 79dae29566..426e05020e 100644 --- a/lib/Doctrine/ODM/MongoDB/PersistentCollection/PersistentCollectionTrait.php +++ b/lib/Doctrine/ODM/MongoDB/PersistentCollection/PersistentCollectionTrait.php @@ -12,6 +12,8 @@ use Doctrine\ODM\MongoDB\MongoDBException; use Doctrine\ODM\MongoDB\UnitOfWork; use Doctrine\ODM\MongoDB\Utility\CollectionHelper; +use MongoDB\BSON\Document; +use MongoDB\BSON\PackedArray; use ReturnTypeWillChange; use Traversable; @@ -84,10 +86,8 @@ trait PersistentCollectionTrait /** * The raw mongo data that will be used to initialize this collection. - * - * @var mixed[] */ - private array $mongoData = []; + private Document|PackedArray $mongoData; /** * Any hints to account for during reconstitution/lookup of the documents. @@ -103,14 +103,14 @@ public function setDocumentManager(DocumentManager $dm) $this->uow = $dm->getUnitOfWork(); } - public function setMongoData(array $mongoData) + public function setMongoData(Document|PackedArray $mongoData) { $this->mongoData = $mongoData; } public function getMongoData() { - return $this->mongoData; + return $this->mongoData ?? null; } public function setHints(array $hints) @@ -143,7 +143,7 @@ public function initialize() $this->uow->loadCollection($this); $this->takeSnapshot(); - $this->mongoData = []; + unset($this->mongoData); // Reattach any NEW objects added through add() if (! $newObjects) { @@ -480,7 +480,7 @@ public function clear() } } - $this->mongoData = []; + unset($this->mongoData); $this->coll->clear(); // Nothing to do for inverse-side collections diff --git a/lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php b/lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php index da09225913..656b4b43db 100644 --- a/lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php +++ b/lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php @@ -30,7 +30,9 @@ use Doctrine\Persistence\Mapping\MappingException; use InvalidArgumentException; use Iterator as SplIterator; +use MongoDB\BSON\Document; use MongoDB\BSON\ObjectId; +use MongoDB\BSON\PackedArray; use MongoDB\Collection; use MongoDB\Driver\CursorInterface; use MongoDB\Driver\Exception\Exception as DriverException; @@ -53,8 +55,8 @@ use function assert; use function count; use function explode; +use function get_debug_type; use function get_object_vars; -use function gettype; use function implode; use function in_array; use function is_array; @@ -454,12 +456,12 @@ public function refresh(object $document): void { assert($this->collection instanceof Collection); $query = $this->getQueryForDocument($document); - $data = $this->collection->findOne($query); + $data = $this->collection->findOne($query, ['typeMap' => DocumentManager::HYDRATION_TYPEMAP]); if ($data === null) { throw MongoDBException::cannotRefreshDocument(); } - $data = $this->hydratorFactory->hydrate($document, (array) $data); + $data = $this->hydratorFactory->hydrate($document, $data); $this->uow->setOriginalDocumentData($document, $data); } @@ -493,14 +495,14 @@ public function load($criteria, ?object $document = null, array $hints = [], int $criteria = $this->addDiscriminatorToPreparedQuery($criteria); $criteria = $this->addFilterToPreparedQuery($criteria); - $options = []; + $options = []; + $options['typeMap'] = DocumentManager::HYDRATION_TYPEMAP; if ($sort !== null) { $options['sort'] = $this->prepareSort($sort); } assert($this->collection instanceof Collection); $result = $this->collection->findOne($criteria, $options); - $result = $result !== null ? (array) $result : null; if ($this->class->isLockable) { $lockMapping = $this->class->fieldMappings[$this->class->lockField]; @@ -541,6 +543,8 @@ public function loadAll(array $criteria = [], ?array $sort = null, ?int $limit = $options['skip'] = $skip; } + $options['typeMap'] = DocumentManager::HYDRATION_TYPEMAP; + assert($this->collection instanceof Collection); $baseCursor = $this->collection->find($criteria, $options); @@ -644,7 +648,7 @@ public function unlock(object $document): void * @return object The filled and managed document object. * @phpstan-return T */ - private function createDocument(array $result, ?object $document = null, array $hints = []): object + private function createDocument(Document $result, ?object $document = null, array $hints = []): object { if ($document !== null) { $hints[Query::HINT_REFRESH] = true; @@ -700,8 +704,8 @@ private function loadEmbedManyCollection(PersistentCollectionInterface $collecti $embeddedMetadata = $this->dm->getClassMetadata($className); $embeddedDocumentObject = $embeddedMetadata->newInstance(); - if (! is_array($embeddedDocument)) { - throw HydratorException::associationItemTypeMismatch($owner::class, $mapping['name'], $key, 'array', gettype($embeddedDocument)); + if (! $embeddedDocument instanceof Document) { + throw HydratorException::associationItemTypeMismatch($owner::class, $mapping['name'], $key, Document::class, get_debug_type($embeddedDocument)); } $this->uow->setParentAssociation($embeddedDocumentObject, $mapping, $owner, $mapping['name'] . '.' . $key); @@ -735,10 +739,14 @@ private function loadReferenceManyCollectionOwningSide(PersistentCollectionInter $sorted = isset($mapping['sort']) && $mapping['sort']; foreach ($collection->getMongoData() as $key => $reference) { + if ($reference instanceof Document || $reference instanceof PackedArray) { + $reference = $reference->toPHP(DocumentManager::CLIENT_TYPEMAP); + } + $className = $this->dm->getClassNameForAssociation($mapping, $reference); if ($mapping['storeAs'] !== ClassMetadata::REFERENCE_STORE_AS_ID && ! is_array($reference)) { - throw HydratorException::associationItemTypeMismatch($owner::class, $mapping['name'], $key, 'array', gettype($reference)); + throw HydratorException::associationItemTypeMismatch($owner::class, $mapping['name'], $key, 'array', get_debug_type($reference)); } $identifier = ClassMetadata::getReferenceId($reference, $mapping['storeAs']); @@ -791,10 +799,17 @@ private function loadReferenceManyCollectionOwningSide(PersistentCollectionInter $options['readPreference'] = $hints[Query::HINT_READ_PREFERENCE]; } + $options['typeMap'] = DocumentManager::HYDRATION_TYPEMAP; + $cursor = $mongoCollection->find($criteria, $options); $documents = $cursor->toArray(); foreach ($documents as $documentData) { - $document = $this->uow->getById($documentData['_id'], $class); + $id = $documentData['_id']; + if ($id instanceof Document || $id instanceof PackedArray) { + $id = $id->toPHP(DocumentManager::CLIENT_TYPEMAP); + } + + $document = $this->uow->getById($id, $class); if ($this->uow->isUninitializedObject($document)) { $data = $this->hydratorFactory->hydrate($document, $documentData); $this->uow->setOriginalDocumentData($document, $data); diff --git a/lib/Doctrine/ODM/MongoDB/Query/Builder.php b/lib/Doctrine/ODM/MongoDB/Query/Builder.php index 0af6b0a067..415677d995 100644 --- a/lib/Doctrine/ODM/MongoDB/Query/Builder.php +++ b/lib/Doctrine/ODM/MongoDB/Query/Builder.php @@ -708,6 +708,10 @@ public function getQuery(array $options = []): IterableResult $query['readPreference'] = new ReadPreference($this->class->readPreference, $this->class->readPreferenceTags); } + if ($this->hydrate) { + $options['typeMap'] = DocumentManager::HYDRATION_TYPEMAP; + } + return new Query( $this->dm, $this->class, diff --git a/lib/Doctrine/ODM/MongoDB/Query/Query.php b/lib/Doctrine/ODM/MongoDB/Query/Query.php index d33961f6d6..1cf14b5bba 100644 --- a/lib/Doctrine/ODM/MongoDB/Query/Query.php +++ b/lib/Doctrine/ODM/MongoDB/Query/Query.php @@ -17,6 +17,7 @@ use Doctrine\ODM\MongoDB\MongoDBException; use Doctrine\ODM\MongoDB\UnitOfWork; use InvalidArgumentException; +use MongoDB\BSON\Document; use MongoDB\Collection; use MongoDB\DeleteResult; use MongoDB\Driver\ReadPreference; @@ -35,7 +36,6 @@ use function array_map; use function array_merge; use function array_values; -use function is_array; use function is_callable; use function is_string; @@ -216,7 +216,7 @@ public function execute(): mixed if ( ($this->query['type'] === self::TYPE_FIND_AND_UPDATE || $this->query['type'] === self::TYPE_FIND_AND_REMOVE) && - is_array($results) && isset($results['_id']) + $results instanceof Document && $results->has('_id') ) { $results = $uow->getOrCreateDocument($this->class->name, $results, $this->unitOfWorkHints); diff --git a/lib/Doctrine/ODM/MongoDB/Types/HashType.php b/lib/Doctrine/ODM/MongoDB/Types/HashType.php index 27ed20a90b..5b40785d78 100644 --- a/lib/Doctrine/ODM/MongoDB/Types/HashType.php +++ b/lib/Doctrine/ODM/MongoDB/Types/HashType.php @@ -4,7 +4,9 @@ namespace Doctrine\ODM\MongoDB\Types; +use Doctrine\ODM\MongoDB\DocumentManager; use Doctrine\ODM\MongoDB\MongoDBException; +use MongoDB\BSON\Document; use function is_array; @@ -15,6 +17,11 @@ class HashType extends Type { public function convertToDatabaseValue($value) { + if ($value instanceof Document) { + // Check the Document could be returned directly, it is serialized in the UoW + return (object) $value->toPHP(DocumentManager::CLIENT_TYPEMAP); + } + if ($value !== null && ! is_array($value)) { throw MongoDBException::invalidValueForType('Hash', ['array', 'null'], $value); } diff --git a/lib/Doctrine/ODM/MongoDB/UnitOfWork.php b/lib/Doctrine/ODM/MongoDB/UnitOfWork.php index 20150702b6..4d53a91b64 100644 --- a/lib/Doctrine/ODM/MongoDB/UnitOfWork.php +++ b/lib/Doctrine/ODM/MongoDB/UnitOfWork.php @@ -25,6 +25,7 @@ use Doctrine\Persistence\NotifyPropertyChanged; use Doctrine\Persistence\PropertyChangedListener; use InvalidArgumentException; +use MongoDB\BSON\Document; use MongoDB\Driver\Exception\RuntimeException; use MongoDB\Driver\Session; use MongoDB\Driver\WriteConcern; @@ -2738,14 +2739,14 @@ public function getOwningDocument(object $document): object * * @template T of object */ - public function getOrCreateDocument(string $className, array $data, array &$hints = [], ?object $document = null): object + public function getOrCreateDocument(string $className, Document $data, array &$hints = [], ?object $document = null): object { $class = $this->dm->getClassMetadata($className); // @TODO figure out how to remove this $discriminatorValue = null; - if (isset($class->discriminatorField, $data[$class->discriminatorField])) { - $discriminatorValue = $data[$class->discriminatorField]; + if (isset($class->discriminatorField) && $data->has($class->discriminatorField)) { + $discriminatorValue = $data->get($class->discriminatorField); } elseif (isset($class->defaultDiscriminatorValue)) { $discriminatorValue = $class->defaultDiscriminatorValue; } @@ -2756,7 +2757,8 @@ public function getOrCreateDocument(string $className, array $data, array &$hint $class = $this->dm->getClassMetadata($className); - unset($data[$class->discriminatorField]); + // @todo we cannot unset a property of the BSON document. Is it really necessary? + //unset($data[$class->discriminatorField]); } if (! empty($hints[Query::HINT_READ_ONLY])) { @@ -2771,7 +2773,7 @@ public function getOrCreateDocument(string $className, array $data, array &$hint $serializedId = null; $id = null; if (! $class->isQueryResultDocument) { - $id = $class->getDatabaseIdentifierValue($data['_id']); + $id = $class->getDatabaseIdentifierValue($data->get('_id')); $serializedId = serialize($id); $isManagedObject = isset($this->identityMap[$class->name][$serializedId]); } @@ -2944,7 +2946,7 @@ public function size(): int * @param mixed $id The identifier values. * @param array $data */ - public function registerManaged(object $document, $id, array $data): void + public function registerManaged(object $document, $id, Document|array $data): void { $oid = spl_object_hash($document); $class = $this->dm->getClassMetadata($document::class); @@ -2956,7 +2958,7 @@ public function registerManaged(object $document, $id, array $data): void } $this->documentStates[$oid] = self::STATE_MANAGED; - $this->originalDocumentData[$oid] = $data; + $this->originalDocumentData[$oid] = is_array($data) ? $data : $data->toPHP(['root' => 'array']); $this->addToIdentityMap($document); } diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Events/PreLoadEventArgsTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Events/PreLoadEventArgsTest.php index 553b56be34..eb744dbd79 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Events/PreLoadEventArgsTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/Events/PreLoadEventArgsTest.php @@ -7,6 +7,7 @@ use Doctrine\ODM\MongoDB\Event\PreLoadEventArgs; use Doctrine\ODM\MongoDB\Tests\BaseTestCase; use Documents\Group; +use MongoDB\BSON\Document; class PreLoadEventArgsTest extends BaseTestCase { @@ -14,7 +15,7 @@ public function testGetData(): void { $document = new Group('test'); $dm = $this->dm; - $data = ['id' => '1234', 'name' => 'test']; + $data = Document::fromPHP(['id' => '1234', 'name' => 'test']); $eventArgs = new PreLoadEventArgs($document, $dm, $data); $eventArgsData =& $eventArgs->getData(); @@ -22,7 +23,8 @@ public function testGetData(): void self::assertEquals('test', $eventArgsData['name']); $eventArgsData['name'] = 'alt name'; + unset($eventArgs); - self::assertEquals('alt name', $data['name']); + self::assertEquals('alt name', $data->get('name')); } } diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Functional/Iterator/HydratingIteratorTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Functional/Iterator/HydratingIteratorTest.php index 438dc5d78c..b4aea25f1d 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Functional/Iterator/HydratingIteratorTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/Functional/Iterator/HydratingIteratorTest.php @@ -8,6 +8,7 @@ use Doctrine\ODM\MongoDB\Tests\BaseTestCase; use Documents\User; use Generator; +use MongoDB\BSON\Document; use MongoDB\BSON\ObjectId; use function is_array; @@ -57,9 +58,9 @@ private function getTraversable(?array $items = null): Generator { if (! is_array($items)) { $items = [ - ['_id' => new ObjectId(), 'username' => 'foo', 'hits' => 1], - ['_id' => new ObjectId(), 'username' => 'bar', 'hits' => 2], - ['_id' => new ObjectId(), 'username' => 'baz', 'hits' => 3], + Document::fromPHP(['_id' => new ObjectId(), 'username' => 'foo', 'hits' => 1]), + Document::fromPHP(['_id' => new ObjectId(), 'username' => 'bar', 'hits' => 2]), + Document::fromPHP(['_id' => new ObjectId(), 'username' => 'baz', 'hits' => 3]), ]; } diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Functional/Ticket/GH1418Test.php b/tests/Doctrine/ODM/MongoDB/Tests/Functional/Ticket/GH1418Test.php index d001f9b07f..5e5ddf1f5d 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Functional/Ticket/GH1418Test.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/Functional/Ticket/GH1418Test.php @@ -8,6 +8,7 @@ use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; use Doctrine\ODM\MongoDB\Query\Query; use Doctrine\ODM\MongoDB\Tests\BaseTestCase; +use MongoDB\BSON\Document; use function assert; @@ -16,14 +17,14 @@ class GH1418Test extends BaseTestCase public function testManualHydrateAndMerge(): void { $document = new GH1418Document(); - $this->dm->getHydratorFactory()->hydrate($document, [ + $this->dm->getHydratorFactory()->hydrate($document, Document::fromPHP([ '_id' => 1, 'name' => 'maciej', 'embedOne' => ['name' => 'maciej', 'sourceId' => 1], 'embedMany' => [ ['name' => 'maciej', 'sourceId' => 2], ], - ], [Query::HINT_READ_ONLY => true]); + ]), [Query::HINT_READ_ONLY => true]); self::assertEquals(1, $document->embedOne->id); self::assertEquals(2, $document->embedMany->first()->id); diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Functional/Ticket/GH852Test.php b/tests/Doctrine/ODM/MongoDB/Tests/Functional/Ticket/GH852Test.php index 3895f43250..c19c344478 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Functional/Ticket/GH852Test.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/Functional/Ticket/GH852Test.php @@ -12,6 +12,7 @@ use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; use Doctrine\ODM\MongoDB\Tests\BaseTestCase; use MongoDB\BSON\Binary; +use MongoDB\BSON\Document; use PHPUnit\Framework\Attributes\DataProvider; class GH852Test extends BaseTestCase diff --git a/tests/Doctrine/ODM/MongoDB/Tests/HydratorTest.php b/tests/Doctrine/ODM/MongoDB/Tests/HydratorTest.php index 0982d629bb..e59843971a 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/HydratorTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/HydratorTest.php @@ -11,6 +11,8 @@ use Doctrine\ODM\MongoDB\PersistentCollection; use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionInterface; use Doctrine\ODM\MongoDB\Query\Query; +use MongoDB\BSON\Document; +use MongoDB\BSON\UTCDateTime; class HydratorTest extends BaseTestCase { @@ -19,11 +21,11 @@ public function testHydrator(): void $class = $this->dm->getClassMetadata(HydrationClosureUser::class); $user = new HydrationClosureUser(); - $this->dm->getHydratorFactory()->hydrate($user, [ + $this->dm->getHydratorFactory()->hydrate($user, Document::fromPHP([ '_id' => 1, 'title' => null, 'name' => 'jon', - 'birthdate' => new DateTime('1961-01-01'), + 'birthdate' => new UTCDateTime(new DateTime('1961-01-01')), 'referenceOne' => ['$id' => '1'], 'referenceMany' => [ ['$id' => '1'], @@ -33,7 +35,7 @@ public function testHydrator(): void 'embedMany' => [ ['name' => 'jon'], ], - ]); + ])); self::assertEquals(1, $user->id); self::assertNull($user->title); @@ -55,11 +57,11 @@ public function testHydrateProxyWithMissingAssociations(): void $user = $this->dm->getReference(HydrationClosureUser::class, 1); self::assertTrue(self::isLazyObject($user)); - $this->dm->getHydratorFactory()->hydrate($user, [ + $this->dm->getHydratorFactory()->hydrate($user, Document::fromPHP([ '_id' => 1, 'title' => null, 'name' => 'jon', - ]); + ])); self::assertEquals(1, $user->id); self::assertNull($user->title); @@ -76,15 +78,15 @@ public function testReadOnly(): void $class = $this->dm->getClassMetadata(HydrationClosureUser::class); $user = new HydrationClosureUser(); - $this->dm->getHydratorFactory()->hydrate($user, [ + $this->dm->getHydratorFactory()->hydrate($user, Document::fromPHP([ '_id' => 1, 'name' => 'maciej', - 'birthdate' => new DateTime('1961-01-01'), + 'birthdate' => new UTCDateTime(new DateTime('1961-01-01')), 'embedOne' => ['name' => 'maciej'], 'embedMany' => [ ['name' => 'maciej'], ], - ], [Query::HINT_READ_ONLY => true]); + ]), [Query::HINT_READ_ONLY => true]); self::assertFalse($this->uow->isInIdentityMap($user)); self::assertFalse($this->uow->isInIdentityMap($user->embedOne)); @@ -96,12 +98,12 @@ public function testEmbedOneWithWrongType(): void $user = new HydrationClosureUser(); $this->expectException(HydratorException::class); - $this->expectExceptionMessage('Expected association for field "embedOne" in document of type "' . HydrationClosureUser::class . '" to be of type "array", "string" received.'); + $this->expectExceptionMessage('Expected association for field "embedOne" in document of type "' . HydrationClosureUser::class . '" to be of type "MongoDB\BSON\Document", "string" received.'); - $this->dm->getHydratorFactory()->hydrate($user, [ + $this->dm->getHydratorFactory()->hydrate($user, Document::fromPHP([ '_id' => 1, 'embedOne' => 'jon', - ]); + ])); } public function testEmbedManyWithWrongType(): void @@ -109,27 +111,27 @@ public function testEmbedManyWithWrongType(): void $user = new HydrationClosureUser(); $this->expectException(HydratorException::class); - $this->expectExceptionMessage('Expected association for field "embedMany" in document of type "' . HydrationClosureUser::class . '" to be of type "array", "string" received.'); + $this->expectExceptionMessage('Expected association for field "embedMany" in document of type "' . HydrationClosureUser::class . '" to be of type "MongoDB\BSON\Document|MongoDB\BSON\PackedArray", "string" received.'); - $this->dm->getHydratorFactory()->hydrate($user, [ + $this->dm->getHydratorFactory()->hydrate($user, Document::fromPHP([ '_id' => 1, 'embedMany' => 'jon', - ]); + ])); } public function testEmbedManyWithWrongElementType(): void { $user = new HydrationClosureUser(); - $this->dm->getHydratorFactory()->hydrate($user, [ + $this->dm->getHydratorFactory()->hydrate($user, Document::fromPHP([ '_id' => 1, 'embedMany' => ['jon'], - ]); + ])); self::assertInstanceOf(PersistentCollectionInterface::class, $user->embedMany); $this->expectException(HydratorException::class); - $this->expectExceptionMessage('Expected association item with key "0" for field "embedMany" in document of type "' . HydrationClosureUser::class . '" to be of type "array", "string" received.'); + $this->expectExceptionMessage('Expected association item with key "0" for field "embedMany" in document of type "' . HydrationClosureUser::class . '" to be of type "MongoDB\BSON\Document", "string" received.'); $user->embedMany->initialize(); } @@ -139,12 +141,12 @@ public function testReferenceOneWithWrongType(): void $user = new HydrationClosureUser(); $this->expectException(HydratorException::class); - $this->expectExceptionMessage('Expected association for field "referenceOne" in document of type "' . HydrationClosureUser::class . '" to be of type "array", "string" received.'); + $this->expectExceptionMessage('Expected association for field "referenceOne" in document of type "' . HydrationClosureUser::class . '" to be of type "MongoDB\BSON\Document", "string" received.'); - $this->dm->getHydratorFactory()->hydrate($user, [ + $this->dm->getHydratorFactory()->hydrate($user, Document::fromPHP([ '_id' => 1, 'referenceOne' => 'jon', - ]); + ])); } public function testReferenceManyWithWrongType(): void @@ -152,22 +154,22 @@ public function testReferenceManyWithWrongType(): void $user = new HydrationClosureUser(); $this->expectException(HydratorException::class); - $this->expectExceptionMessage('Expected association for field "referenceMany" in document of type "' . HydrationClosureUser::class . '" to be of type "array", "string" received.'); + $this->expectExceptionMessage('Expected association for field "referenceMany" in document of type "' . HydrationClosureUser::class . '" to be of type "MongoDB\BSON\Document|MongoDB\BSON\PackedArray", "string" received.'); - $this->dm->getHydratorFactory()->hydrate($user, [ + $this->dm->getHydratorFactory()->hydrate($user, Document::fromPHP([ '_id' => 1, 'referenceMany' => 'jon', - ]); + ])); } public function testReferenceManyWithWrongElementType(): void { $user = new HydrationClosureUser(); - $this->dm->getHydratorFactory()->hydrate($user, [ + $this->dm->getHydratorFactory()->hydrate($user, Document::fromPHP([ '_id' => 1, 'referenceMany' => ['jon'], - ]); + ])); self::assertInstanceOf(PersistentCollectionInterface::class, $user->referenceMany); diff --git a/tests/Doctrine/ODM/MongoDB/Tests/PersistentCollectionTest.php b/tests/Doctrine/ODM/MongoDB/Tests/PersistentCollectionTest.php index 41b52f1dd2..2cd27f1159 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/PersistentCollectionTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/PersistentCollectionTest.php @@ -12,6 +12,7 @@ use Doctrine\ODM\MongoDB\PersistentCollection; use Documents\User; use MongoDB\BSON\ObjectId; +use MongoDB\BSON\PackedArray; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\MockObject\MockObject; use stdClass; @@ -86,7 +87,7 @@ public function testGetTypeClassWorksAfterUnserialization(): void public function testMongoDataIsPreservedDuringSerialization(): void { - $mongoData = [ + $mongoData = PackedArray::fromPHP([ [ '$ref' => 'group', '$id' => new ObjectId(), @@ -95,7 +96,7 @@ public function testMongoDataIsPreservedDuringSerialization(): void '$ref' => 'group', '$id' => new ObjectId(), ], - ]; + ]); $collection = new PersistentCollection(new ArrayCollection(), $this->dm, $this->uow); $collection->setMongoData($mongoData);