Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions src/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -1726,6 +1726,49 @@ public function toArray(mixed $obj): ?array
return $ret;
}

/**
* Replace BSON Int64 wrappers with native PHP integers.
*
* MongoDB\BSON\Document::toPHP() decodes every 64-bit BSON integer as an
* Int64 object on all platforms, so any value outside the int32 range comes
* back wrapped. Callers expect plain PHP values, and an Int64 that survives
* into user code either fatals when array-accessed or degrades to 1 under an
* (int) cast. On 64-bit PHP the unwrap is lossless; on 32-bit builds the
* wrapper is the only representation that preserves precision, so it stays.
*
* @param mixed $value
* @param array<string, true> $skip Top-level keys to leave untouched
* @return mixed
*/
private static function normalizeInt64(mixed $value, array $skip = []): mixed
{
if ($value instanceof Int64) {
return \PHP_INT_SIZE >= 8 ? (int)(string)$value : $value;
}

if (\is_array($value)) {
foreach ($value as $key => $item) {
$value[$key] = self::normalizeInt64($item);
}

return $value;
}

if ($value instanceof stdClass) {
foreach (\get_object_vars($value) as $key => $item) {
if (isset($skip[$key])) {
continue;
}

$value->{$key} = self::normalizeInt64($item);
}

return $value;
}

return $value;
}

private function cleanFilters($filters): array
{
$cleanedFilters = [];
Expand Down Expand Up @@ -1910,6 +1953,11 @@ private function parseResponse(string $response, int $responseLength): stdClass|
if (\is_array($result)) {
$result = (object)$result;
}

// $clusterTime is echoed back to the server verbatim on subsequent
// commands, so its BSON types must survive intact — signature.keyId
// is an int64 the server rejects if it comes back as an int32.
$result = self::normalizeInt64($result, ['$clusterTime' => true]);
} catch (\Throwable $error) {
$this->invalidate();
throw new Exception('Failed to parse BSON response: ' . $error->getMessage(), 0, $error);
Expand Down
54 changes: 54 additions & 0 deletions tests/MongoTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,60 @@ public function testToArrayWithNestedDocumentFromMongo()
$client->dropCollection('movies_nested');
}

public function testInt64ValuesDecodeToNativeIntegers()
{
if (\PHP_INT_SIZE < 8) {
// normalizeInt64() deliberately keeps the wrapper on 32-bit builds,
// where it is the only lossless representation, and the literals
// below would already be floats before reaching the driver.
self::markTestSkipped('Native int64 round-trip requires a 64-bit PHP build.');
}

$client = $this->getDatabase();

// Beyond the int32 range, so MongoDB stores these as BSON int64 and
// Document::toPHP() hands them back as MongoDB\BSON\Int64 wrappers.
$negative = -3408048000;
$positive = 3408048000;
$extreme = \PHP_INT_MAX;

try {
$client->insert('movies_int64', [
'_id' => 'int64-test-1',
'small' => -42,
'big' => $negative,
'list' => [$negative, -42, $positive, $extreme],
'nested' => ['deep' => ['value' => $negative]],
]);

$result = $client->find('movies_int64', ['_id' => 'int64-test-1'])->cursor->firstBatch[0] ?? null;
self::assertNotNull($result);

self::assertIsInt($result->small);
self::assertSame(-42, $result->small);

self::assertIsInt($result->big);
Comment thread
greptile-apps[bot] marked this conversation as resolved.
self::assertSame($negative, $result->big);

self::assertIsInt($result->list[0]);
self::assertSame($negative, $result->list[0]);
self::assertSame(-42, $result->list[1]);
self::assertSame($positive, $result->list[2]);
self::assertSame($extreme, $result->list[3]);

self::assertIsInt($result->nested->deep->value);
self::assertSame($negative, $result->nested->deep->value);

// toArray() must carry the native integers through untouched.
$array = $client->toArray($result);
self::assertSame($negative, $array['big']);
self::assertSame([$negative, -42, $positive, $extreme], $array['list']);
self::assertSame($negative, $array['nested']['deep']['value']);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
} finally {
$client->dropCollection('movies_int64');
}
}

public function testToArrayNestedConversion()
{
$client = $this->getDatabase();
Expand Down
Loading