diff --git a/src/Database/Adapter/Mongo.php b/src/Database/Adapter/Mongo.php index 6894d3508e..3e2aee1e85 100644 --- a/src/Database/Adapter/Mongo.php +++ b/src/Database/Adapter/Mongo.php @@ -1267,7 +1267,7 @@ public function getDocument(Document $collection, string $id, array $queries = [ $resultArray = $this->client->toArray($result[0]); $result = $this->replaceChars('_', '$', $resultArray); - $document = new Document($result); + $document = new Document($this->convertStdClassToArray($result)); $document = $this->castingAfter($collection, $document); // Ensure missing relationship attributes are set to null (MongoDB doesn't store null fields) @@ -1308,6 +1308,7 @@ public function createDocument(Document $collection, Document $document): Docume $options = $this->getTransactionOptions(); $result = $this->insertDocument($name, $this->removeNullKeys($record), $options); $result = $this->replaceChars('_', '$', $result); + $result = $this->convertStdClassToArray($result); // in order to keep the original object refrence. foreach ($result as $key => $value) { $document->setAttribute($key, $value); @@ -1366,7 +1367,7 @@ public function castingAfter(Document $collection, Document $document): Document switch ($type) { case Database::VAR_INTEGER: case Database::VAR_BIGINT: - $node = (int)$node; + $node = is_object($node) ? (int)(string)$node : (int)$node; break; case Database::VAR_DATETIME: $node = $this->convertUTCDateToString($node); @@ -1400,6 +1401,10 @@ public function castingAfter(Document $collection, Document $document): Document private function convertStdClassToArray(mixed $value): mixed { + if (is_object($value) && (get_class($value) === 'MongoDB\BSON\Int64' || $value instanceof \MongoDB\BSON\Int64)) { + return (int)(string)$value; + } + if (is_object($value) && get_class($value) === stdClass::class) { return array_map($this->convertStdClassToArray(...), get_object_vars($value)); } @@ -1587,7 +1592,7 @@ public function createDocuments(Document $collection, array $documents): array foreach ($documents as $index => $document) { $documents[$index] = $this->replaceChars('_', '$', $this->client->toArray($document)); - $documents[$index] = new Document($documents[$index]); + $documents[$index] = new Document($this->convertStdClassToArray($documents[$index])); } return $documents; diff --git a/src/Database/Document.php b/src/Database/Document.php index 73bd458cd5..7fe8eae525 100644 --- a/src/Database/Document.php +++ b/src/Database/Document.php @@ -46,12 +46,21 @@ public function __construct(array $input = []) } foreach ($value as $childKey => $child) { - // An array value is either a list of nested sub-documents or a list of - // plain items (dates, numbers, strings): wrap the former, leave the latter. - // is_array() tells them apart and avoids array-accessing a non-array - // value (e.g. a UTCDateTime), which would otherwise fatal. + if ($child instanceof self) { + continue; + } + if (\is_array($child) && (isset($child['$id']) || isset($child['$collection']))) { $value[$childKey] = new self($child); + } elseif ($child instanceof \ArrayAccess && (isset($child['$id']) || isset($child['$collection']))) { + if ($child instanceof \Traversable) { + $value[$childKey] = new self(\iterator_to_array($child)); + } elseif (\method_exists($child, 'getArrayCopy')) { + $value[$childKey] = new self($child->getArrayCopy()); + } elseif (\method_exists($child, 'toArray')) { + $value[$childKey] = new self($child->toArray()); + } + // If none of the above, we cannot safely enumerate the offsets — leave as-is. } } diff --git a/tests/unit/DocumentTest.php b/tests/unit/DocumentTest.php index 9a41ab5340..94a119dc99 100644 --- a/tests/unit/DocumentTest.php +++ b/tests/unit/DocumentTest.php @@ -417,4 +417,46 @@ public function testEmptyDocumentSequence(): void $this->assertNull($empty->getSequence()); $this->assertNotSame('', $empty->getSequence()); } + + public function testNonArrayAccessObjectsInArray(): void + { + $mockObject = new class () { + public function __toString(): string + { + return '-3408048000'; + } + }; + + $doc = new Document([ + '$id' => 'test_non_array_access', + 'listOfIntegers' => [ + $mockObject, + ], + ]); + + $this->assertEquals('test_non_array_access', $doc->getId()); + $this->assertCount(1, $doc->getAttribute('listOfIntegers')); + $this->assertSame($mockObject, $doc->getAttribute('listOfIntegers')[0]); + } + + public function testArrayAccessHydrationInArray(): void + { + $arrayAccessObject = new \ArrayObject([ + '$id' => 'nested_doc', + 'name' => 'nested_name', + ]); + + $doc = new Document([ + '$id' => 'parent_doc', + 'children' => [ + $arrayAccessObject, + ], + ]); + + $this->assertEquals('parent_doc', $doc->getId()); + $this->assertCount(1, $doc->getAttribute('children')); + $this->assertInstanceOf(Document::class, $doc->getAttribute('children')[0]); + $this->assertEquals('nested_doc', $doc->getAttribute('children')[0]->getId()); + $this->assertEquals('nested_name', $doc->getAttribute('children')[0]->getAttribute('name')); + } }