Skip to content

Commit 3b4ad5a

Browse files
committed
PHPORM-255 Add a method to disable renaming the id to _id field in embedded documents
1 parent e62b1d5 commit 3b4ad5a

File tree

3 files changed

+68
-18
lines changed

3 files changed

+68
-18
lines changed

src/Connection.php

Lines changed: 15 additions & 0 deletions
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 = true;
58+
5659
/**
5760
* Create a new database connection instance.
5861
*/
@@ -395,6 +398,18 @@ public function __call($method, $parameters)
395398
return $this->db->$method(...$parameters);
396399
}
397400

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

src/Query/Builder.php

Lines changed: 19 additions & 9 deletions
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+
/** @method Connection getConnection() */
8688
class Builder extends BaseBuilder
8789
{
8890
private const REGEX_DELIMITERS = ['/', '#', '~'];
@@ -124,6 +126,8 @@ class Builder extends BaseBuilder
124126
*/
125127
public $options = [];
126128

129+
private ?bool $renameEmbeddedIdField;
130+
127131
/**
128132
* All of the available clause operators.
129133
*
@@ -1764,9 +1768,9 @@ public function orWhereIntegerNotInRaw($column, $values, $boolean = 'and')
17641768
throw new BadMethodCallException('This method is not supported by MongoDB');
17651769
}
17661770

1767-
private function aliasIdForQuery(array $values): array
1771+
private function aliasIdForQuery(array $values, bool $root = true): array
17681772
{
1769-
if (array_key_exists('id', $values)) {
1773+
if (array_key_exists('id', $values) && ($root || $this->getConnection()->getRenameEmbeddedIdField())) {
17701774
if (array_key_exists('_id', $values) && $values['id'] !== $values['_id']) {
17711775
throw new InvalidArgumentException('Cannot have both "id" and "_id" fields.');
17721776
}
@@ -1793,7 +1797,7 @@ private function aliasIdForQuery(array $values): array
17931797
}
17941798

17951799
// ".id" subfield are alias for "._id"
1796-
if (str_ends_with($key, '.id')) {
1800+
if (str_ends_with($key, '.id') && ($root || $this->getConnection()->getRenameEmbeddedIdField())) {
17971801
$newkey = substr($key, 0, -3) . '._id';
17981802
if (array_key_exists($newkey, $values) && $value !== $values[$newkey]) {
17991803
throw new InvalidArgumentException(sprintf('Cannot have both "%s" and "%s" fields.', $key, $newkey));
@@ -1806,7 +1810,7 @@ private function aliasIdForQuery(array $values): array
18061810

18071811
foreach ($values as &$value) {
18081812
if (is_array($value)) {
1809-
$value = $this->aliasIdForQuery($value);
1813+
$value = $this->aliasIdForQuery($value, false);
18101814
} elseif ($value instanceof DateTimeInterface) {
18111815
$value = new UTCDateTime($value);
18121816
}
@@ -1824,10 +1828,13 @@ private function aliasIdForQuery(array $values): array
18241828
*
18251829
* @template T of array|object
18261830
*/
1827-
public function aliasIdForResult(array|object $values): array|object
1831+
public function aliasIdForResult(array|object $values, bool $root = true): array|object
18281832
{
18291833
if (is_array($values)) {
1830-
if (array_key_exists('_id', $values) && ! array_key_exists('id', $values)) {
1834+
if (
1835+
array_key_exists('_id', $values) && ! array_key_exists('id', $values)
1836+
&& ($root || $this->getConnection()->getRenameEmbeddedIdField())
1837+
) {
18311838
$values['id'] = $values['_id'];
18321839
unset($values['_id']);
18331840
}
@@ -1837,13 +1844,16 @@ public function aliasIdForResult(array|object $values): array|object
18371844
$values[$key] = Date::instance($value->toDateTime())
18381845
->setTimezone(new DateTimeZone(date_default_timezone_get()));
18391846
} elseif (is_array($value) || is_object($value)) {
1840-
$values[$key] = $this->aliasIdForResult($value);
1847+
$values[$key] = $this->aliasIdForResult($value, false);
18411848
}
18421849
}
18431850
}
18441851

18451852
if ($values instanceof stdClass) {
1846-
if (property_exists($values, '_id') && ! property_exists($values, 'id')) {
1853+
if (
1854+
property_exists($values, '_id') && ! property_exists($values, 'id')
1855+
&& ($root || $this->getConnection()->getRenameEmbeddedIdField())
1856+
) {
18471857
$values->id = $values->_id;
18481858
unset($values->_id);
18491859
}
@@ -1853,7 +1863,7 @@ public function aliasIdForResult(array|object $values): array|object
18531863
$values->{$key} = Date::instance($value->toDateTime())
18541864
->setTimezone(new DateTimeZone(date_default_timezone_get()));
18551865
} elseif (is_array($value) || is_object($value)) {
1856-
$values->{$key} = $this->aliasIdForResult($value);
1866+
$values->{$key} = $this->aliasIdForResult($value, false);
18571867
}
18581868
}
18591869
}

tests/Query/BuilderTest.php

Lines changed: 34 additions & 9 deletions
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
}

0 commit comments

Comments
 (0)