Skip to content

Commit bd8705a

Browse files
authoredApr 9, 2025··
PHPORM-255 Enable disabling the id to _id field rename in embedded documents (#3332)
1 parent e62b1d5 commit bd8705a

File tree

5 files changed

+159
-19
lines changed

5 files changed

+159
-19
lines changed
 

‎src/Connection.php

+17
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ class Connection extends BaseConnection
5353

5454
private ?CommandSubscriber $commandSubscriber = null;
5555

56+
/** @var bool Whether to rename the rename "id" into "_id" for embedded documents. */
57+
private bool $renameEmbeddedIdField;
58+
5659
/**
5760
* Create a new database connection instance.
5861
*/
@@ -80,6 +83,8 @@ public function __construct(array $config)
8083
$this->useDefaultSchemaGrammar();
8184

8285
$this->useDefaultQueryGrammar();
86+
87+
$this->renameEmbeddedIdField = $config['rename_embedded_id_field'] ?? true;
8388
}
8489

8590
/**
@@ -395,6 +400,18 @@ public function __call($method, $parameters)
395400
return $this->db->$method(...$parameters);
396401
}
397402

403+
/** Set whether to rename "id" field into "_id" for embedded documents. */
404+
public function setRenameEmbeddedIdField(bool $rename): void
405+
{
406+
$this->renameEmbeddedIdField = $rename;
407+
}
408+
409+
/** Get whether to rename "id" field into "_id" for embedded documents. */
410+
public function getRenameEmbeddedIdField(): bool
411+
{
412+
return $this->renameEmbeddedIdField;
413+
}
414+
398415
/**
399416
* Return the server version of one of the MongoDB servers: primary for
400417
* replica sets and standalone, and the selected server for sharded clusters.

‎src/Eloquent/Builder.php

+2-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use MongoDB\Model\BSONDocument;
1919

2020
use function array_key_exists;
21+
use function array_map;
2122
use function array_replace;
2223
use function collect;
2324
use function is_array;
@@ -237,7 +238,7 @@ public function raw($value = null)
237238
// Convert MongoCursor results to a collection of models.
238239
if ($results instanceof CursorInterface) {
239240
$results->setTypeMap(['root' => 'array', 'document' => 'array', 'array' => 'array']);
240-
$results = $this->query->aliasIdForResult(iterator_to_array($results));
241+
$results = array_map(fn ($document) => $this->query->aliasIdForResult($document), iterator_to_array($results));
241242

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

‎src/Query/Builder.php

+17-9
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
use MongoDB\Builder\Type\SearchOperatorInterface;
3030
use MongoDB\Driver\Cursor;
3131
use MongoDB\Driver\ReadPreference;
32+
use MongoDB\Laravel\Connection;
3233
use Override;
3334
use RuntimeException;
3435
use stdClass;
@@ -83,6 +84,7 @@
8384
use function trait_exists;
8485
use function var_export;
8586

87+
/** @property Connection $connection */
8688
class Builder extends BaseBuilder
8789
{
8890
private const REGEX_DELIMITERS = ['/', '#', '~'];
@@ -1764,9 +1766,9 @@ public function orWhereIntegerNotInRaw($column, $values, $boolean = 'and')
17641766
throw new BadMethodCallException('This method is not supported by MongoDB');
17651767
}
17661768

1767-
private function aliasIdForQuery(array $values): array
1769+
private function aliasIdForQuery(array $values, bool $root = true): array
17681770
{
1769-
if (array_key_exists('id', $values)) {
1771+
if (array_key_exists('id', $values) && ($root || $this->connection->getRenameEmbeddedIdField())) {
17701772
if (array_key_exists('_id', $values) && $values['id'] !== $values['_id']) {
17711773
throw new InvalidArgumentException('Cannot have both "id" and "_id" fields.');
17721774
}
@@ -1793,7 +1795,7 @@ private function aliasIdForQuery(array $values): array
17931795
}
17941796

17951797
// ".id" subfield are alias for "._id"
1796-
if (str_ends_with($key, '.id')) {
1798+
if (str_ends_with($key, '.id') && ($root || $this->connection->getRenameEmbeddedIdField())) {
17971799
$newkey = substr($key, 0, -3) . '._id';
17981800
if (array_key_exists($newkey, $values) && $value !== $values[$newkey]) {
17991801
throw new InvalidArgumentException(sprintf('Cannot have both "%s" and "%s" fields.', $key, $newkey));
@@ -1806,7 +1808,7 @@ private function aliasIdForQuery(array $values): array
18061808

18071809
foreach ($values as &$value) {
18081810
if (is_array($value)) {
1809-
$value = $this->aliasIdForQuery($value);
1811+
$value = $this->aliasIdForQuery($value, false);
18101812
} elseif ($value instanceof DateTimeInterface) {
18111813
$value = new UTCDateTime($value);
18121814
}
@@ -1824,10 +1826,13 @@ private function aliasIdForQuery(array $values): array
18241826
*
18251827
* @template T of array|object
18261828
*/
1827-
public function aliasIdForResult(array|object $values): array|object
1829+
public function aliasIdForResult(array|object $values, bool $root = true): array|object
18281830
{
18291831
if (is_array($values)) {
1830-
if (array_key_exists('_id', $values) && ! array_key_exists('id', $values)) {
1832+
if (
1833+
array_key_exists('_id', $values) && ! array_key_exists('id', $values)
1834+
&& ($root || $this->connection->getRenameEmbeddedIdField())
1835+
) {
18311836
$values['id'] = $values['_id'];
18321837
unset($values['_id']);
18331838
}
@@ -1837,13 +1842,16 @@ public function aliasIdForResult(array|object $values): array|object
18371842
$values[$key] = Date::instance($value->toDateTime())
18381843
->setTimezone(new DateTimeZone(date_default_timezone_get()));
18391844
} elseif (is_array($value) || is_object($value)) {
1840-
$values[$key] = $this->aliasIdForResult($value);
1845+
$values[$key] = $this->aliasIdForResult($value, false);
18411846
}
18421847
}
18431848
}
18441849

18451850
if ($values instanceof stdClass) {
1846-
if (property_exists($values, '_id') && ! property_exists($values, 'id')) {
1851+
if (
1852+
property_exists($values, '_id') && ! property_exists($values, 'id')
1853+
&& ($root || $this->connection->getRenameEmbeddedIdField())
1854+
) {
18471855
$values->id = $values->_id;
18481856
unset($values->_id);
18491857
}
@@ -1853,7 +1861,7 @@ public function aliasIdForResult(array|object $values): array|object
18531861
$values->{$key} = Date::instance($value->toDateTime())
18541862
->setTimezone(new DateTimeZone(date_default_timezone_get()));
18551863
} elseif (is_array($value) || is_object($value)) {
1856-
$values->{$key} = $this->aliasIdForResult($value);
1864+
$values->{$key} = $this->aliasIdForResult($value, false);
18571865
}
18581866
}
18591867
}

‎tests/Query/BuilderTest.php

+34-9
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
use Illuminate\Tests\Database\DatabaseQueryBuilderTest;
1313
use InvalidArgumentException;
1414
use LogicException;
15-
use Mockery as m;
1615
use MongoDB\BSON\Regex;
1716
use MongoDB\BSON\UTCDateTime;
1817
use MongoDB\Driver\ReadPreference;
@@ -39,7 +38,7 @@ public function testMql(array $expected, Closure $build, ?string $requiredMethod
3938
$this->markTestSkipped(sprintf('Method "%s::%s()" does not exist.', Builder::class, $requiredMethod));
4039
}
4140

42-
$builder = $build(self::getBuilder());
41+
$builder = $build($this->getBuilder());
4342
$this->assertInstanceOf(Builder::class, $builder);
4443
$mql = $builder->toMql();
4544

@@ -1447,7 +1446,7 @@ function (Builder $elemMatchQuery): void {
14471446
#[DataProvider('provideExceptions')]
14481447
public function testException($class, $message, Closure $build): void
14491448
{
1450-
$builder = self::getBuilder();
1449+
$builder = $this->getBuilder();
14511450

14521451
$this->expectException($class);
14531452
$this->expectExceptionMessage($message);
@@ -1545,7 +1544,7 @@ public static function provideExceptions(): iterable
15451544
#[DataProvider('getEloquentMethodsNotSupported')]
15461545
public function testEloquentMethodsNotSupported(Closure $callback)
15471546
{
1548-
$builder = self::getBuilder();
1547+
$builder = $this->getBuilder();
15491548

15501549
$this->expectException(BadMethodCallException::class);
15511550
$this->expectExceptionMessage('This method is not supported by MongoDB');
@@ -1600,12 +1599,38 @@ public static function getEloquentMethodsNotSupported()
16001599
yield 'orWhereIntegerNotInRaw' => [fn (Builder $builder) => $builder->orWhereIntegerNotInRaw('id', ['1a', 2])];
16011600
}
16021601

1603-
private static function getBuilder(): Builder
1602+
public function testRenameEmbeddedIdFieldCanBeDisabled()
16041603
{
1605-
$connection = m::mock(Connection::class);
1606-
$processor = m::mock(Processor::class);
1607-
$connection->shouldReceive('getSession')->andReturn(null);
1608-
$connection->shouldReceive('getQueryGrammar')->andReturn(new Grammar($connection));
1604+
$builder = $this->getBuilder(false);
1605+
$this->assertFalse($builder->getConnection()->getRenameEmbeddedIdField());
1606+
1607+
$mql = $builder
1608+
->where('id', '=', 10)
1609+
->where('nested.id', '=', 20)
1610+
->where('embed', '=', ['id' => 30])
1611+
->toMql();
1612+
1613+
$this->assertEquals([
1614+
'find' => [
1615+
[
1616+
'$and' => [
1617+
['_id' => 10],
1618+
['nested.id' => 20],
1619+
['embed' => ['id' => 30]],
1620+
],
1621+
],
1622+
['typeMap' => ['root' => 'object', 'document' => 'array']],
1623+
],
1624+
], $mql);
1625+
}
1626+
1627+
private function getBuilder(bool $renameEmbeddedIdField = true): Builder
1628+
{
1629+
$connection = $this->createStub(Connection::class);
1630+
$connection->method('getRenameEmbeddedIdField')->willReturn($renameEmbeddedIdField);
1631+
$processor = $this->createStub(Processor::class);
1632+
$connection->method('getSession')->willReturn(null);
1633+
$connection->method('getQueryGrammar')->willReturn(new Grammar($connection));
16091634

16101635
return new Builder($connection, null, $processor);
16111636
}

‎tests/QueryBuilderTest.php

+89
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,12 @@
2020
use MongoDB\BSON\UTCDateTime;
2121
use MongoDB\Collection;
2222
use MongoDB\Driver\Cursor;
23+
use MongoDB\Driver\CursorInterface;
2324
use MongoDB\Driver\Monitoring\CommandFailedEvent;
2425
use MongoDB\Driver\Monitoring\CommandStartedEvent;
2526
use MongoDB\Driver\Monitoring\CommandSubscriber;
2627
use MongoDB\Driver\Monitoring\CommandSucceededEvent;
28+
use MongoDB\Laravel\Connection;
2729
use MongoDB\Laravel\Query\Builder;
2830
use MongoDB\Laravel\Tests\Models\Item;
2931
use MongoDB\Laravel\Tests\Models\User;
@@ -336,6 +338,93 @@ public function testRaw()
336338
$this->assertEquals('Jane Doe', $results[0]->name);
337339
}
338340

341+
public function testRawResultRenameId()
342+
{
343+
$connection = DB::connection('mongodb');
344+
self::assertInstanceOf(Connection::class, $connection);
345+
346+
$date = Carbon::createFromDate(1986, 12, 31)->setTime(12, 0, 0);
347+
User::insert([
348+
['id' => 1, 'name' => 'Jane Doe', 'address' => ['id' => 11, 'city' => 'Ghent'], 'birthday' => $date],
349+
['id' => 2, 'name' => 'John Doe', 'address' => ['id' => 12, 'city' => 'Brussels'], 'birthday' => $date],
350+
]);
351+
352+
// Using raw database query, result is not altered
353+
$results = $connection->table('users')->raw(fn (Collection $collection) => $collection->find([]));
354+
self::assertInstanceOf(CursorInterface::class, $results);
355+
$results = $results->toArray();
356+
self::assertCount(2, $results);
357+
358+
self::assertObjectHasProperty('_id', $results[0]);
359+
self::assertObjectNotHasProperty('id', $results[0]);
360+
self::assertSame(1, $results[0]->_id);
361+
362+
self::assertObjectHasProperty('_id', $results[0]->address);
363+
self::assertObjectNotHasProperty('id', $results[0]->address);
364+
self::assertSame(11, $results[0]->address->_id);
365+
366+
self::assertInstanceOf(UTCDateTime::class, $results[0]->birthday);
367+
368+
// Using Eloquent query, result is transformed
369+
self::assertTrue($connection->getRenameEmbeddedIdField());
370+
$results = User::raw(fn (Collection $collection) => $collection->find([]));
371+
self::assertInstanceOf(LaravelCollection::class, $results);
372+
self::assertCount(2, $results);
373+
374+
$attributes = $results->first()->getAttributes();
375+
self::assertArrayHasKey('id', $attributes);
376+
self::assertArrayNotHasKey('_id', $attributes);
377+
self::assertSame(1, $attributes['id']);
378+
379+
self::assertArrayHasKey('id', $attributes['address']);
380+
self::assertArrayNotHasKey('_id', $attributes['address']);
381+
self::assertSame(11, $attributes['address']['id']);
382+
383+
self::assertEquals($date, $attributes['birthday']);
384+
385+
// Single result
386+
$result = User::raw(fn (Collection $collection) => $collection->findOne([], ['typeMap' => ['root' => 'object', 'document' => 'array']]));
387+
self::assertInstanceOf(User::class, $result);
388+
389+
$attributes = $result->getAttributes();
390+
self::assertArrayHasKey('id', $attributes);
391+
self::assertArrayNotHasKey('_id', $attributes);
392+
self::assertSame(1, $attributes['id']);
393+
394+
self::assertArrayHasKey('id', $attributes['address']);
395+
self::assertArrayNotHasKey('_id', $attributes['address']);
396+
self::assertSame(11, $attributes['address']['id']);
397+
398+
// Change the renameEmbeddedIdField option
399+
$connection->setRenameEmbeddedIdField(false);
400+
401+
$results = User::raw(fn (Collection $collection) => $collection->find([]));
402+
self::assertInstanceOf(LaravelCollection::class, $results);
403+
self::assertCount(2, $results);
404+
405+
$attributes = $results->first()->getAttributes();
406+
self::assertArrayHasKey('id', $attributes);
407+
self::assertArrayNotHasKey('_id', $attributes);
408+
self::assertSame(1, $attributes['id']);
409+
410+
self::assertArrayHasKey('_id', $attributes['address']);
411+
self::assertArrayNotHasKey('id', $attributes['address']);
412+
self::assertSame(11, $attributes['address']['_id']);
413+
414+
// Single result
415+
$result = User::raw(fn (Collection $collection) => $collection->findOne([]));
416+
self::assertInstanceOf(User::class, $result);
417+
418+
$attributes = $result->getAttributes();
419+
self::assertArrayHasKey('id', $attributes);
420+
self::assertArrayNotHasKey('_id', $attributes);
421+
self::assertSame(1, $attributes['id']);
422+
423+
self::assertArrayHasKey('_id', $attributes['address']);
424+
self::assertArrayNotHasKey('id', $attributes['address']);
425+
self::assertSame(11, $attributes['address']['_id']);
426+
}
427+
339428
public function testPush()
340429
{
341430
$id = DB::table('users')->insertGetId([

0 commit comments

Comments
 (0)
Please sign in to comment.