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