These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | |||
3 | namespace Doctrine\ODM\MongoDB\Persisters; |
||
4 | |||
5 | use Doctrine\Common\EventManager; |
||
6 | use Doctrine\Common\Persistence\Mapping\MappingException; |
||
7 | use Doctrine\ODM\MongoDB\DocumentManager; |
||
8 | use Doctrine\ODM\MongoDB\Iterator\CachingIterator; |
||
9 | use Doctrine\ODM\MongoDB\Iterator\HydratingIterator; |
||
10 | use Doctrine\ODM\MongoDB\Iterator\Iterator; |
||
11 | use Doctrine\ODM\MongoDB\Iterator\PrimingIterator; |
||
12 | use Doctrine\ODM\MongoDB\Query\ReferencePrimer; |
||
13 | use Doctrine\ODM\MongoDB\Utility\CollectionHelper; |
||
14 | use Doctrine\ODM\MongoDB\Hydrator\HydratorFactory; |
||
15 | use Doctrine\ODM\MongoDB\LockException; |
||
16 | use Doctrine\ODM\MongoDB\LockMode; |
||
17 | use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; |
||
18 | use Doctrine\ODM\MongoDB\MongoDBException; |
||
19 | use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionInterface; |
||
20 | use Doctrine\ODM\MongoDB\Proxy\Proxy; |
||
21 | use Doctrine\ODM\MongoDB\Query\CriteriaMerger; |
||
22 | use Doctrine\ODM\MongoDB\Query\Query; |
||
23 | use Doctrine\ODM\MongoDB\Types\Type; |
||
24 | use Doctrine\ODM\MongoDB\UnitOfWork; |
||
25 | use MongoDB\Collection; |
||
26 | use MongoDB\Driver\Cursor; |
||
27 | use MongoDB\Driver\Exception\Exception as DriverException; |
||
28 | |||
29 | /** |
||
30 | * The DocumentPersister is responsible for persisting documents. |
||
31 | * |
||
32 | * @since 1.0 |
||
33 | */ |
||
34 | class DocumentPersister |
||
35 | { |
||
36 | /** |
||
37 | * The PersistenceBuilder instance. |
||
38 | * |
||
39 | * @var PersistenceBuilder |
||
40 | */ |
||
41 | private $pb; |
||
42 | |||
43 | /** |
||
44 | * The DocumentManager instance. |
||
45 | * |
||
46 | * @var DocumentManager |
||
47 | */ |
||
48 | private $dm; |
||
49 | |||
50 | /** |
||
51 | * The EventManager instance |
||
52 | * |
||
53 | * @var EventManager |
||
54 | */ |
||
55 | private $evm; |
||
56 | |||
57 | /** |
||
58 | * The UnitOfWork instance. |
||
59 | * |
||
60 | * @var UnitOfWork |
||
61 | */ |
||
62 | private $uow; |
||
63 | |||
64 | /** |
||
65 | * The ClassMetadata instance for the document type being persisted. |
||
66 | * |
||
67 | * @var ClassMetadata |
||
68 | */ |
||
69 | private $class; |
||
70 | |||
71 | /** |
||
72 | * The MongoCollection instance for this document. |
||
73 | * |
||
74 | * @var Collection |
||
75 | */ |
||
76 | private $collection; |
||
77 | |||
78 | /** |
||
79 | * Array of queued inserts for the persister to insert. |
||
80 | * |
||
81 | * @var array |
||
82 | */ |
||
83 | private $queuedInserts = array(); |
||
84 | |||
85 | /** |
||
86 | * Array of queued inserts for the persister to insert. |
||
87 | * |
||
88 | * @var array |
||
89 | */ |
||
90 | private $queuedUpserts = array(); |
||
91 | |||
92 | /** |
||
93 | * The CriteriaMerger instance. |
||
94 | * |
||
95 | * @var CriteriaMerger |
||
96 | */ |
||
97 | private $cm; |
||
98 | |||
99 | /** |
||
100 | * The CollectionPersister instance. |
||
101 | * |
||
102 | * @var CollectionPersister |
||
103 | */ |
||
104 | private $cp; |
||
105 | |||
106 | /** |
||
107 | * Initializes this instance. |
||
108 | * |
||
109 | * @param PersistenceBuilder $pb |
||
110 | * @param DocumentManager $dm |
||
111 | * @param EventManager $evm |
||
112 | * @param UnitOfWork $uow |
||
113 | * @param HydratorFactory $hydratorFactory |
||
114 | * @param ClassMetadata $class |
||
115 | * @param CriteriaMerger $cm |
||
116 | */ |
||
117 | 1074 | public function __construct( |
|
118 | PersistenceBuilder $pb, |
||
119 | DocumentManager $dm, |
||
120 | EventManager $evm, |
||
121 | UnitOfWork $uow, |
||
122 | HydratorFactory $hydratorFactory, |
||
123 | ClassMetadata $class, |
||
124 | CriteriaMerger $cm = null |
||
125 | ) { |
||
126 | 1074 | $this->pb = $pb; |
|
127 | 1074 | $this->dm = $dm; |
|
128 | 1074 | $this->evm = $evm; |
|
129 | 1074 | $this->cm = $cm ?: new CriteriaMerger(); |
|
130 | 1074 | $this->uow = $uow; |
|
131 | 1074 | $this->hydratorFactory = $hydratorFactory; |
|
132 | 1074 | $this->class = $class; |
|
133 | 1074 | $this->collection = $dm->getDocumentCollection($class->name); |
|
134 | 1074 | $this->cp = $this->uow->getCollectionPersister(); |
|
135 | 1074 | } |
|
136 | |||
137 | /** |
||
138 | * @return array |
||
139 | */ |
||
140 | public function getInserts() |
||
141 | { |
||
142 | return $this->queuedInserts; |
||
143 | } |
||
144 | |||
145 | /** |
||
146 | * @param object $document |
||
147 | * @return bool |
||
148 | */ |
||
149 | public function isQueuedForInsert($document) |
||
150 | { |
||
151 | return isset($this->queuedInserts[spl_object_hash($document)]); |
||
152 | } |
||
153 | |||
154 | /** |
||
155 | * Adds a document to the queued insertions. |
||
156 | * The document remains queued until {@link executeInserts} is invoked. |
||
157 | * |
||
158 | * @param object $document The document to queue for insertion. |
||
159 | */ |
||
160 | 476 | public function addInsert($document) |
|
161 | { |
||
162 | 476 | $this->queuedInserts[spl_object_hash($document)] = $document; |
|
163 | 476 | } |
|
164 | |||
165 | /** |
||
166 | * @return array |
||
167 | */ |
||
168 | public function getUpserts() |
||
169 | { |
||
170 | return $this->queuedUpserts; |
||
171 | } |
||
172 | |||
173 | /** |
||
174 | * @param object $document |
||
175 | * @return boolean |
||
176 | */ |
||
177 | public function isQueuedForUpsert($document) |
||
178 | { |
||
179 | return isset($this->queuedUpserts[spl_object_hash($document)]); |
||
180 | } |
||
181 | |||
182 | /** |
||
183 | * Adds a document to the queued upserts. |
||
184 | * The document remains queued until {@link executeUpserts} is invoked. |
||
185 | * |
||
186 | * @param object $document The document to queue for insertion. |
||
187 | */ |
||
188 | 84 | public function addUpsert($document) |
|
189 | { |
||
190 | 84 | $this->queuedUpserts[spl_object_hash($document)] = $document; |
|
191 | 84 | } |
|
192 | |||
193 | /** |
||
194 | * Gets the ClassMetadata instance of the document class this persister is used for. |
||
195 | * |
||
196 | * @return ClassMetadata |
||
197 | */ |
||
198 | public function getClassMetadata() |
||
199 | { |
||
200 | return $this->class; |
||
201 | } |
||
202 | |||
203 | /** |
||
204 | * Executes all queued document insertions. |
||
205 | * |
||
206 | * Queued documents without an ID will inserted in a batch and queued |
||
207 | * documents with an ID will be upserted individually. |
||
208 | * |
||
209 | * If no inserts are queued, invoking this method is a NOOP. |
||
210 | * |
||
211 | * @param array $options Options for batchInsert() and update() driver methods |
||
212 | */ |
||
213 | 476 | public function executeInserts(array $options = array()) |
|
214 | { |
||
215 | 476 | if ( ! $this->queuedInserts) { |
|
216 | return; |
||
217 | } |
||
218 | |||
219 | 476 | $inserts = array(); |
|
220 | 476 | $options = $this->getWriteOptions($options); |
|
221 | 476 | foreach ($this->queuedInserts as $oid => $document) { |
|
222 | 476 | $data = $this->pb->prepareInsertData($document); |
|
223 | |||
224 | // Set the initial version for each insert |
||
225 | 475 | View Code Duplication | if ($this->class->isVersioned) { |
0 ignored issues
–
show
|
|||
226 | 20 | $versionMapping = $this->class->fieldMappings[$this->class->versionField]; |
|
227 | 20 | if ($versionMapping['type'] === 'int') { |
|
228 | 18 | $nextVersion = max(1, (int) $this->class->reflFields[$this->class->versionField]->getValue($document)); |
|
229 | 18 | $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion); |
|
230 | 2 | } elseif ($versionMapping['type'] === 'date') { |
|
231 | 2 | $nextVersionDateTime = new \DateTime(); |
|
232 | 2 | $nextVersion = Type::convertPHPToDatabaseValue($nextVersionDateTime); |
|
233 | 2 | $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersionDateTime); |
|
234 | } |
||
235 | 20 | $data[$versionMapping['name']] = $nextVersion; |
|
236 | } |
||
237 | |||
238 | 475 | $inserts[] = $data; |
|
239 | } |
||
240 | |||
241 | 475 | if ($inserts) { |
|
242 | try { |
||
243 | 475 | $this->collection->insertMany($inserts, $options); |
|
244 | 6 | } catch (DriverException $e) { |
|
245 | 6 | $this->queuedInserts = array(); |
|
246 | 6 | throw $e; |
|
247 | } |
||
248 | } |
||
249 | |||
250 | /* All collections except for ones using addToSet have already been |
||
251 | * saved. We have left these to be handled separately to avoid checking |
||
252 | * collection for uniqueness on PHP side. |
||
253 | */ |
||
254 | 475 | foreach ($this->queuedInserts as $document) { |
|
255 | 475 | $this->handleCollections($document, $options); |
|
256 | } |
||
257 | |||
258 | 475 | $this->queuedInserts = array(); |
|
259 | 475 | } |
|
260 | |||
261 | /** |
||
262 | * Executes all queued document upserts. |
||
263 | * |
||
264 | * Queued documents with an ID are upserted individually. |
||
265 | * |
||
266 | * If no upserts are queued, invoking this method is a NOOP. |
||
267 | * |
||
268 | * @param array $options Options for batchInsert() and update() driver methods |
||
269 | */ |
||
270 | 84 | public function executeUpserts(array $options = array()) |
|
271 | { |
||
272 | 84 | if ( ! $this->queuedUpserts) { |
|
273 | return; |
||
274 | } |
||
275 | |||
276 | 84 | $options = $this->getWriteOptions($options); |
|
277 | 84 | foreach ($this->queuedUpserts as $oid => $document) { |
|
278 | try { |
||
279 | 84 | $this->executeUpsert($document, $options); |
|
280 | 84 | $this->handleCollections($document, $options); |
|
281 | 84 | unset($this->queuedUpserts[$oid]); |
|
282 | } catch (\MongoException $e) { |
||
283 | unset($this->queuedUpserts[$oid]); |
||
284 | 84 | throw $e; |
|
285 | } |
||
286 | } |
||
287 | 84 | } |
|
288 | |||
289 | /** |
||
290 | * Executes a single upsert in {@link executeUpserts} |
||
291 | * |
||
292 | * @param object $document |
||
293 | * @param array $options |
||
294 | */ |
||
295 | 84 | private function executeUpsert($document, array $options) |
|
296 | { |
||
297 | 84 | $options['upsert'] = true; |
|
298 | 84 | $criteria = $this->getQueryForDocument($document); |
|
299 | |||
300 | 84 | $data = $this->pb->prepareUpsertData($document); |
|
301 | |||
302 | // Set the initial version for each upsert |
||
303 | 84 | View Code Duplication | if ($this->class->isVersioned) { |
0 ignored issues
–
show
This code seems to be duplicated across your project.
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation. You can also find more detailed suggestions in the “Code” section of your repository.
Loading history...
|
|||
304 | 2 | $versionMapping = $this->class->fieldMappings[$this->class->versionField]; |
|
305 | 2 | if ($versionMapping['type'] === 'int') { |
|
306 | 1 | $nextVersion = max(1, (int) $this->class->reflFields[$this->class->versionField]->getValue($document)); |
|
307 | 1 | $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion); |
|
308 | 1 | } elseif ($versionMapping['type'] === 'date') { |
|
309 | 1 | $nextVersionDateTime = new \DateTime(); |
|
310 | 1 | $nextVersion = Type::convertPHPToDatabaseValue($nextVersionDateTime); |
|
311 | 1 | $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersionDateTime); |
|
312 | } |
||
313 | 2 | $data['$set'][$versionMapping['name']] = $nextVersion; |
|
314 | } |
||
315 | |||
316 | 84 | foreach (array_keys($criteria) as $field) { |
|
317 | 84 | unset($data['$set'][$field]); |
|
318 | } |
||
319 | |||
320 | // Do not send an empty $set modifier |
||
321 | 84 | if (empty($data['$set'])) { |
|
322 | 17 | unset($data['$set']); |
|
323 | } |
||
324 | |||
325 | /* If there are no modifiers remaining, we're upserting a document with |
||
326 | * an identifier as its only field. Since a document with the identifier |
||
327 | * may already exist, the desired behavior is "insert if not exists" and |
||
328 | * NOOP otherwise. MongoDB 2.6+ does not allow empty modifiers, so $set |
||
329 | * the identifier to the same value in our criteria. |
||
330 | * |
||
331 | * This will fail for versions before MongoDB 2.6, which require an |
||
332 | * empty $set modifier. The best we can do (without attempting to check |
||
333 | * server versions in advance) is attempt the 2.6+ behavior and retry |
||
334 | * after the relevant exception. |
||
335 | * |
||
336 | * See: https://jira.mongodb.org/browse/SERVER-12266 |
||
337 | */ |
||
338 | 84 | if (empty($data)) { |
|
339 | 17 | $retry = true; |
|
340 | 17 | $data = array('$set' => array('_id' => $criteria['_id'])); |
|
341 | } |
||
342 | |||
343 | try { |
||
344 | 84 | $this->collection->updateOne($criteria, $data, $options); |
|
345 | 84 | return; |
|
346 | } catch (\MongoCursorException $e) { |
||
347 | if (empty($retry) || strpos($e->getMessage(), 'Mod on _id not allowed') === false) { |
||
348 | throw $e; |
||
349 | } |
||
350 | } |
||
351 | |||
352 | $this->collection->updateOne($criteria, array('$set' => new \stdClass), $options); |
||
353 | } |
||
354 | |||
355 | /** |
||
356 | * Updates the already persisted document if it has any new changesets. |
||
357 | * |
||
358 | * @param object $document |
||
359 | * @param array $options Array of options to be used with update() |
||
360 | * @throws \Doctrine\ODM\MongoDB\LockException |
||
361 | */ |
||
362 | 195 | public function update($document, array $options = array()) |
|
363 | { |
||
364 | 195 | $update = $this->pb->prepareUpdateData($document); |
|
365 | |||
366 | 195 | $query = $this->getQueryForDocument($document); |
|
367 | |||
368 | 195 | foreach (array_keys($query) as $field) { |
|
369 | 195 | unset($update['$set'][$field]); |
|
370 | } |
||
371 | |||
372 | 195 | if (empty($update['$set'])) { |
|
373 | 89 | unset($update['$set']); |
|
374 | } |
||
375 | |||
376 | |||
377 | // Include versioning logic to set the new version value in the database |
||
378 | // and to ensure the version has not changed since this document object instance |
||
379 | // was fetched from the database |
||
380 | 195 | $nextVersion = null; |
|
381 | 195 | if ($this->class->isVersioned) { |
|
382 | 13 | $versionMapping = $this->class->fieldMappings[$this->class->versionField]; |
|
383 | 13 | $currentVersion = $this->class->reflFields[$this->class->versionField]->getValue($document); |
|
384 | 13 | if ($versionMapping['type'] === 'int') { |
|
385 | 10 | $nextVersion = $currentVersion + 1; |
|
386 | 10 | $update['$inc'][$versionMapping['name']] = 1; |
|
387 | 10 | $query[$versionMapping['name']] = $currentVersion; |
|
388 | 3 | } elseif ($versionMapping['type'] === 'date') { |
|
389 | 3 | $nextVersion = new \DateTime(); |
|
390 | 3 | $update['$set'][$versionMapping['name']] = Type::convertPHPToDatabaseValue($nextVersion); |
|
391 | 3 | $query[$versionMapping['name']] = Type::convertPHPToDatabaseValue($currentVersion); |
|
392 | } |
||
393 | } |
||
394 | |||
395 | 195 | if ( ! empty($update)) { |
|
396 | // Include locking logic so that if the document object in memory is currently |
||
397 | // locked then it will remove it, otherwise it ensures the document is not locked. |
||
398 | 129 | if ($this->class->isLockable) { |
|
399 | 11 | $isLocked = $this->class->reflFields[$this->class->lockField]->getValue($document); |
|
400 | 11 | $lockMapping = $this->class->fieldMappings[$this->class->lockField]; |
|
401 | 11 | if ($isLocked) { |
|
402 | 2 | $update['$unset'] = array($lockMapping['name'] => true); |
|
403 | } else { |
||
404 | 9 | $query[$lockMapping['name']] = array('$exists' => false); |
|
405 | } |
||
406 | } |
||
407 | |||
408 | 129 | $options = $this->getWriteOptions($options); |
|
409 | |||
410 | 129 | $result = $this->collection->updateOne($query, $update, $options); |
|
411 | |||
412 | 129 | if (($this->class->isVersioned || $this->class->isLockable) && $result->getModifiedCount() !== 1) { |
|
413 | 4 | throw LockException::lockFailed($document); |
|
414 | 125 | } elseif ($this->class->isVersioned) { |
|
415 | 9 | $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion); |
|
416 | } |
||
417 | } |
||
418 | |||
419 | 191 | $this->handleCollections($document, $options); |
|
420 | 191 | } |
|
421 | |||
422 | /** |
||
423 | * Removes document from mongo |
||
424 | * |
||
425 | * @param mixed $document |
||
426 | * @param array $options Array of options to be used with remove() |
||
427 | * @throws \Doctrine\ODM\MongoDB\LockException |
||
428 | */ |
||
429 | 32 | public function delete($document, array $options = array()) |
|
430 | { |
||
431 | 32 | $query = $this->getQueryForDocument($document); |
|
432 | |||
433 | 32 | if ($this->class->isLockable) { |
|
434 | 2 | $query[$this->class->lockField] = array('$exists' => false); |
|
435 | } |
||
436 | |||
437 | 32 | $options = $this->getWriteOptions($options); |
|
438 | |||
439 | 32 | $result = $this->collection->deleteOne($query, $options); |
|
440 | |||
441 | 32 | if (($this->class->isVersioned || $this->class->isLockable) && ! $result->getDeletedCount()) { |
|
442 | 2 | throw LockException::lockFailed($document); |
|
443 | } |
||
444 | 30 | } |
|
445 | |||
446 | /** |
||
447 | * Refreshes a managed document. |
||
448 | * |
||
449 | * @param object $document The document to refresh. |
||
450 | */ |
||
451 | 20 | public function refresh($document) |
|
452 | { |
||
453 | 20 | $query = $this->getQueryForDocument($document); |
|
454 | 20 | $data = $this->collection->findOne($query); |
|
455 | 20 | $data = $this->hydratorFactory->hydrate($document, $data); |
|
456 | 20 | $this->uow->setOriginalDocumentData($document, $data); |
|
457 | 20 | } |
|
458 | |||
459 | /** |
||
460 | * Finds a document by a set of criteria. |
||
461 | * |
||
462 | * If a scalar or MongoDB\BSON\ObjectId is provided for $criteria, it will |
||
463 | * be used to match an _id value. |
||
464 | * |
||
465 | * @param mixed $criteria Query criteria |
||
466 | * @param object $document Document to load the data into. If not specified, a new document is created. |
||
467 | * @param array $hints Hints for document creation |
||
468 | * @param integer $lockMode |
||
469 | * @param array $sort Sort array for Cursor::sort() |
||
470 | * @throws \Doctrine\ODM\MongoDB\LockException |
||
471 | * @return object|null The loaded and managed document instance or null if no document was found |
||
472 | * @todo Check identity map? loadById method? Try to guess whether $criteria is the id? |
||
473 | */ |
||
474 | 342 | public function load($criteria, $document = null, array $hints = array(), $lockMode = 0, array $sort = null) |
|
475 | { |
||
476 | // TODO: remove this |
||
477 | 342 | if ($criteria === null || is_scalar($criteria) || $criteria instanceof \MongoDB\BSON\ObjectId) { |
|
478 | $criteria = array('_id' => $criteria); |
||
479 | } |
||
480 | |||
481 | 342 | $criteria = $this->prepareQueryOrNewObj($criteria); |
|
482 | 342 | $criteria = $this->addDiscriminatorToPreparedQuery($criteria); |
|
483 | 342 | $criteria = $this->addFilterToPreparedQuery($criteria); |
|
484 | |||
485 | 342 | $options = []; |
|
486 | 342 | if (null !== $sort) { |
|
487 | 92 | $options['sort'] = $this->prepareSort($sort); |
|
488 | } |
||
489 | 342 | $result = $this->collection->findOne($criteria, $options); |
|
490 | |||
491 | 342 | if ($this->class->isLockable) { |
|
492 | 1 | $lockMapping = $this->class->fieldMappings[$this->class->lockField]; |
|
493 | 1 | if (isset($result[$lockMapping['name']]) && $result[$lockMapping['name']] === LockMode::PESSIMISTIC_WRITE) { |
|
494 | 1 | throw LockException::lockFailed($result); |
|
495 | } |
||
496 | } |
||
497 | |||
498 | 341 | return $this->createDocument($result, $document, $hints); |
|
499 | } |
||
500 | |||
501 | /** |
||
502 | * Finds documents by a set of criteria. |
||
503 | * |
||
504 | * @param array $criteria Query criteria |
||
505 | * @param array $sort Sort array for Cursor::sort() |
||
506 | * @param integer|null $limit Limit for Cursor::limit() |
||
507 | * @param integer|null $skip Skip for Cursor::skip() |
||
508 | * @return Iterator |
||
509 | */ |
||
510 | 22 | public function loadAll(array $criteria = array(), array $sort = null, $limit = null, $skip = null) |
|
511 | { |
||
512 | 22 | $criteria = $this->prepareQueryOrNewObj($criteria); |
|
513 | 22 | $criteria = $this->addDiscriminatorToPreparedQuery($criteria); |
|
514 | 22 | $criteria = $this->addFilterToPreparedQuery($criteria); |
|
515 | |||
516 | 22 | $options = []; |
|
517 | 22 | if (null !== $sort) { |
|
518 | 11 | $options['sort'] = $this->prepareSort($sort); |
|
519 | } |
||
520 | |||
521 | 22 | if (null !== $limit) { |
|
522 | 10 | $options['limit'] = $limit; |
|
523 | } |
||
524 | |||
525 | 22 | if (null !== $skip) { |
|
526 | 1 | $options['skip'] = $skip; |
|
527 | } |
||
528 | |||
529 | 22 | $baseCursor = $this->collection->find($criteria, $options); |
|
530 | 22 | $cursor = $this->wrapCursor($baseCursor); |
|
531 | |||
532 | 22 | return $cursor; |
|
533 | } |
||
534 | |||
535 | /** |
||
536 | * @param object $document |
||
537 | * |
||
538 | * @return array |
||
539 | * @throws MongoDBException |
||
540 | */ |
||
541 | 268 | private function getShardKeyQuery($document) |
|
542 | { |
||
543 | 268 | if ( ! $this->class->isSharded()) { |
|
544 | 264 | return array(); |
|
545 | } |
||
546 | |||
547 | 4 | $shardKey = $this->class->getShardKey(); |
|
548 | 4 | $keys = array_keys($shardKey['keys']); |
|
549 | 4 | $data = $this->uow->getDocumentActualData($document); |
|
550 | |||
551 | 4 | $shardKeyQueryPart = array(); |
|
552 | 4 | foreach ($keys as $key) { |
|
553 | 4 | $mapping = $this->class->getFieldMappingByDbFieldName($key); |
|
554 | 4 | $this->guardMissingShardKey($document, $key, $data); |
|
555 | |||
556 | 4 | if (isset($mapping['association']) && $mapping['association'] === ClassMetadata::REFERENCE_ONE) { |
|
557 | 1 | $reference = $this->prepareReference( |
|
558 | 1 | $key, |
|
559 | 1 | $data[$mapping['fieldName']], |
|
560 | 1 | $mapping, |
|
561 | 1 | false |
|
562 | ); |
||
563 | 1 | foreach ($reference as $keyValue) { |
|
564 | 1 | $shardKeyQueryPart[$keyValue[0]] = $keyValue[1]; |
|
565 | } |
||
566 | } else { |
||
567 | 3 | $value = Type::getType($mapping['type'])->convertToDatabaseValue($data[$mapping['fieldName']]); |
|
568 | 4 | $shardKeyQueryPart[$key] = $value; |
|
569 | } |
||
570 | } |
||
571 | |||
572 | 4 | return $shardKeyQueryPart; |
|
573 | } |
||
574 | |||
575 | /** |
||
576 | * Wraps the supplied base cursor in the corresponding ODM class. |
||
577 | * |
||
578 | * @param Cursor $baseCursor |
||
579 | * @return Iterator |
||
580 | */ |
||
581 | 22 | private function wrapCursor(Cursor $baseCursor): Iterator |
|
582 | { |
||
583 | 22 | return new CachingIterator(new HydratingIterator($baseCursor, $this->dm->getUnitOfWork(), $this->class)); |
|
584 | } |
||
585 | |||
586 | /** |
||
587 | * Checks whether the given managed document exists in the database. |
||
588 | * |
||
589 | * @param object $document |
||
590 | * @return boolean TRUE if the document exists in the database, FALSE otherwise. |
||
591 | */ |
||
592 | 3 | public function exists($document) |
|
593 | { |
||
594 | 3 | $id = $this->class->getIdentifierObject($document); |
|
595 | 3 | return (boolean) $this->collection->findOne(array('_id' => $id), array('_id')); |
|
596 | } |
||
597 | |||
598 | /** |
||
599 | * Locks document by storing the lock mode on the mapped lock field. |
||
600 | * |
||
601 | * @param object $document |
||
602 | * @param int $lockMode |
||
603 | */ |
||
604 | 5 | public function lock($document, $lockMode) |
|
605 | { |
||
606 | 5 | $id = $this->uow->getDocumentIdentifier($document); |
|
607 | 5 | $criteria = array('_id' => $this->class->getDatabaseIdentifierValue($id)); |
|
608 | 5 | $lockMapping = $this->class->fieldMappings[$this->class->lockField]; |
|
609 | 5 | $this->collection->updateOne($criteria, array('$set' => array($lockMapping['name'] => $lockMode))); |
|
610 | 5 | $this->class->reflFields[$this->class->lockField]->setValue($document, $lockMode); |
|
611 | 5 | } |
|
612 | |||
613 | /** |
||
614 | * Releases any lock that exists on this document. |
||
615 | * |
||
616 | * @param object $document |
||
617 | */ |
||
618 | 1 | public function unlock($document) |
|
619 | { |
||
620 | 1 | $id = $this->uow->getDocumentIdentifier($document); |
|
621 | 1 | $criteria = array('_id' => $this->class->getDatabaseIdentifierValue($id)); |
|
622 | 1 | $lockMapping = $this->class->fieldMappings[$this->class->lockField]; |
|
623 | 1 | $this->collection->updateOne($criteria, array('$unset' => array($lockMapping['name'] => true))); |
|
624 | 1 | $this->class->reflFields[$this->class->lockField]->setValue($document, null); |
|
625 | 1 | } |
|
626 | |||
627 | /** |
||
628 | * Creates or fills a single document object from an query result. |
||
629 | * |
||
630 | * @param object $result The query result. |
||
631 | * @param object $document The document object to fill, if any. |
||
632 | * @param array $hints Hints for document creation. |
||
633 | * @return object The filled and managed document object or NULL, if the query result is empty. |
||
634 | */ |
||
635 | 341 | private function createDocument($result, $document = null, array $hints = array()) |
|
636 | { |
||
637 | 341 | if ($result === null) { |
|
638 | 111 | return null; |
|
639 | } |
||
640 | |||
641 | 300 | if ($document !== null) { |
|
642 | 38 | $hints[Query::HINT_REFRESH] = true; |
|
643 | 38 | $id = $this->class->getPHPIdentifierValue($result['_id']); |
|
644 | 38 | $this->uow->registerManaged($document, $id, $result); |
|
645 | } |
||
646 | |||
647 | 300 | return $this->uow->getOrCreateDocument($this->class->name, $result, $hints, $document); |
|
648 | } |
||
649 | |||
650 | /** |
||
651 | * Loads a PersistentCollection data. Used in the initialize() method. |
||
652 | * |
||
653 | * @param PersistentCollectionInterface $collection |
||
654 | */ |
||
655 | 163 | public function loadCollection(PersistentCollectionInterface $collection) |
|
656 | { |
||
657 | 163 | $mapping = $collection->getMapping(); |
|
658 | 163 | switch ($mapping['association']) { |
|
659 | case ClassMetadata::EMBED_MANY: |
||
660 | 109 | $this->loadEmbedManyCollection($collection); |
|
661 | 109 | break; |
|
662 | |||
663 | case ClassMetadata::REFERENCE_MANY: |
||
664 | 76 | if (isset($mapping['repositoryMethod']) && $mapping['repositoryMethod']) { |
|
665 | 5 | $this->loadReferenceManyWithRepositoryMethod($collection); |
|
666 | } else { |
||
667 | 72 | if ($mapping['isOwningSide']) { |
|
668 | 60 | $this->loadReferenceManyCollectionOwningSide($collection); |
|
669 | } else { |
||
670 | 17 | $this->loadReferenceManyCollectionInverseSide($collection); |
|
671 | } |
||
672 | } |
||
673 | 76 | break; |
|
674 | } |
||
675 | 163 | } |
|
676 | |||
677 | 109 | private function loadEmbedManyCollection(PersistentCollectionInterface $collection) |
|
678 | { |
||
679 | 109 | $embeddedDocuments = $collection->getMongoData(); |
|
680 | 109 | $mapping = $collection->getMapping(); |
|
681 | 109 | $owner = $collection->getOwner(); |
|
682 | 109 | if ($embeddedDocuments) { |
|
683 | 82 | foreach ($embeddedDocuments as $key => $embeddedDocument) { |
|
684 | 82 | $className = $this->uow->getClassNameForAssociation($mapping, $embeddedDocument); |
|
685 | 82 | $embeddedMetadata = $this->dm->getClassMetadata($className); |
|
686 | 82 | $embeddedDocumentObject = $embeddedMetadata->newInstance(); |
|
687 | |||
688 | 82 | $this->uow->setParentAssociation($embeddedDocumentObject, $mapping, $owner, $mapping['name'] . '.' . $key); |
|
689 | |||
690 | 82 | $data = $this->hydratorFactory->hydrate($embeddedDocumentObject, $embeddedDocument, $collection->getHints()); |
|
691 | 82 | $id = $embeddedMetadata->identifier && $data[$embeddedMetadata->identifier] ?? null; |
|
692 | |||
693 | 82 | if (empty($collection->getHints()[Query::HINT_READ_ONLY])) { |
|
694 | 81 | $this->uow->registerManaged($embeddedDocumentObject, $id, $data); |
|
695 | } |
||
696 | 82 | if (CollectionHelper::isHash($mapping['strategy'])) { |
|
697 | 10 | $collection->set($key, $embeddedDocumentObject); |
|
698 | } else { |
||
699 | 82 | $collection->add($embeddedDocumentObject); |
|
700 | } |
||
701 | } |
||
702 | } |
||
703 | 109 | } |
|
704 | |||
705 | 60 | private function loadReferenceManyCollectionOwningSide(PersistentCollectionInterface $collection) |
|
706 | { |
||
707 | 60 | $hints = $collection->getHints(); |
|
708 | 60 | $mapping = $collection->getMapping(); |
|
709 | 60 | $groupedIds = array(); |
|
710 | |||
711 | 60 | $sorted = isset($mapping['sort']) && $mapping['sort']; |
|
712 | |||
713 | 60 | foreach ($collection->getMongoData() as $key => $reference) { |
|
714 | 54 | $className = $this->uow->getClassNameForAssociation($mapping, $reference); |
|
715 | 54 | $identifier = ClassMetadata::getReferenceId($reference, $mapping['storeAs']); |
|
716 | 54 | $id = $this->dm->getClassMetadata($className)->getPHPIdentifierValue($identifier); |
|
717 | |||
718 | // create a reference to the class and id |
||
719 | 54 | $reference = $this->dm->getReference($className, $id); |
|
720 | |||
721 | // no custom sort so add the references right now in the order they are embedded |
||
722 | 54 | if ( ! $sorted) { |
|
723 | 53 | if (CollectionHelper::isHash($mapping['strategy'])) { |
|
724 | 2 | $collection->set($key, $reference); |
|
725 | } else { |
||
726 | 51 | $collection->add($reference); |
|
727 | } |
||
728 | } |
||
729 | |||
730 | // only query for the referenced object if it is not already initialized or the collection is sorted |
||
731 | 54 | if (($reference instanceof Proxy && ! $reference->__isInitialized__) || $sorted) { |
|
732 | 54 | $groupedIds[$className][] = $identifier; |
|
733 | } |
||
734 | } |
||
735 | 60 | foreach ($groupedIds as $className => $ids) { |
|
736 | 39 | $class = $this->dm->getClassMetadata($className); |
|
737 | 39 | $mongoCollection = $this->dm->getDocumentCollection($className); |
|
738 | 39 | $criteria = $this->cm->merge( |
|
739 | 39 | array('_id' => array('$in' => array_values($ids))), |
|
740 | 39 | $this->dm->getFilterCollection()->getFilterCriteria($class), |
|
741 | 39 | $mapping['criteria'] ?? array() |
|
742 | ); |
||
743 | 39 | $criteria = $this->uow->getDocumentPersister($className)->prepareQueryOrNewObj($criteria); |
|
744 | |||
745 | 39 | $options = []; |
|
746 | 39 | if (isset($mapping['sort'])) { |
|
747 | 39 | $options['sort'] = $this->prepareSort($mapping['sort']); |
|
748 | } |
||
749 | 39 | if (isset($mapping['limit'])) { |
|
750 | $options['limit'] = $mapping['limit']; |
||
751 | } |
||
752 | 39 | if (isset($mapping['skip'])) { |
|
753 | $options['skip'] = $mapping['skip']; |
||
754 | } |
||
755 | 39 | if ( ! empty($hints[Query::HINT_READ_PREFERENCE])) { |
|
756 | $options['readPreference'] = $hints[Query::HINT_READ_PREFERENCE]; |
||
757 | } |
||
758 | |||
759 | 39 | $cursor = $mongoCollection->find($criteria, $options); |
|
760 | 39 | $documents = $cursor->toArray(); |
|
761 | 39 | foreach ($documents as $documentData) { |
|
762 | 38 | $document = $this->uow->getById($documentData['_id'], $class); |
|
763 | 38 | if ($document instanceof Proxy && ! $document->__isInitialized()) { |
|
764 | 38 | $data = $this->hydratorFactory->hydrate($document, $documentData); |
|
765 | 38 | $this->uow->setOriginalDocumentData($document, $data); |
|
766 | 38 | $document->__isInitialized__ = true; |
|
767 | } |
||
768 | 38 | if ($sorted) { |
|
769 | 39 | $collection->add($document); |
|
770 | } |
||
771 | } |
||
772 | } |
||
773 | 60 | } |
|
774 | |||
775 | 17 | private function loadReferenceManyCollectionInverseSide(PersistentCollectionInterface $collection) |
|
776 | { |
||
777 | 17 | $query = $this->createReferenceManyInverseSideQuery($collection); |
|
778 | 17 | $documents = $query->execute()->toArray(); |
|
779 | 17 | foreach ($documents as $key => $document) { |
|
780 | 16 | $collection->add($document); |
|
781 | } |
||
782 | 17 | } |
|
783 | |||
784 | /** |
||
785 | * @param PersistentCollectionInterface $collection |
||
786 | * |
||
787 | * @return Query |
||
788 | */ |
||
789 | 17 | public function createReferenceManyInverseSideQuery(PersistentCollectionInterface $collection) |
|
790 | { |
||
791 | 17 | $hints = $collection->getHints(); |
|
792 | 17 | $mapping = $collection->getMapping(); |
|
793 | 17 | $owner = $collection->getOwner(); |
|
794 | 17 | $ownerClass = $this->dm->getClassMetadata(get_class($owner)); |
|
795 | 17 | $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']); |
|
796 | 17 | $mappedByMapping = $targetClass->fieldMappings[$mapping['mappedBy']] ?? array(); |
|
797 | 17 | $mappedByFieldName = ClassMetadata::getReferenceFieldName($mappedByMapping['storeAs'] ?? ClassMetadata::REFERENCE_STORE_AS_DB_REF, $mapping['mappedBy']); |
|
798 | |||
799 | 17 | $criteria = $this->cm->merge( |
|
800 | 17 | array($mappedByFieldName => $ownerClass->getIdentifierObject($owner)), |
|
801 | 17 | $this->dm->getFilterCollection()->getFilterCriteria($targetClass), |
|
802 | 17 | $mapping['criteria'] ?? array() |
|
803 | ); |
||
804 | 17 | $criteria = $this->uow->getDocumentPersister($mapping['targetDocument'])->prepareQueryOrNewObj($criteria); |
|
805 | 17 | $qb = $this->dm->createQueryBuilder($mapping['targetDocument']) |
|
806 | 17 | ->setQueryArray($criteria); |
|
807 | |||
808 | 17 | if (isset($mapping['sort'])) { |
|
809 | 17 | $qb->sort($mapping['sort']); |
|
810 | } |
||
811 | 17 | if (isset($mapping['limit'])) { |
|
812 | 2 | $qb->limit($mapping['limit']); |
|
813 | } |
||
814 | 17 | if (isset($mapping['skip'])) { |
|
815 | $qb->skip($mapping['skip']); |
||
816 | } |
||
817 | |||
818 | 17 | if ( ! empty($hints[Query::HINT_READ_PREFERENCE])) { |
|
819 | $qb->setReadPreference($hints[Query::HINT_READ_PREFERENCE]); |
||
820 | } |
||
821 | |||
822 | 17 | foreach ($mapping['prime'] as $field) { |
|
823 | 4 | $qb->field($field)->prime(true); |
|
824 | } |
||
825 | |||
826 | 17 | return $qb->getQuery(); |
|
827 | } |
||
828 | |||
829 | 5 | private function loadReferenceManyWithRepositoryMethod(PersistentCollectionInterface $collection) |
|
830 | { |
||
831 | 5 | $cursor = $this->createReferenceManyWithRepositoryMethodCursor($collection); |
|
832 | 5 | $mapping = $collection->getMapping(); |
|
833 | 5 | $documents = $cursor->toArray(); |
|
834 | 5 | foreach ($documents as $key => $obj) { |
|
835 | 5 | if (CollectionHelper::isHash($mapping['strategy'])) { |
|
836 | 1 | $collection->set($key, $obj); |
|
837 | } else { |
||
838 | 5 | $collection->add($obj); |
|
839 | } |
||
840 | } |
||
841 | 5 | } |
|
842 | |||
843 | /** |
||
844 | * @param PersistentCollectionInterface $collection |
||
845 | * |
||
846 | * @return \Iterator |
||
847 | */ |
||
848 | 5 | public function createReferenceManyWithRepositoryMethodCursor(PersistentCollectionInterface $collection) |
|
849 | { |
||
850 | 5 | $mapping = $collection->getMapping(); |
|
851 | 5 | $repositoryMethod = $mapping['repositoryMethod']; |
|
852 | 5 | $cursor = $this->dm->getRepository($mapping['targetDocument']) |
|
853 | 5 | ->$repositoryMethod($collection->getOwner()); |
|
854 | |||
855 | 5 | if ( ! $cursor instanceof Iterator) { |
|
856 | throw new \BadMethodCallException("Expected repository method {$repositoryMethod} to return an iterable object"); |
||
857 | } |
||
858 | |||
859 | 5 | if (!empty($mapping['prime'])) { |
|
860 | 1 | $referencePrimer = new ReferencePrimer($this->dm, $this->dm->getUnitOfWork()); |
|
861 | 1 | $primers = array_combine($mapping['prime'], array_fill(0, count($mapping['prime']), true)); |
|
862 | 1 | $class = $this->dm->getClassMetadata($mapping['targetDocument']); |
|
863 | |||
864 | 1 | $cursor = new PrimingIterator($cursor, $class, $referencePrimer, $primers, $collection->getHints()); |
|
865 | } |
||
866 | |||
867 | 5 | return $cursor; |
|
868 | } |
||
869 | |||
870 | /** |
||
871 | * Prepare a projection array by converting keys, which are PHP property |
||
872 | * names, to MongoDB field names. |
||
873 | * |
||
874 | * @param array $fields |
||
875 | * @return array |
||
876 | */ |
||
877 | 14 | public function prepareProjection(array $fields) |
|
878 | { |
||
879 | 14 | $preparedFields = array(); |
|
880 | |||
881 | 14 | foreach ($fields as $key => $value) { |
|
882 | 14 | $preparedFields[$this->prepareFieldName($key)] = $value; |
|
883 | } |
||
884 | |||
885 | 14 | return $preparedFields; |
|
886 | } |
||
887 | |||
888 | /** |
||
889 | * @param $sort |
||
890 | * @return int |
||
891 | */ |
||
892 | 25 | private function getSortDirection($sort) |
|
893 | { |
||
894 | 25 | switch (strtolower($sort)) { |
|
895 | case 'desc': |
||
896 | 15 | return -1; |
|
897 | |||
898 | case 'asc': |
||
899 | 13 | return 1; |
|
900 | } |
||
901 | |||
902 | 12 | return $sort; |
|
903 | } |
||
904 | |||
905 | /** |
||
906 | * Prepare a sort specification array by converting keys to MongoDB field |
||
907 | * names and changing direction strings to int. |
||
908 | * |
||
909 | * @param array $fields |
||
910 | * @return array |
||
911 | */ |
||
912 | 141 | public function prepareSort(array $fields) |
|
913 | { |
||
914 | 141 | $sortFields = []; |
|
915 | |||
916 | 141 | foreach ($fields as $key => $value) { |
|
917 | 25 | $sortFields[$this->prepareFieldName($key)] = $this->getSortDirection($value); |
|
918 | } |
||
919 | |||
920 | 141 | return $sortFields; |
|
921 | } |
||
922 | |||
923 | /** |
||
924 | * Prepare a mongodb field name and convert the PHP property names to MongoDB field names. |
||
925 | * |
||
926 | * @param string $fieldName |
||
927 | * @return string |
||
928 | */ |
||
929 | 433 | public function prepareFieldName($fieldName) |
|
930 | { |
||
931 | 433 | $fieldNames = $this->prepareQueryElement($fieldName, null, null, false); |
|
932 | |||
933 | 433 | return $fieldNames[0][0]; |
|
934 | } |
||
935 | |||
936 | /** |
||
937 | * Adds discriminator criteria to an already-prepared query. |
||
938 | * |
||
939 | * This method should be used once for query criteria and not be used for |
||
940 | * nested expressions. It should be called before |
||
941 | * {@link DocumentPerister::addFilterToPreparedQuery()}. |
||
942 | * |
||
943 | * @param array $preparedQuery |
||
944 | * @return array |
||
945 | */ |
||
946 | 492 | public function addDiscriminatorToPreparedQuery(array $preparedQuery) |
|
947 | { |
||
948 | /* If the class has a discriminator field, which is not already in the |
||
949 | * criteria, inject it now. The field/values need no preparation. |
||
950 | */ |
||
951 | 492 | if ($this->class->hasDiscriminator() && ! isset($preparedQuery[$this->class->discriminatorField])) { |
|
952 | 29 | $discriminatorValues = $this->getClassDiscriminatorValues($this->class); |
|
953 | 29 | if (count($discriminatorValues) === 1) { |
|
954 | 21 | $preparedQuery[$this->class->discriminatorField] = $discriminatorValues[0]; |
|
955 | } else { |
||
956 | 10 | $preparedQuery[$this->class->discriminatorField] = array('$in' => $discriminatorValues); |
|
957 | } |
||
958 | } |
||
959 | |||
960 | 492 | return $preparedQuery; |
|
961 | } |
||
962 | |||
963 | /** |
||
964 | * Adds filter criteria to an already-prepared query. |
||
965 | * |
||
966 | * This method should be used once for query criteria and not be used for |
||
967 | * nested expressions. It should be called after |
||
968 | * {@link DocumentPerister::addDiscriminatorToPreparedQuery()}. |
||
969 | * |
||
970 | * @param array $preparedQuery |
||
971 | * @return array |
||
972 | */ |
||
973 | 493 | public function addFilterToPreparedQuery(array $preparedQuery) |
|
974 | { |
||
975 | /* If filter criteria exists for this class, prepare it and merge |
||
976 | * over the existing query. |
||
977 | * |
||
978 | * @todo Consider recursive merging in case the filter criteria and |
||
979 | * prepared query both contain top-level $and/$or operators. |
||
980 | */ |
||
981 | 493 | if ($filterCriteria = $this->dm->getFilterCollection()->getFilterCriteria($this->class)) { |
|
982 | 18 | $preparedQuery = $this->cm->merge($preparedQuery, $this->prepareQueryOrNewObj($filterCriteria)); |
|
983 | } |
||
984 | |||
985 | 493 | return $preparedQuery; |
|
986 | } |
||
987 | |||
988 | /** |
||
989 | * Prepares the query criteria or new document object. |
||
990 | * |
||
991 | * PHP field names and types will be converted to those used by MongoDB. |
||
992 | * |
||
993 | * @param array $query |
||
994 | * @param bool $isNewObj |
||
995 | * @return array |
||
996 | */ |
||
997 | 525 | public function prepareQueryOrNewObj(array $query, $isNewObj = false) |
|
998 | { |
||
999 | 525 | $preparedQuery = array(); |
|
1000 | |||
1001 | 525 | foreach ($query as $key => $value) { |
|
1002 | // Recursively prepare logical query clauses |
||
1003 | 483 | if (in_array($key, array('$and', '$or', '$nor')) && is_array($value)) { |
|
1004 | 20 | foreach ($value as $k2 => $v2) { |
|
1005 | 20 | $preparedQuery[$key][$k2] = $this->prepareQueryOrNewObj($v2, $isNewObj); |
|
1006 | } |
||
1007 | 20 | continue; |
|
1008 | } |
||
1009 | |||
1010 | 483 | if (isset($key[0]) && $key[0] === '$' && is_array($value)) { |
|
1011 | 38 | $preparedQuery[$key] = $this->prepareQueryOrNewObj($value, $isNewObj); |
|
1012 | 38 | continue; |
|
1013 | } |
||
1014 | |||
1015 | 483 | $preparedQueryElements = $this->prepareQueryElement($key, $value, null, true, $isNewObj); |
|
1016 | 483 | foreach ($preparedQueryElements as list($preparedKey, $preparedValue)) { |
|
1017 | 483 | $preparedQuery[$preparedKey] = is_array($preparedValue) |
|
1018 | 133 | ? array_map('\Doctrine\ODM\MongoDB\Types\Type::convertPHPToDatabaseValue', $preparedValue) |
|
1019 | 483 | : Type::convertPHPToDatabaseValue($preparedValue); |
|
1020 | } |
||
1021 | } |
||
1022 | |||
1023 | 525 | return $preparedQuery; |
|
1024 | } |
||
1025 | |||
1026 | /** |
||
1027 | * Prepares a query value and converts the PHP value to the database value |
||
1028 | * if it is an identifier. |
||
1029 | * |
||
1030 | * It also handles converting $fieldName to the database name if they are different. |
||
1031 | * |
||
1032 | * @param string $fieldName |
||
1033 | * @param mixed $value |
||
1034 | * @param ClassMetadata $class Defaults to $this->class |
||
1035 | * @param bool $prepareValue Whether or not to prepare the value |
||
1036 | * @param bool $inNewObj Whether or not newObj is being prepared |
||
1037 | * @return array An array of tuples containing prepared field names and values |
||
1038 | */ |
||
1039 | 876 | private function prepareQueryElement($fieldName, $value = null, $class = null, $prepareValue = true, $inNewObj = false) |
|
1040 | { |
||
1041 | 876 | $class = $class ?? $this->class; |
|
1042 | |||
1043 | // @todo Consider inlining calls to ClassMetadata methods |
||
1044 | |||
1045 | // Process all non-identifier fields by translating field names |
||
1046 | 876 | if ($class->hasField($fieldName) && ! $class->isIdentifier($fieldName)) { |
|
1047 | 247 | $mapping = $class->fieldMappings[$fieldName]; |
|
1048 | 247 | $fieldName = $mapping['name']; |
|
1049 | |||
1050 | 247 | if ( ! $prepareValue) { |
|
1051 | 52 | return [[$fieldName, $value]]; |
|
1052 | } |
||
1053 | |||
1054 | // Prepare mapped, embedded objects |
||
1055 | 205 | if ( ! empty($mapping['embedded']) && is_object($value) && |
|
1056 | 205 | ! $this->dm->getMetadataFactory()->isTransient(get_class($value))) { |
|
1057 | 3 | return [[$fieldName, $this->pb->prepareEmbeddedDocumentValue($mapping, $value)]]; |
|
1058 | } |
||
1059 | |||
1060 | 203 | if (! empty($mapping['reference']) && is_object($value) && ! ($value instanceof \MongoDB\BSON\ObjectId)) { |
|
1061 | try { |
||
1062 | 14 | return $this->prepareReference($fieldName, $value, $mapping, $inNewObj); |
|
1063 | 1 | } catch (MappingException $e) { |
|
1064 | // do nothing in case passed object is not mapped document |
||
1065 | } |
||
1066 | } |
||
1067 | |||
1068 | // No further preparation unless we're dealing with a simple reference |
||
1069 | // We can't have expressions in empty() with PHP < 5.5, so store it in a variable |
||
1070 | 190 | $arrayValue = (array) $value; |
|
1071 | 190 | if (empty($mapping['reference']) || $mapping['storeAs'] !== ClassMetadata::REFERENCE_STORE_AS_ID || empty($arrayValue)) { |
|
1072 | 126 | return [[$fieldName, $value]]; |
|
1073 | } |
||
1074 | |||
1075 | // Additional preparation for one or more simple reference values |
||
1076 | 91 | $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']); |
|
1077 | |||
1078 | 91 | if ( ! is_array($value)) { |
|
1079 | 87 | return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]]; |
|
1080 | } |
||
1081 | |||
1082 | // Objects without operators or with DBRef fields can be converted immediately |
||
1083 | 6 | View Code Duplication | if ( ! $this->hasQueryOperators($value) || $this->hasDBRefFields($value)) { |
1084 | 3 | return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]]; |
|
1085 | } |
||
1086 | |||
1087 | 6 | return [[$fieldName, $this->prepareQueryExpression($value, $targetClass)]]; |
|
1088 | } |
||
1089 | |||
1090 | // Process identifier fields |
||
1091 | 789 | if (($class->hasField($fieldName) && $class->isIdentifier($fieldName)) || $fieldName === '_id') { |
|
1092 | 334 | $fieldName = '_id'; |
|
1093 | |||
1094 | 334 | if ( ! $prepareValue) { |
|
1095 | 42 | return [[$fieldName, $value]]; |
|
1096 | } |
||
1097 | |||
1098 | 295 | if ( ! is_array($value)) { |
|
1099 | 272 | return [[$fieldName, $class->getDatabaseIdentifierValue($value)]]; |
|
1100 | } |
||
1101 | |||
1102 | // Objects without operators or with DBRef fields can be converted immediately |
||
1103 | 61 | View Code Duplication | if ( ! $this->hasQueryOperators($value) || $this->hasDBRefFields($value)) { |
1104 | 6 | return [[$fieldName, $class->getDatabaseIdentifierValue($value)]]; |
|
1105 | } |
||
1106 | |||
1107 | 56 | return [[$fieldName, $this->prepareQueryExpression($value, $class)]]; |
|
1108 | } |
||
1109 | |||
1110 | // No processing for unmapped, non-identifier, non-dotted field names |
||
1111 | 553 | if (strpos($fieldName, '.') === false) { |
|
1112 | 414 | return [[$fieldName, $value]]; |
|
1113 | } |
||
1114 | |||
1115 | /* Process "fieldName.objectProperty" queries (on arrays or objects). |
||
1116 | * |
||
1117 | * We can limit parsing here, since at most three segments are |
||
1118 | * significant: "fieldName.objectProperty" with an optional index or key |
||
1119 | * for collections stored as either BSON arrays or objects. |
||
1120 | */ |
||
1121 | 152 | $e = explode('.', $fieldName, 4); |
|
1122 | |||
1123 | // No further processing for unmapped fields |
||
1124 | 152 | if ( ! isset($class->fieldMappings[$e[0]])) { |
|
1125 | 6 | return [[$fieldName, $value]]; |
|
1126 | } |
||
1127 | |||
1128 | 147 | $mapping = $class->fieldMappings[$e[0]]; |
|
1129 | 147 | $e[0] = $mapping['name']; |
|
1130 | |||
1131 | // Hash and raw fields will not be prepared beyond the field name |
||
1132 | 147 | if ($mapping['type'] === Type::HASH || $mapping['type'] === Type::RAW) { |
|
1133 | 1 | $fieldName = implode('.', $e); |
|
1134 | |||
1135 | 1 | return [[$fieldName, $value]]; |
|
1136 | } |
||
1137 | |||
1138 | 146 | if ($mapping['type'] == 'many' && CollectionHelper::isHash($mapping['strategy']) |
|
1139 | 146 | && isset($e[2])) { |
|
1140 | 1 | $objectProperty = $e[2]; |
|
1141 | 1 | $objectPropertyPrefix = $e[1] . '.'; |
|
1142 | 1 | $nextObjectProperty = implode('.', array_slice($e, 3)); |
|
1143 | 145 | } elseif ($e[1] != '$') { |
|
1144 | 144 | $fieldName = $e[0] . '.' . $e[1]; |
|
1145 | 144 | $objectProperty = $e[1]; |
|
1146 | 144 | $objectPropertyPrefix = ''; |
|
1147 | 144 | $nextObjectProperty = implode('.', array_slice($e, 2)); |
|
1148 | 1 | } elseif (isset($e[2])) { |
|
1149 | 1 | $fieldName = $e[0] . '.' . $e[1] . '.' . $e[2]; |
|
1150 | 1 | $objectProperty = $e[2]; |
|
1151 | 1 | $objectPropertyPrefix = $e[1] . '.'; |
|
1152 | 1 | $nextObjectProperty = implode('.', array_slice($e, 3)); |
|
1153 | } else { |
||
1154 | 1 | $fieldName = $e[0] . '.' . $e[1]; |
|
1155 | |||
1156 | 1 | return [[$fieldName, $value]]; |
|
1157 | } |
||
1158 | |||
1159 | // No further processing for fields without a targetDocument mapping |
||
1160 | 146 | if ( ! isset($mapping['targetDocument'])) { |
|
1161 | 3 | if ($nextObjectProperty) { |
|
1162 | $fieldName .= '.'.$nextObjectProperty; |
||
1163 | } |
||
1164 | |||
1165 | 3 | return [[$fieldName, $value]]; |
|
1166 | } |
||
1167 | |||
1168 | 143 | $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']); |
|
1169 | |||
1170 | // No further processing for unmapped targetDocument fields |
||
1171 | 143 | if ( ! $targetClass->hasField($objectProperty)) { |
|
1172 | 25 | if ($nextObjectProperty) { |
|
1173 | $fieldName .= '.'.$nextObjectProperty; |
||
1174 | } |
||
1175 | |||
1176 | 25 | return [[$fieldName, $value]]; |
|
1177 | } |
||
1178 | |||
1179 | 123 | $targetMapping = $targetClass->getFieldMapping($objectProperty); |
|
1180 | 123 | $objectPropertyIsId = $targetClass->isIdentifier($objectProperty); |
|
1181 | |||
1182 | // Prepare DBRef identifiers or the mapped field's property path |
||
1183 | 123 | $fieldName = ($objectPropertyIsId && ! empty($mapping['reference']) && $mapping['storeAs'] !== ClassMetadata::REFERENCE_STORE_AS_ID) |
|
1184 | 105 | ? ClassMetadata::getReferenceFieldName($mapping['storeAs'], $e[0]) |
|
1185 | 123 | : $e[0] . '.' . $objectPropertyPrefix . $targetMapping['name']; |
|
1186 | |||
1187 | // Process targetDocument identifier fields |
||
1188 | 123 | if ($objectPropertyIsId) { |
|
1189 | 106 | if ( ! $prepareValue) { |
|
1190 | 7 | return [[$fieldName, $value]]; |
|
1191 | } |
||
1192 | |||
1193 | 99 | if ( ! is_array($value)) { |
|
1194 | 85 | return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]]; |
|
1195 | } |
||
1196 | |||
1197 | // Objects without operators or with DBRef fields can be converted immediately |
||
1198 | 16 | View Code Duplication | if ( ! $this->hasQueryOperators($value) || $this->hasDBRefFields($value)) { |
1199 | 6 | return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]]; |
|
1200 | } |
||
1201 | |||
1202 | 16 | return [[$fieldName, $this->prepareQueryExpression($value, $targetClass)]]; |
|
1203 | } |
||
1204 | |||
1205 | /* The property path may include a third field segment, excluding the |
||
1206 | * collection item pointer. If present, this next object property must |
||
1207 | * be processed recursively. |
||
1208 | */ |
||
1209 | 17 | if ($nextObjectProperty) { |
|
1210 | // Respect the targetDocument's class metadata when recursing |
||
1211 | 14 | $nextTargetClass = isset($targetMapping['targetDocument']) |
|
1212 | 8 | ? $this->dm->getClassMetadata($targetMapping['targetDocument']) |
|
1213 | 14 | : null; |
|
1214 | |||
1215 | 14 | $fieldNames = $this->prepareQueryElement($nextObjectProperty, $value, $nextTargetClass, $prepareValue); |
|
1216 | |||
1217 | 14 | return array_map(function ($preparedTuple) use ($fieldName) { |
|
1218 | 14 | list($key, $value) = $preparedTuple; |
|
1219 | |||
1220 | 14 | return [$fieldName . '.' . $key, $value]; |
|
1221 | 14 | }, $fieldNames); |
|
1222 | } |
||
1223 | |||
1224 | 5 | return [[$fieldName, $value]]; |
|
1225 | } |
||
1226 | |||
1227 | /** |
||
1228 | * Prepares a query expression. |
||
1229 | * |
||
1230 | * @param array|object $expression |
||
1231 | * @param ClassMetadata $class |
||
1232 | * @return array |
||
1233 | */ |
||
1234 | 78 | private function prepareQueryExpression($expression, $class) |
|
1235 | { |
||
1236 | 78 | foreach ($expression as $k => $v) { |
|
1237 | // Ignore query operators whose arguments need no type conversion |
||
1238 | 78 | if (in_array($k, array('$exists', '$type', '$mod', '$size'))) { |
|
1239 | 16 | continue; |
|
1240 | } |
||
1241 | |||
1242 | // Process query operators whose argument arrays need type conversion |
||
1243 | 78 | if (in_array($k, array('$in', '$nin', '$all')) && is_array($v)) { |
|
1244 | 76 | foreach ($v as $k2 => $v2) { |
|
1245 | 76 | $expression[$k][$k2] = $class->getDatabaseIdentifierValue($v2); |
|
1246 | } |
||
1247 | 76 | continue; |
|
1248 | } |
||
1249 | |||
1250 | // Recursively process expressions within a $not operator |
||
1251 | 18 | if ($k === '$not' && is_array($v)) { |
|
1252 | 15 | $expression[$k] = $this->prepareQueryExpression($v, $class); |
|
1253 | 15 | continue; |
|
1254 | } |
||
1255 | |||
1256 | 18 | $expression[$k] = $class->getDatabaseIdentifierValue($v); |
|
1257 | } |
||
1258 | |||
1259 | 78 | return $expression; |
|
1260 | } |
||
1261 | |||
1262 | /** |
||
1263 | * Checks whether the value has DBRef fields. |
||
1264 | * |
||
1265 | * This method doesn't check if the the value is a complete DBRef object, |
||
1266 | * although it should return true for a DBRef. Rather, we're checking that |
||
1267 | * the value has one or more fields for a DBref. In practice, this could be |
||
1268 | * $elemMatch criteria for matching a DBRef. |
||
1269 | * |
||
1270 | * @param mixed $value |
||
1271 | * @return boolean |
||
1272 | */ |
||
1273 | 79 | private function hasDBRefFields($value) |
|
1274 | { |
||
1275 | 79 | if ( ! is_array($value) && ! is_object($value)) { |
|
1276 | return false; |
||
1277 | } |
||
1278 | |||
1279 | 79 | if (is_object($value)) { |
|
1280 | $value = get_object_vars($value); |
||
1281 | } |
||
1282 | |||
1283 | 79 | foreach ($value as $key => $_) { |
|
1284 | 79 | if ($key === '$ref' || $key === '$id' || $key === '$db') { |
|
1285 | 79 | return true; |
|
1286 | } |
||
1287 | } |
||
1288 | |||
1289 | 78 | return false; |
|
1290 | } |
||
1291 | |||
1292 | /** |
||
1293 | * Checks whether the value has query operators. |
||
1294 | * |
||
1295 | * @param mixed $value |
||
1296 | * @return boolean |
||
1297 | */ |
||
1298 | 83 | private function hasQueryOperators($value) |
|
1299 | { |
||
1300 | 83 | if ( ! is_array($value) && ! is_object($value)) { |
|
1301 | return false; |
||
1302 | } |
||
1303 | |||
1304 | 83 | if (is_object($value)) { |
|
1305 | $value = get_object_vars($value); |
||
1306 | } |
||
1307 | |||
1308 | 83 | foreach ($value as $key => $_) { |
|
1309 | 83 | if (isset($key[0]) && $key[0] === '$') { |
|
1310 | 83 | return true; |
|
1311 | } |
||
1312 | } |
||
1313 | |||
1314 | 11 | return false; |
|
1315 | } |
||
1316 | |||
1317 | /** |
||
1318 | * Gets the array of discriminator values for the given ClassMetadata |
||
1319 | * |
||
1320 | * @param ClassMetadata $metadata |
||
1321 | * @return array |
||
1322 | */ |
||
1323 | 29 | private function getClassDiscriminatorValues(ClassMetadata $metadata) |
|
1324 | { |
||
1325 | 29 | $discriminatorValues = array($metadata->discriminatorValue); |
|
1326 | 29 | foreach ($metadata->subClasses as $className) { |
|
1327 | 8 | if ($key = array_search($className, $metadata->discriminatorMap)) { |
|
1328 | 8 | $discriminatorValues[] = $key; |
|
1329 | } |
||
1330 | } |
||
1331 | |||
1332 | // If a defaultDiscriminatorValue is set and it is among the discriminators being queries, add NULL to the list |
||
1333 | 29 | View Code Duplication | if ($metadata->defaultDiscriminatorValue && array_search($metadata->defaultDiscriminatorValue, $discriminatorValues) !== false) { |
1334 | 2 | $discriminatorValues[] = null; |
|
1335 | } |
||
1336 | |||
1337 | 29 | return $discriminatorValues; |
|
1338 | } |
||
1339 | |||
1340 | 547 | private function handleCollections($document, $options) |
|
1341 | { |
||
1342 | // Collection deletions (deletions of complete collections) |
||
1343 | 547 | foreach ($this->uow->getScheduledCollections($document) as $coll) { |
|
1344 | 103 | if ($this->uow->isCollectionScheduledForDeletion($coll)) { |
|
1345 | 103 | $this->cp->delete($coll, $options); |
|
1346 | } |
||
1347 | } |
||
1348 | // Collection updates (deleteRows, updateRows, insertRows) |
||
1349 | 547 | foreach ($this->uow->getScheduledCollections($document) as $coll) { |
|
1350 | 103 | if ($this->uow->isCollectionScheduledForUpdate($coll)) { |
|
1351 | 103 | $this->cp->update($coll, $options); |
|
1352 | } |
||
1353 | } |
||
1354 | // Take new snapshots from visited collections |
||
1355 | 547 | foreach ($this->uow->getVisitedCollections($document) as $coll) { |
|
1356 | 226 | $coll->takeSnapshot(); |
|
1357 | } |
||
1358 | 547 | } |
|
1359 | |||
1360 | /** |
||
1361 | * If the document is new, ignore shard key field value, otherwise throw an exception. |
||
1362 | * Also, shard key field should be present in actual document data. |
||
1363 | * |
||
1364 | * @param object $document |
||
1365 | * @param string $shardKeyField |
||
1366 | * @param array $actualDocumentData |
||
1367 | * |
||
1368 | * @throws MongoDBException |
||
1369 | */ |
||
1370 | 4 | private function guardMissingShardKey($document, $shardKeyField, $actualDocumentData) |
|
1371 | { |
||
1372 | 4 | $dcs = $this->uow->getDocumentChangeSet($document); |
|
1373 | 4 | $isUpdate = $this->uow->isScheduledForUpdate($document); |
|
1374 | |||
1375 | 4 | $fieldMapping = $this->class->getFieldMappingByDbFieldName($shardKeyField); |
|
1376 | 4 | $fieldName = $fieldMapping['fieldName']; |
|
1377 | |||
1378 | 4 | if ($isUpdate && isset($dcs[$fieldName]) && $dcs[$fieldName][0] != $dcs[$fieldName][1]) { |
|
1379 | throw MongoDBException::shardKeyFieldCannotBeChanged($shardKeyField, $this->class->getName()); |
||
1380 | } |
||
1381 | |||
1382 | 4 | if (!isset($actualDocumentData[$fieldName])) { |
|
1383 | throw MongoDBException::shardKeyFieldMissing($shardKeyField, $this->class->getName()); |
||
1384 | } |
||
1385 | 4 | } |
|
1386 | |||
1387 | /** |
||
1388 | * Get shard key aware query for single document. |
||
1389 | * |
||
1390 | * @param object $document |
||
1391 | * |
||
1392 | * @return array |
||
1393 | */ |
||
1394 | 264 | private function getQueryForDocument($document) |
|
1395 | { |
||
1396 | 264 | $id = $this->uow->getDocumentIdentifier($document); |
|
1397 | 264 | $id = $this->class->getDatabaseIdentifierValue($id); |
|
1398 | |||
1399 | 264 | $shardKeyQueryPart = $this->getShardKeyQuery($document); |
|
1400 | 264 | $query = array_merge(array('_id' => $id), $shardKeyQueryPart); |
|
1401 | |||
1402 | 264 | return $query; |
|
1403 | } |
||
1404 | |||
1405 | /** |
||
1406 | * @param array $options |
||
1407 | * |
||
1408 | * @return array |
||
1409 | */ |
||
1410 | 548 | private function getWriteOptions(array $options = array()) |
|
1411 | { |
||
1412 | 548 | $defaultOptions = $this->dm->getConfiguration()->getDefaultCommitOptions(); |
|
1413 | 548 | $documentOptions = []; |
|
1414 | 548 | if ($this->class->hasWriteConcern()) { |
|
1415 | 9 | $documentOptions['w'] = $this->class->getWriteConcern(); |
|
1416 | } |
||
1417 | |||
1418 | 548 | return array_merge($defaultOptions, $documentOptions, $options); |
|
1419 | } |
||
1420 | |||
1421 | /** |
||
1422 | * @param string $fieldName |
||
1423 | * @param mixed $value |
||
1424 | * @param array $mapping |
||
1425 | * @param bool $inNewObj |
||
1426 | * @return array |
||
1427 | */ |
||
1428 | 15 | private function prepareReference($fieldName, $value, array $mapping, $inNewObj) |
|
1429 | { |
||
1430 | 15 | $reference = $this->dm->createReference($value, $mapping); |
|
1431 | 14 | if ($inNewObj || $mapping['storeAs'] === ClassMetadata::REFERENCE_STORE_AS_ID) { |
|
1432 | 8 | return [[$fieldName, $reference]]; |
|
1433 | } |
||
1434 | |||
1435 | 6 | switch ($mapping['storeAs']) { |
|
1436 | case ClassMetadata::REFERENCE_STORE_AS_REF: |
||
1437 | $keys = ['id' => true]; |
||
1438 | break; |
||
1439 | |||
1440 | case ClassMetadata::REFERENCE_STORE_AS_DB_REF: |
||
1441 | case ClassMetadata::REFERENCE_STORE_AS_DB_REF_WITH_DB: |
||
1442 | 6 | $keys = ['$ref' => true, '$id' => true, '$db' => true]; |
|
1443 | |||
1444 | 6 | if ($mapping['storeAs'] === ClassMetadata::REFERENCE_STORE_AS_DB_REF) { |
|
1445 | 5 | unset($keys['$db']); |
|
1446 | } |
||
1447 | |||
1448 | 6 | if (isset($mapping['targetDocument'])) { |
|
1449 | 4 | unset($keys['$ref'], $keys['$db']); |
|
1450 | } |
||
1451 | 6 | break; |
|
1452 | |||
1453 | default: |
||
1454 | throw new \InvalidArgumentException("Reference type {$mapping['storeAs']} is invalid."); |
||
1455 | } |
||
1456 | |||
1457 | 6 | if ($mapping['type'] === 'many') { |
|
1458 | 2 | return [[$fieldName, ['$elemMatch' => array_intersect_key($reference, $keys)]]]; |
|
1459 | } |
||
1460 | |||
1461 | 4 | return array_map( |
|
1462 | 4 | function ($key) use ($reference, $fieldName) { |
|
1463 | 4 | return [$fieldName . '.' . $key, $reference[$key]]; |
|
1464 | 4 | }, |
|
1465 | 4 | array_keys($keys) |
|
1466 | ); |
||
1467 | } |
||
1468 | } |
||
1469 |
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.