Skip to content

Commit d3dddc0

Browse files
authored
Merge pull request doctrine#1481 from alcaeus/fix-dbref-queries
Fix query preparation when querying for referenced object
2 parents 1ee3996 + afe12fa commit d3dddc0

File tree

2 files changed

+135
-32
lines changed

2 files changed

+135
-32
lines changed

lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php

Lines changed: 68 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -908,9 +908,9 @@ public function prepareSortOrProjection(array $fields)
908908
*/
909909
public function prepareFieldName($fieldName)
910910
{
911-
list($fieldName) = $this->prepareQueryElement($fieldName, null, null, false);
911+
$fieldNames = $this->prepareQueryElement($fieldName, null, null, false);
912912

913-
return $fieldName;
913+
return $fieldNames[0][0];
914914
}
915915

916916
/**
@@ -991,11 +991,12 @@ public function prepareQueryOrNewObj(array $query)
991991
continue;
992992
}
993993

994-
list($key, $value) = $this->prepareQueryElement($key, $value, null, true);
995-
996-
$preparedQuery[$key] = is_array($value)
997-
? array_map('\Doctrine\ODM\MongoDB\Types\Type::convertPHPToDatabaseValue', $value)
998-
: Type::convertPHPToDatabaseValue($value);
994+
$preparedQueryElements = $this->prepareQueryElement($key, $value, null, true);
995+
foreach ($preparedQueryElements as list($preparedKey, $preparedValue)) {
996+
$preparedQuery[$preparedKey] = is_array($preparedValue)
997+
? array_map('\Doctrine\ODM\MongoDB\Types\Type::convertPHPToDatabaseValue', $preparedValue)
998+
: Type::convertPHPToDatabaseValue($preparedValue);
999+
}
9991000
}
10001001

10011002
return $preparedQuery;
@@ -1011,7 +1012,7 @@ public function prepareQueryOrNewObj(array $query)
10111012
* @param mixed $value
10121013
* @param ClassMetadata $class Defaults to $this->class
10131014
* @param boolean $prepareValue Whether or not to prepare the value
1014-
* @return array Prepared field name and value
1015+
* @return array An array of tuples containing prepared field names and values
10151016
*/
10161017
private function prepareQueryElement($fieldName, $value = null, $class = null, $prepareValue = true)
10171018
{
@@ -1025,18 +1026,18 @@ private function prepareQueryElement($fieldName, $value = null, $class = null, $
10251026
$fieldName = $mapping['name'];
10261027

10271028
if ( ! $prepareValue) {
1028-
return array($fieldName, $value);
1029+
return [[$fieldName, $value]];
10291030
}
10301031

10311032
// Prepare mapped, embedded objects
10321033
if ( ! empty($mapping['embedded']) && is_object($value) &&
10331034
! $this->dm->getMetadataFactory()->isTransient(get_class($value))) {
1034-
return array($fieldName, $this->pb->prepareEmbeddedDocumentValue($mapping, $value));
1035+
return [[$fieldName, $this->pb->prepareEmbeddedDocumentValue($mapping, $value)]];
10351036
}
10361037

10371038
if (! empty($mapping['reference']) && is_object($value) && ! ($value instanceof \MongoId)) {
10381039
try {
1039-
return array($fieldName, $this->dm->createDBRef($value, $mapping));
1040+
return $this->prepareDbRefElement($fieldName, $value, $mapping);
10401041
} catch (MappingException $e) {
10411042
// do nothing in case passed object is not mapped document
10421043
}
@@ -1046,47 +1047,47 @@ private function prepareQueryElement($fieldName, $value = null, $class = null, $
10461047
// We can't have expressions in empty() with PHP < 5.5, so store it in a variable
10471048
$arrayValue = (array) $value;
10481049
if (empty($mapping['reference']) || $mapping['storeAs'] !== ClassMetadataInfo::REFERENCE_STORE_AS_ID || empty($arrayValue)) {
1049-
return array($fieldName, $value);
1050+
return [[$fieldName, $value]];
10501051
}
10511052

10521053
// Additional preparation for one or more simple reference values
10531054
$targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
10541055

10551056
if ( ! is_array($value)) {
1056-
return array($fieldName, $targetClass->getDatabaseIdentifierValue($value));
1057+
return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
10571058
}
10581059

10591060
// Objects without operators or with DBRef fields can be converted immediately
10601061
if ( ! $this->hasQueryOperators($value) || $this->hasDBRefFields($value)) {
1061-
return array($fieldName, $targetClass->getDatabaseIdentifierValue($value));
1062+
return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
10621063
}
10631064

1064-
return array($fieldName, $this->prepareQueryExpression($value, $targetClass));
1065+
return [[$fieldName, $this->prepareQueryExpression($value, $targetClass)]];
10651066
}
10661067

10671068
// Process identifier fields
10681069
if (($class->hasField($fieldName) && $class->isIdentifier($fieldName)) || $fieldName === '_id') {
10691070
$fieldName = '_id';
10701071

10711072
if ( ! $prepareValue) {
1072-
return array($fieldName, $value);
1073+
return [[$fieldName, $value]];
10731074
}
10741075

10751076
if ( ! is_array($value)) {
1076-
return array($fieldName, $class->getDatabaseIdentifierValue($value));
1077+
return [[$fieldName, $class->getDatabaseIdentifierValue($value)]];
10771078
}
10781079

10791080
// Objects without operators or with DBRef fields can be converted immediately
10801081
if ( ! $this->hasQueryOperators($value) || $this->hasDBRefFields($value)) {
1081-
return array($fieldName, $class->getDatabaseIdentifierValue($value));
1082+
return [[$fieldName, $class->getDatabaseIdentifierValue($value)]];
10821083
}
10831084

1084-
return array($fieldName, $this->prepareQueryExpression($value, $class));
1085+
return [[$fieldName, $this->prepareQueryExpression($value, $class)]];
10851086
}
10861087

10871088
// No processing for unmapped, non-identifier, non-dotted field names
10881089
if (strpos($fieldName, '.') === false) {
1089-
return array($fieldName, $value);
1090+
return [[$fieldName, $value]];
10901091
}
10911092

10921093
/* Process "fieldName.objectProperty" queries (on arrays or objects).
@@ -1099,7 +1100,7 @@ private function prepareQueryElement($fieldName, $value = null, $class = null, $
10991100

11001101
// No further processing for unmapped fields
11011102
if ( ! isset($class->fieldMappings[$e[0]])) {
1102-
return array($fieldName, $value);
1103+
return [[$fieldName, $value]];
11031104
}
11041105

11051106
$mapping = $class->fieldMappings[$e[0]];
@@ -1109,7 +1110,7 @@ private function prepareQueryElement($fieldName, $value = null, $class = null, $
11091110
if ($mapping['type'] === Type::HASH || $mapping['type'] === Type::RAW) {
11101111
$fieldName = implode('.', $e);
11111112

1112-
return array($fieldName, $value);
1113+
return [[$fieldName, $value]];
11131114
}
11141115

11151116
if ($mapping['type'] == 'many' && CollectionHelper::isHash($mapping['strategy'])
@@ -1130,7 +1131,7 @@ private function prepareQueryElement($fieldName, $value = null, $class = null, $
11301131
} else {
11311132
$fieldName = $e[0] . '.' . $e[1];
11321133

1133-
return array($fieldName, $value);
1134+
return [[$fieldName, $value]];
11341135
}
11351136

11361137
// No further processing for fields without a targetDocument mapping
@@ -1139,7 +1140,7 @@ private function prepareQueryElement($fieldName, $value = null, $class = null, $
11391140
$fieldName .= '.'.$nextObjectProperty;
11401141
}
11411142

1142-
return array($fieldName, $value);
1143+
return [[$fieldName, $value]];
11431144
}
11441145

11451146
$targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
@@ -1150,7 +1151,7 @@ private function prepareQueryElement($fieldName, $value = null, $class = null, $
11501151
$fieldName .= '.'.$nextObjectProperty;
11511152
}
11521153

1153-
return array($fieldName, $value);
1154+
return [[$fieldName, $value]];
11541155
}
11551156

11561157
$targetMapping = $targetClass->getFieldMapping($objectProperty);
@@ -1164,19 +1165,19 @@ private function prepareQueryElement($fieldName, $value = null, $class = null, $
11641165
// Process targetDocument identifier fields
11651166
if ($objectPropertyIsId) {
11661167
if ( ! $prepareValue) {
1167-
return array($fieldName, $value);
1168+
return [[$fieldName, $value]];
11681169
}
11691170

11701171
if ( ! is_array($value)) {
1171-
return array($fieldName, $targetClass->getDatabaseIdentifierValue($value));
1172+
return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
11721173
}
11731174

11741175
// Objects without operators or with DBRef fields can be converted immediately
11751176
if ( ! $this->hasQueryOperators($value) || $this->hasDBRefFields($value)) {
1176-
return array($fieldName, $targetClass->getDatabaseIdentifierValue($value));
1177+
return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
11771178
}
11781179

1179-
return array($fieldName, $this->prepareQueryExpression($value, $targetClass));
1180+
return [[$fieldName, $this->prepareQueryExpression($value, $targetClass)]];
11801181
}
11811182

11821183
/* The property path may include a third field segment, excluding the
@@ -1189,12 +1190,16 @@ private function prepareQueryElement($fieldName, $value = null, $class = null, $
11891190
? $this->dm->getClassMetadata($targetMapping['targetDocument'])
11901191
: null;
11911192

1192-
list($key, $value) = $this->prepareQueryElement($nextObjectProperty, $value, $nextTargetClass, $prepareValue);
1193+
$fieldNames = $this->prepareQueryElement($nextObjectProperty, $value, $nextTargetClass, $prepareValue);
1194+
1195+
return array_map(function ($preparedTuple) use ($fieldName) {
1196+
list($key, $value) = $preparedTuple;
11931197

1194-
$fieldName .= '.' . $key;
1198+
return [$fieldName . '.' . $key, $value];
1199+
}, $fieldNames);
11951200
}
11961201

1197-
return array($fieldName, $value);
1202+
return [[$fieldName, $value]];
11981203
}
11991204

12001205
/**
@@ -1390,4 +1395,35 @@ private function getWriteOptions(array $options = array())
13901395

13911396
return array_merge($defaultOptions, $documentOptions, $options);
13921397
}
1398+
1399+
/**
1400+
* @param string $fieldName
1401+
* @param mixed $value
1402+
* @param array $mapping
1403+
* @return array
1404+
*/
1405+
private function prepareDbRefElement($fieldName, $value, array $mapping)
1406+
{
1407+
$dbRef = $this->dm->createDBRef($value, $mapping);
1408+
$keys = ['$ref' => true, '$id' => true, '$db' => true];
1409+
if ($mapping['storeAs'] === ClassMetadataInfo::REFERENCE_STORE_AS_ID) {
1410+
unset($keys['$db']);
1411+
}
1412+
if (isset($mapping['targetDocument'])) {
1413+
unset($keys['$ref'], $keys['$db']);
1414+
}
1415+
1416+
if ($mapping['storeAs'] === ClassMetadataInfo::REFERENCE_STORE_AS_ID) {
1417+
return [[$fieldName, $dbRef]];
1418+
} elseif ($mapping['type'] === 'many') {
1419+
return [[$fieldName, ['$elemMatch' => array_intersect_key($dbRef, $keys)]]];
1420+
} else {
1421+
return array_map(
1422+
function ($key) use ($dbRef, $fieldName) {
1423+
return [$fieldName . '.' . $key, $dbRef[$key]];
1424+
},
1425+
array_keys($keys)
1426+
);
1427+
}
1428+
}
13931429
}

tests/Doctrine/ODM/MongoDB/Tests/DocumentRepositoryTest.php

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@
22

33
namespace Doctrine\ODM\MongoDB\Tests;
44

5+
use Doctrine\Common\Collections\ArrayCollection;
56
use Doctrine\Common\Collections\Criteria;
67
use Doctrine\ODM\MongoDB\LockMode;
78
use Documents\Account;
89
use Documents\Address;
10+
use Documents\Developer;
911
use Documents\Group;
1012
use Documents\Phonenumber;
13+
use Documents\SubProject;
1114
use Documents\User;
1215

1316
class DocumentRepositoryTest extends BaseTest
@@ -39,9 +42,58 @@ public function testFindByRefOneFull()
3942
$this->dm->persist($user);
4043
$this->dm->persist($account);
4144
$this->dm->flush();
45+
46+
$query = $this->dm
47+
->getUnitOfWork()
48+
->getDocumentPersister(User::class)
49+
->prepareQueryOrNewObj(['account' => $account]);
50+
$expectedQuery = ['account.$id' => new \MongoId($account->getId())];
51+
$this->assertEquals($expectedQuery, $query);
52+
4253
$this->assertSame($user, $this->dm->getRepository(User::class)->findOneBy(['account' => $account]));
4354
}
4455

56+
public function testFindByRefOneWithoutTargetDocumentFull()
57+
{
58+
$user = new User();
59+
$account = new Account('name');
60+
$user->setAccount($account);
61+
$this->dm->persist($user);
62+
$this->dm->persist($account);
63+
$this->dm->flush();
64+
65+
$query = $this->dm
66+
->getUnitOfWork()
67+
->getDocumentPersister(Account::class)
68+
->prepareQueryOrNewObj(['user' => $user]);
69+
$expectedQuery = [
70+
'user.$ref' => 'users',
71+
'user.$id' => new \MongoId($user->getId()),
72+
'user.$db' => DOCTRINE_MONGODB_DATABASE
73+
];
74+
$this->assertEquals($expectedQuery, $query);
75+
76+
$this->assertSame($account, $this->dm->getRepository(Account::class)->findOneBy(['user' => $user]));
77+
}
78+
79+
public function testFindDiscriminatedByRefManyFull()
80+
{
81+
$project = new SubProject('mongodb-odm');
82+
$developer = new Developer('alcaeus', new ArrayCollection([$project]));
83+
$this->dm->persist($project);
84+
$this->dm->persist($developer);
85+
$this->dm->flush();
86+
87+
$query = $this->dm
88+
->getUnitOfWork()
89+
->getDocumentPersister(Developer::class)
90+
->prepareQueryOrNewObj(['projects' => $project]);
91+
$expectedQuery = ['projects' => ['$elemMatch' => ['$id' => new \MongoId($project->getId())]]];
92+
$this->assertEquals($expectedQuery, $query);
93+
94+
$this->assertSame($developer, $this->dm->getRepository(Developer::class)->findOneBy(['projects' => $project]));
95+
}
96+
4597
public function testFindByRefOneSimple()
4698
{
4799
$user = new User();
@@ -72,6 +124,21 @@ public function testFindByRefManyFull()
72124
$this->dm->persist($user);
73125
$this->dm->persist($group);
74126
$this->dm->flush();
127+
128+
$query = $this->dm
129+
->getUnitOfWork()
130+
->getDocumentPersister(User::class)
131+
->prepareQueryOrNewObj(['groups' => $group]);
132+
133+
$expectedQuery = [
134+
'groups' => [
135+
'$elemMatch' => [
136+
'$id' => new \MongoId($group->getId()),
137+
],
138+
],
139+
];
140+
$this->assertEquals($expectedQuery, $query);
141+
75142
$this->assertSame($user, $this->dm->getRepository(User::class)->findOneBy(['groups' => $group]));
76143
}
77144

0 commit comments

Comments
 (0)