1
|
|
|
<?php |
2
|
|
|
/* |
3
|
|
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
4
|
|
|
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
5
|
|
|
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
6
|
|
|
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
7
|
|
|
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
8
|
|
|
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
9
|
|
|
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
10
|
|
|
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
11
|
|
|
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
12
|
|
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
13
|
|
|
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
14
|
|
|
* |
15
|
|
|
* This software consists of voluntary contributions made by many individuals |
16
|
|
|
* and is licensed under the MIT license. For more information, see |
17
|
|
|
* <http://www.doctrine-project.org>. |
18
|
|
|
*/ |
19
|
|
|
|
20
|
|
|
namespace Doctrine\ODM\MongoDB\Query; |
21
|
|
|
|
22
|
|
|
use Doctrine\ODM\MongoDB\DocumentManager; |
23
|
|
|
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; |
24
|
|
|
use Doctrine\ODM\MongoDB\Mapping\ClassMetadataInfo; |
25
|
|
|
use Doctrine\ODM\MongoDB\PersistentCollection; |
26
|
|
|
use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionInterface; |
27
|
|
|
use Doctrine\ODM\MongoDB\Proxy\Proxy; |
28
|
|
|
use Doctrine\ODM\MongoDB\UnitOfWork; |
29
|
|
|
|
30
|
|
|
/** |
31
|
|
|
* The ReferencePrimer is responsible for priming reference relationships. |
32
|
|
|
* |
33
|
|
|
* Priming a field mapped as either reference-one or reference-many will load |
34
|
|
|
* the referenced document(s) eagerly and avoid individual lazy loading through |
35
|
|
|
* proxy object initialization. |
36
|
|
|
* |
37
|
|
|
* Priming can only be used for the owning side side of a relationship, since |
38
|
|
|
* the referenced identifiers are not immediately available on an inverse side. |
39
|
|
|
* |
40
|
|
|
* @since 1.0 |
41
|
|
|
*/ |
42
|
|
|
class ReferencePrimer |
43
|
|
|
{ |
44
|
|
|
/** |
45
|
|
|
* The default primer Closure. |
46
|
|
|
* |
47
|
|
|
* @var \Closure |
48
|
|
|
*/ |
49
|
|
|
private $defaultPrimer; |
50
|
|
|
|
51
|
|
|
/** |
52
|
|
|
* The DocumentManager instance. |
53
|
|
|
* |
54
|
|
|
* @var DocumentManager $dm |
55
|
|
|
*/ |
56
|
|
|
private $dm; |
57
|
|
|
|
58
|
|
|
/** |
59
|
|
|
* The UnitOfWork instance. |
60
|
|
|
* |
61
|
|
|
* @var UnitOfWork |
62
|
|
|
*/ |
63
|
|
|
private $uow; |
64
|
|
|
|
65
|
|
|
/** |
66
|
|
|
* Initializes this instance with the specified document manager and unit of work. |
67
|
|
|
* |
68
|
|
|
* @param DocumentManager $dm Document manager. |
69
|
|
|
* @param UnitOfWork $uow Unit of work. |
70
|
|
|
*/ |
71
|
24 |
|
public function __construct(DocumentManager $dm, UnitOfWork $uow) |
72
|
|
|
{ |
73
|
24 |
|
$this->dm = $dm; |
74
|
24 |
|
$this->uow = $uow; |
75
|
|
|
|
76
|
14 |
|
$this->defaultPrimer = function(DocumentManager $dm, ClassMetadata $class, array $ids, array $hints) { |
77
|
14 |
|
$qb = $dm->createQueryBuilder($class->name) |
78
|
14 |
|
->field($class->identifier)->in($ids); |
79
|
|
|
|
80
|
14 |
|
if ( ! empty($hints[Query::HINT_SLAVE_OKAY])) { |
|
|
|
|
81
|
|
|
$qb->slaveOkay(true); |
|
|
|
|
82
|
|
|
} |
83
|
|
|
|
84
|
14 |
View Code Duplication |
if ( ! empty($hints[Query::HINT_READ_PREFERENCE])) { |
|
|
|
|
85
|
|
|
$qb->setReadPreference($hints[Query::HINT_READ_PREFERENCE], $hints[Query::HINT_READ_PREFERENCE_TAGS]); |
86
|
|
|
} |
87
|
|
|
|
88
|
14 |
|
$qb->getQuery()->execute()->toArray(false); |
89
|
14 |
|
}; |
90
|
24 |
|
} |
91
|
|
|
|
92
|
|
|
|
93
|
|
|
/** |
94
|
|
|
* Prime references within a mapped field of one or more documents. |
95
|
|
|
* |
96
|
|
|
* If a $primer callable is provided, it should have the same signature as |
97
|
|
|
* the default primer defined in the constructor. If $primer is not |
98
|
|
|
* callable, the default primer will be used. |
99
|
|
|
* |
100
|
|
|
* @param ClassMetadata $class Class metadata for the document |
101
|
|
|
* @param array|\Traversable $documents Documents containing references to prime |
102
|
|
|
* @param string $fieldName Field name containing references to prime |
103
|
|
|
* @param array $hints UnitOfWork hints for priming queries |
104
|
|
|
* @param callable $primer Optional primer callable |
105
|
|
|
* @throws \InvalidArgumentException If the mapped field is not the owning |
106
|
|
|
* side of a reference relationship. |
107
|
|
|
* @throws \InvalidArgumentException If $primer is not callable |
108
|
|
|
* @throws \LogicException If the mapped field is a simple reference and is |
109
|
|
|
* missing a target document class. |
110
|
|
|
*/ |
111
|
21 |
|
public function primeReferences(ClassMetadata $class, $documents, $fieldName, array $hints = array(), $primer = null) |
112
|
|
|
{ |
113
|
21 |
|
$data = $this->parseDotSyntaxForPrimer($fieldName, $class, $documents); |
114
|
20 |
|
$mapping = $data['mapping']; |
115
|
20 |
|
$fieldName = $data['fieldName']; |
116
|
20 |
|
$class = $data['class']; |
117
|
20 |
|
$documents = $data['documents']; |
118
|
|
|
|
119
|
|
|
/* Inverse-side references would need to be populated before we can |
120
|
|
|
* collect references to be primed. This is not supported. |
121
|
|
|
*/ |
122
|
20 |
|
if ( ! isset($mapping['reference']) || ! $mapping['isOwningSide']) { |
123
|
1 |
|
throw new \InvalidArgumentException(sprintf('Field "%s" is not the owning side of a reference relationship in class "%s"', $fieldName, $class->name)); |
124
|
|
|
} |
125
|
|
|
|
126
|
|
|
/* Simple reference require a target document class so we can construct |
127
|
|
|
* the priming query. |
128
|
|
|
*/ |
129
|
19 |
|
if ($mapping['storeAs'] === ClassMetadataInfo::REFERENCE_STORE_AS_ID && empty($mapping['targetDocument'])) { |
130
|
|
|
throw new \LogicException(sprintf('Field "%s" is a simple reference without a target document class in class "%s"', $fieldName, $class->name)); |
131
|
|
|
} |
132
|
|
|
|
133
|
19 |
|
if ($primer !== null && ! is_callable($primer)) { |
134
|
|
|
throw new \InvalidArgumentException('$primer is not callable'); |
135
|
|
|
} |
136
|
|
|
|
137
|
19 |
|
$primer = $primer ?: $this->defaultPrimer; |
138
|
19 |
|
$groupedIds = array(); |
139
|
|
|
|
140
|
|
|
/* @var $document PersistentCollectionInterface */ |
141
|
19 |
|
foreach ($documents as $document) { |
142
|
19 |
|
$fieldValue = $class->getFieldValue($document, $fieldName); |
143
|
|
|
|
144
|
|
|
/* The field will need to be either a Proxy (reference-one) or |
145
|
|
|
* PersistentCollection (reference-many) in order to prime anything. |
146
|
|
|
*/ |
147
|
19 |
|
if ( ! is_object($fieldValue)) { |
148
|
2 |
|
continue; |
149
|
|
|
} |
150
|
|
|
|
151
|
17 |
|
if ($mapping['type'] === 'one' && $fieldValue instanceof Proxy && ! $fieldValue->__isInitialized()) { |
152
|
12 |
|
$refClass = $this->dm->getClassMetadata(get_class($fieldValue)); |
153
|
12 |
|
$id = $this->uow->getDocumentIdentifier($fieldValue); |
154
|
12 |
|
$groupedIds[$refClass->name][serialize($id)] = $id; |
155
|
10 |
|
} elseif ($mapping['type'] == 'many' && $fieldValue instanceof PersistentCollectionInterface) { |
156
|
17 |
|
$this->addManyReferences($fieldValue, $groupedIds); |
157
|
|
|
} |
158
|
|
|
} |
159
|
|
|
|
160
|
19 |
|
foreach ($groupedIds as $className => $ids) { |
161
|
16 |
|
$refClass = $this->dm->getClassMetadata($className); |
162
|
16 |
|
call_user_func($primer, $this->dm, $refClass, array_values($ids), $hints); |
163
|
|
|
} |
164
|
19 |
|
} |
165
|
|
|
|
166
|
|
|
/** |
167
|
|
|
* If you are priming references inside an embedded document you'll need to parse the dot syntax. |
168
|
|
|
* This method will traverse through embedded documents to find the reference to prime. |
169
|
|
|
* However this method will not traverse through multiple layers of references. |
170
|
|
|
* I.e. you can prime this: myDocument.embeddedDocument.embeddedDocuments.embeddedDocuments.referencedDocument(s) |
171
|
|
|
* ... but you cannot prime this: myDocument.embeddedDocument.referencedDocuments.referencedDocument(s) |
172
|
|
|
* This addresses Issue #624. |
173
|
|
|
* |
174
|
|
|
* @param string $fieldName |
175
|
|
|
* @param ClassMetadata $class |
176
|
|
|
* @param array|\Traversable $documents |
177
|
|
|
* @param array $mapping |
178
|
|
|
* @return array |
179
|
|
|
*/ |
180
|
21 |
|
private function parseDotSyntaxForPrimer($fieldName, $class, $documents, $mapping = null) |
181
|
|
|
{ |
182
|
|
|
// Recursion passthrough: |
183
|
21 |
|
if ($mapping != null) { |
184
|
|
|
return array('fieldName' => $fieldName, 'class' => $class, 'documents' => $documents, 'mapping' => $mapping); |
185
|
|
|
} |
186
|
|
|
|
187
|
|
|
// Gather mapping data: |
188
|
21 |
|
$e = explode('.', $fieldName); |
189
|
|
|
|
190
|
21 |
|
if ( ! isset($class->fieldMappings[$e[0]])) { |
191
|
|
|
throw new \InvalidArgumentException(sprintf('Field %s cannot be further parsed for priming because it is unmapped.', $fieldName)); |
192
|
|
|
} |
193
|
|
|
|
194
|
21 |
|
$mapping = $class->fieldMappings[$e[0]]; |
195
|
21 |
|
$e[0] = $mapping['fieldName']; |
196
|
|
|
|
197
|
|
|
// Case of embedded document(s) to recurse through: |
198
|
21 |
|
if ( ! isset($mapping['reference'])) { |
199
|
4 |
|
if (empty($mapping['embedded'])) { |
200
|
1 |
|
throw new \InvalidArgumentException(sprintf('Field "%s" of fieldName "%s" is not an embedded document, therefore no children can be primed. Aborting. This feature does not support traversing nested referenced documents at this time.', $e[0], $fieldName)); |
201
|
|
|
} |
202
|
|
|
|
203
|
3 |
|
if ( ! isset($mapping['targetDocument'])) { |
204
|
|
|
throw new \InvalidArgumentException(sprintf('No target document class has been specified for this embedded document. However, targetDocument mapping must be specified in order for prime to work on fieldName "%s" for mapping of field "%s".', $fieldName, $mapping['fieldName'])); |
205
|
|
|
} |
206
|
|
|
|
207
|
3 |
|
$childDocuments = array(); |
208
|
|
|
|
209
|
3 |
|
foreach ($documents as $document) { |
210
|
3 |
|
$fieldValue = $class->getFieldValue($document, $e[0]); |
211
|
|
|
|
212
|
3 |
|
if ($fieldValue instanceof PersistentCollectionInterface) { |
213
|
3 |
|
foreach ($fieldValue as $elemDocument) { |
214
|
3 |
|
array_push($childDocuments, $elemDocument); |
215
|
|
|
} |
216
|
|
|
} else { |
217
|
3 |
|
array_push($childDocuments,$fieldValue); |
218
|
|
|
} |
219
|
|
|
} |
220
|
|
|
|
221
|
3 |
|
array_shift($e); |
222
|
|
|
|
223
|
3 |
|
$childClass = $this->dm->getClassMetadata($mapping['targetDocument']); |
224
|
|
|
|
225
|
3 |
|
if ( ! $childClass->hasField($e[0])) { |
226
|
|
|
throw new \InvalidArgumentException(sprintf('Field to prime must exist in embedded target document. Reference fieldName "%s" for mapping of target document class "%s".', $fieldName, $mapping['targetDocument'])); |
227
|
|
|
} |
228
|
|
|
|
229
|
3 |
|
$childFieldName = implode('.',$e); |
230
|
|
|
|
231
|
3 |
|
return $this->parseDotSyntaxForPrimer($childFieldName, $childClass, $childDocuments); |
232
|
|
|
} |
233
|
|
|
|
234
|
|
|
// Case of reference(s) to prime: |
235
|
20 |
|
if ($mapping['reference']) { |
236
|
20 |
|
if (count($e) > 1) { |
237
|
|
|
throw new \InvalidArgumentException(sprintf('Cannot prime more than one layer deep but field "%s" is a reference and has children in fieldName "%s".', $e[0], $fieldName)); |
238
|
|
|
} |
239
|
|
|
|
240
|
20 |
|
return array('fieldName' => $fieldName, 'class' => $class, 'documents' => $documents, 'mapping' => $mapping); |
241
|
|
|
} |
242
|
|
|
} |
243
|
|
|
|
244
|
|
|
/** |
245
|
|
|
* Adds identifiers from a PersistentCollection to $groupedIds. |
246
|
|
|
* |
247
|
|
|
* If the relation contains simple references, the mapping is assumed to |
248
|
|
|
* have a target document class defined. Without that, there is no way to |
249
|
|
|
* infer the class of the referenced documents. |
250
|
|
|
* |
251
|
|
|
* @param PersistentCollectionInterface $persistentCollection |
252
|
|
|
* @param array $groupedIds |
253
|
|
|
*/ |
254
|
10 |
|
private function addManyReferences(PersistentCollectionInterface $persistentCollection, array &$groupedIds) |
255
|
|
|
{ |
256
|
10 |
|
$mapping = $persistentCollection->getMapping(); |
257
|
|
|
|
258
|
10 |
View Code Duplication |
if ($mapping['storeAs'] === ClassMetadataInfo::REFERENCE_STORE_AS_ID) { |
|
|
|
|
259
|
2 |
|
$className = $mapping['targetDocument']; |
260
|
2 |
|
$class = $this->dm->getClassMetadata($className); |
261
|
|
|
} |
262
|
|
|
|
263
|
10 |
|
foreach ($persistentCollection->getMongoData() as $reference) { |
264
|
10 |
|
$id = ClassMetadataInfo::getReferenceId($reference, $mapping['storeAs']); |
265
|
|
|
|
266
|
10 |
View Code Duplication |
if ($mapping['storeAs'] !== ClassMetadataInfo::REFERENCE_STORE_AS_ID) { |
|
|
|
|
267
|
9 |
|
$className = $this->uow->getClassNameForAssociation($mapping, $reference); |
268
|
9 |
|
$class = $this->dm->getClassMetadata($className); |
269
|
|
|
} |
270
|
|
|
|
271
|
10 |
|
$document = $this->uow->tryGetById($id, $class); |
|
|
|
|
272
|
|
|
|
273
|
10 |
|
if ( ! $document || ($document instanceof Proxy && ! $document->__isInitialized())) { |
274
|
9 |
|
$id = $class->getPHPIdentifierValue($id); |
275
|
10 |
|
$groupedIds[$className][serialize($id)] = $id; |
|
|
|
|
276
|
|
|
} |
277
|
|
|
} |
278
|
10 |
|
} |
279
|
|
|
} |
280
|
|
|
|
This class constant has been deprecated.