Skip to content

Commit

Permalink
PHPORM-239 Convert _id and UTCDateTime in results of `Model::raw(…
Browse files Browse the repository at this point in the history
…)` before hydratation (#3152)
  • Loading branch information
GromNaN authored Sep 17, 2024
1 parent d6ac34a commit 38dc1e3
Show file tree
Hide file tree
Showing 5 changed files with 106 additions and 18 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# Changelog
All notable changes to this project will be documented in this file.

## [5.1.0] - next

* Convert `_id` and `UTCDateTime` in results of `Model::raw()` before hydratation by @GromNaN in [#3152](https://github.com/mongodb/laravel-mongodb/pull/3152)

## [5.0.2] - 2024-09-17

* Fix missing return types in CommandSubscriber by @GromNaN in [#3158](https://github.com/mongodb/laravel-mongodb/pull/3158)
Expand Down
28 changes: 18 additions & 10 deletions src/Eloquent/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
namespace MongoDB\Laravel\Eloquent;

use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use MongoDB\Driver\Cursor;
use MongoDB\BSON\Document;
use MongoDB\Driver\CursorInterface;
use MongoDB\Driver\Exception\WriteException;
use MongoDB\Laravel\Connection;
use MongoDB\Laravel\Helpers\QueriesRelationships;
Expand All @@ -16,7 +17,9 @@
use function array_merge;
use function collect;
use function is_array;
use function is_object;
use function iterator_to_array;
use function property_exists;

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

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

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

// Convert MongoDB BSONDocument to a single object.
if ($results instanceof BSONDocument) {
$results = $results->getArrayCopy();

return $this->model->newFromBuilder((array) $results);
// Convert MongoDB Document to a single object.
if (is_object($results) && (property_exists($results, '_id') || property_exists($results, 'id'))) {
$results = (array) match (true) {
$results instanceof BSONDocument => $results->getArrayCopy(),
$results instanceof Document => $results->toPHP(['root' => 'array', 'document' => 'array', 'array' => 'array']),
default => $results,
};
}

// The result is a single object.
if (is_array($results) && array_key_exists('_id', $results)) {
return $this->model->newFromBuilder((array) $results);
if (is_array($results) && (array_key_exists('_id', $results) || array_key_exists('id', $results))) {
$results = $this->query->aliasIdForResult($results);

return $this->model->newFromBuilder($results);
}

return $results;
Expand Down
4 changes: 3 additions & 1 deletion src/Query/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -1648,13 +1648,15 @@ private function aliasIdForQuery(array $values): array
}

/**
* @internal
*
* @psalm-param T $values
*
* @psalm-return T
*
* @template T of array|object
*/
private function aliasIdForResult(array|object $values): array|object
public function aliasIdForResult(array|object $values): array|object
{
if (is_array($values)) {
if (array_key_exists('_id', $values) && ! array_key_exists('id', $values)) {
Expand Down
63 changes: 56 additions & 7 deletions tests/ModelTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
use MongoDB\Laravel\Tests\Models\Soft;
use MongoDB\Laravel\Tests\Models\SqlUser;
use MongoDB\Laravel\Tests\Models\User;
use MongoDB\Model\BSONArray;
use MongoDB\Model\BSONDocument;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\TestWith;

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

$user = User::raw(function (Collection $collection) {
return $collection->findOne(['age' => 35]);
});

$this->assertTrue(Model::isDocumentModel($user));

$count = User::raw(function (Collection $collection) {
return $collection->count();
return $collection->estimatedDocumentCount();
});
$this->assertEquals(3, $count);

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

#[DataProvider('provideTypeMap')]
public function testRawHyradeModel(array $typeMap): void
{
User::insert([
['name' => 'John Doe', 'age' => 35, 'embed' => ['foo' => 'bar'], 'list' => [1, 2, 3]],
['name' => 'Jane Doe', 'age' => 35, 'embed' => ['foo' => 'bar'], 'list' => [1, 2, 3]],
['name' => 'Harry Hoe', 'age' => 15, 'embed' => ['foo' => 'bar'], 'list' => [1, 2, 3]],
]);

// Single document result
$user = User::raw(fn (Collection $collection) => $collection->findOne(
['age' => 35],
[
'projection' => ['_id' => 1, 'name' => 1, 'age' => 1, 'now' => '$$NOW', 'embed' => 1, 'list' => 1],
'typeMap' => $typeMap,
],
));

$this->assertInstanceOf(User::class, $user);
$this->assertArrayNotHasKey('_id', $user->getAttributes());
$this->assertArrayHasKey('id', $user->getAttributes());
$this->assertNotEmpty($user->id);
$this->assertInstanceOf(Carbon::class, $user->now);
$this->assertEquals(['foo' => 'bar'], (array) $user->embed);
$this->assertEquals([1, 2, 3], (array) $user->list);

// Cursor result
$result = User::raw(fn (Collection $collection) => $collection->aggregate([
['$set' => ['now' => '$$NOW']],
['$limit' => 2],
], ['typeMap' => $typeMap]));

$this->assertInstanceOf(EloquentCollection::class, $result);
$this->assertCount(2, $result);
$user = $result->first();
$this->assertInstanceOf(User::class, $user);
$this->assertArrayNotHasKey('_id', $user->getAttributes());
$this->assertArrayHasKey('id', $user->getAttributes());
$this->assertNotEmpty($user->id);
$this->assertInstanceOf(Carbon::class, $user->now);
$this->assertEquals(['foo' => 'bar'], $user->embed);
$this->assertEquals([1, 2, 3], $user->list);
}

public static function provideTypeMap(): Generator
{
yield 'default' => [[]];
yield 'array' => [['root' => 'array', 'document' => 'array', 'array' => 'array']];
yield 'object' => [['root' => 'object', 'document' => 'object', 'array' => 'array']];
yield 'Library BSON' => [['root' => BSONDocument::class, 'document' => BSONDocument::class, 'array' => BSONArray::class]];
yield 'Driver BSON' => [['root' => 'bson', 'document' => 'bson', 'array' => 'bson']];
}

public function testDotNotation(): void
{
$user = User::create([
Expand Down
25 changes: 25 additions & 0 deletions tests/Query/BuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1381,6 +1381,31 @@ function (Builder $elemMatchQuery): void {
->orWhereAny(['last_name', 'email'], 'not like', '%Doe%'),
'orWhereAny',
];

yield 'raw filter with _id and date' => [
[
'find' => [
[
'$and' => [
[
'$or' => [
['foo._id' => 1],
['created_at' => ['$gte' => new UTCDateTime(new DateTimeImmutable('2018-09-30 00:00:00.000 +00:00'))]],
],
],
['age' => 15],
],
],
[], // options
],
],
fn (Builder $builder) => $builder->where([
'$or' => [
['foo.id' => 1],
['created_at' => ['$gte' => new DateTimeImmutable('2018-09-30 00:00:00 +00:00')]],
],
])->where('age', 15),
];
}

#[DataProvider('provideExceptions')]
Expand Down

0 comments on commit 38dc1e3

Please sign in to comment.