Skip to content

Commit 38dc1e3

Browse files
authored
PHPORM-239 Convert _id and UTCDateTime in results of Model::raw() before hydratation (#3152)
1 parent d6ac34a commit 38dc1e3

File tree

5 files changed

+106
-18
lines changed

5 files changed

+106
-18
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
# Changelog
22
All notable changes to this project will be documented in this file.
33

4+
## [5.1.0] - next
5+
6+
* Convert `_id` and `UTCDateTime` in results of `Model::raw()` before hydratation by @GromNaN in [#3152](https://github.com/mongodb/laravel-mongodb/pull/3152)
7+
48
## [5.0.2] - 2024-09-17
59

610
* Fix missing return types in CommandSubscriber by @GromNaN in [#3158](https://github.com/mongodb/laravel-mongodb/pull/3158)

src/Eloquent/Builder.php

+18-10
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
namespace MongoDB\Laravel\Eloquent;
66

77
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
8-
use MongoDB\Driver\Cursor;
8+
use MongoDB\BSON\Document;
9+
use MongoDB\Driver\CursorInterface;
910
use MongoDB\Driver\Exception\WriteException;
1011
use MongoDB\Laravel\Connection;
1112
use MongoDB\Laravel\Helpers\QueriesRelationships;
@@ -16,7 +17,9 @@
1617
use function array_merge;
1718
use function collect;
1819
use function is_array;
20+
use function is_object;
1921
use function iterator_to_array;
22+
use function property_exists;
2023

2124
/** @method \MongoDB\Laravel\Query\Builder toBase() */
2225
class Builder extends EloquentBuilder
@@ -177,22 +180,27 @@ public function raw($value = null)
177180
$results = $this->query->raw($value);
178181

179182
// Convert MongoCursor results to a collection of models.
180-
if ($results instanceof Cursor) {
181-
$results = iterator_to_array($results, false);
183+
if ($results instanceof CursorInterface) {
184+
$results->setTypeMap(['root' => 'array', 'document' => 'array', 'array' => 'array']);
185+
$results = $this->query->aliasIdForResult(iterator_to_array($results));
182186

183187
return $this->model->hydrate($results);
184188
}
185189

186-
// Convert MongoDB BSONDocument to a single object.
187-
if ($results instanceof BSONDocument) {
188-
$results = $results->getArrayCopy();
189-
190-
return $this->model->newFromBuilder((array) $results);
190+
// Convert MongoDB Document to a single object.
191+
if (is_object($results) && (property_exists($results, '_id') || property_exists($results, 'id'))) {
192+
$results = (array) match (true) {
193+
$results instanceof BSONDocument => $results->getArrayCopy(),
194+
$results instanceof Document => $results->toPHP(['root' => 'array', 'document' => 'array', 'array' => 'array']),
195+
default => $results,
196+
};
191197
}
192198

193199
// The result is a single object.
194-
if (is_array($results) && array_key_exists('_id', $results)) {
195-
return $this->model->newFromBuilder((array) $results);
200+
if (is_array($results) && (array_key_exists('_id', $results) || array_key_exists('id', $results))) {
201+
$results = $this->query->aliasIdForResult($results);
202+
203+
return $this->model->newFromBuilder($results);
196204
}
197205

198206
return $results;

src/Query/Builder.php

+3-1
Original file line numberDiff line numberDiff line change
@@ -1648,13 +1648,15 @@ private function aliasIdForQuery(array $values): array
16481648
}
16491649

16501650
/**
1651+
* @internal
1652+
*
16511653
* @psalm-param T $values
16521654
*
16531655
* @psalm-return T
16541656
*
16551657
* @template T of array|object
16561658
*/
1657-
private function aliasIdForResult(array|object $values): array|object
1659+
public function aliasIdForResult(array|object $values): array|object
16581660
{
16591661
if (is_array($values)) {
16601662
if (array_key_exists('_id', $values) && ! array_key_exists('id', $values)) {

tests/ModelTest.php

+56-7
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
use MongoDB\Laravel\Tests\Models\Soft;
2929
use MongoDB\Laravel\Tests\Models\SqlUser;
3030
use MongoDB\Laravel\Tests\Models\User;
31+
use MongoDB\Model\BSONArray;
32+
use MongoDB\Model\BSONDocument;
3133
use PHPUnit\Framework\Attributes\DataProvider;
3234
use PHPUnit\Framework\Attributes\TestWith;
3335

@@ -907,14 +909,8 @@ public function testRaw(): void
907909
$this->assertInstanceOf(EloquentCollection::class, $users);
908910
$this->assertInstanceOf(User::class, $users[0]);
909911

910-
$user = User::raw(function (Collection $collection) {
911-
return $collection->findOne(['age' => 35]);
912-
});
913-
914-
$this->assertTrue(Model::isDocumentModel($user));
915-
916912
$count = User::raw(function (Collection $collection) {
917-
return $collection->count();
913+
return $collection->estimatedDocumentCount();
918914
});
919915
$this->assertEquals(3, $count);
920916

@@ -924,6 +920,59 @@ public function testRaw(): void
924920
$this->assertNotNull($result);
925921
}
926922

923+
#[DataProvider('provideTypeMap')]
924+
public function testRawHyradeModel(array $typeMap): void
925+
{
926+
User::insert([
927+
['name' => 'John Doe', 'age' => 35, 'embed' => ['foo' => 'bar'], 'list' => [1, 2, 3]],
928+
['name' => 'Jane Doe', 'age' => 35, 'embed' => ['foo' => 'bar'], 'list' => [1, 2, 3]],
929+
['name' => 'Harry Hoe', 'age' => 15, 'embed' => ['foo' => 'bar'], 'list' => [1, 2, 3]],
930+
]);
931+
932+
// Single document result
933+
$user = User::raw(fn (Collection $collection) => $collection->findOne(
934+
['age' => 35],
935+
[
936+
'projection' => ['_id' => 1, 'name' => 1, 'age' => 1, 'now' => '$$NOW', 'embed' => 1, 'list' => 1],
937+
'typeMap' => $typeMap,
938+
],
939+
));
940+
941+
$this->assertInstanceOf(User::class, $user);
942+
$this->assertArrayNotHasKey('_id', $user->getAttributes());
943+
$this->assertArrayHasKey('id', $user->getAttributes());
944+
$this->assertNotEmpty($user->id);
945+
$this->assertInstanceOf(Carbon::class, $user->now);
946+
$this->assertEquals(['foo' => 'bar'], (array) $user->embed);
947+
$this->assertEquals([1, 2, 3], (array) $user->list);
948+
949+
// Cursor result
950+
$result = User::raw(fn (Collection $collection) => $collection->aggregate([
951+
['$set' => ['now' => '$$NOW']],
952+
['$limit' => 2],
953+
], ['typeMap' => $typeMap]));
954+
955+
$this->assertInstanceOf(EloquentCollection::class, $result);
956+
$this->assertCount(2, $result);
957+
$user = $result->first();
958+
$this->assertInstanceOf(User::class, $user);
959+
$this->assertArrayNotHasKey('_id', $user->getAttributes());
960+
$this->assertArrayHasKey('id', $user->getAttributes());
961+
$this->assertNotEmpty($user->id);
962+
$this->assertInstanceOf(Carbon::class, $user->now);
963+
$this->assertEquals(['foo' => 'bar'], $user->embed);
964+
$this->assertEquals([1, 2, 3], $user->list);
965+
}
966+
967+
public static function provideTypeMap(): Generator
968+
{
969+
yield 'default' => [[]];
970+
yield 'array' => [['root' => 'array', 'document' => 'array', 'array' => 'array']];
971+
yield 'object' => [['root' => 'object', 'document' => 'object', 'array' => 'array']];
972+
yield 'Library BSON' => [['root' => BSONDocument::class, 'document' => BSONDocument::class, 'array' => BSONArray::class]];
973+
yield 'Driver BSON' => [['root' => 'bson', 'document' => 'bson', 'array' => 'bson']];
974+
}
975+
927976
public function testDotNotation(): void
928977
{
929978
$user = User::create([

tests/Query/BuilderTest.php

+25
Original file line numberDiff line numberDiff line change
@@ -1381,6 +1381,31 @@ function (Builder $elemMatchQuery): void {
13811381
->orWhereAny(['last_name', 'email'], 'not like', '%Doe%'),
13821382
'orWhereAny',
13831383
];
1384+
1385+
yield 'raw filter with _id and date' => [
1386+
[
1387+
'find' => [
1388+
[
1389+
'$and' => [
1390+
[
1391+
'$or' => [
1392+
['foo._id' => 1],
1393+
['created_at' => ['$gte' => new UTCDateTime(new DateTimeImmutable('2018-09-30 00:00:00.000 +00:00'))]],
1394+
],
1395+
],
1396+
['age' => 15],
1397+
],
1398+
],
1399+
[], // options
1400+
],
1401+
],
1402+
fn (Builder $builder) => $builder->where([
1403+
'$or' => [
1404+
['foo.id' => 1],
1405+
['created_at' => ['$gte' => new DateTimeImmutable('2018-09-30 00:00:00 +00:00')]],
1406+
],
1407+
])->where('age', 15),
1408+
];
13841409
}
13851410

13861411
#[DataProvider('provideExceptions')]

0 commit comments

Comments
 (0)