Completed
Pull Request — master (#1803)
by Maciej
18:23 queued 15:20
created

DocumentPersister::refresh()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2.0078

Importance

Changes 0
Metric Value
dl 0
loc 10
ccs 7
cts 8
cp 0.875
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 7
nc 2
nop 1
crap 2.0078
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ODM\MongoDB\Persisters;
6
7
use Doctrine\Common\Persistence\Mapping\MappingException;
8
use Doctrine\ODM\MongoDB\DocumentManager;
9
use Doctrine\ODM\MongoDB\Hydrator\HydratorFactory;
10
use Doctrine\ODM\MongoDB\Iterator\CachingIterator;
11
use Doctrine\ODM\MongoDB\Iterator\HydratingIterator;
12
use Doctrine\ODM\MongoDB\Iterator\Iterator;
13
use Doctrine\ODM\MongoDB\Iterator\PrimingIterator;
14
use Doctrine\ODM\MongoDB\LockException;
15
use Doctrine\ODM\MongoDB\LockMode;
16
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
17
use Doctrine\ODM\MongoDB\MongoDBException;
18
use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionInterface;
19
use Doctrine\ODM\MongoDB\Proxy\Proxy;
20
use Doctrine\ODM\MongoDB\Query\CriteriaMerger;
21
use Doctrine\ODM\MongoDB\Query\Query;
22
use Doctrine\ODM\MongoDB\Query\ReferencePrimer;
23
use Doctrine\ODM\MongoDB\Types\Type;
24
use Doctrine\ODM\MongoDB\UnitOfWork;
25
use Doctrine\ODM\MongoDB\Utility\CollectionHelper;
26
use MongoDB\BSON\ObjectId;
27
use MongoDB\Collection;
28
use MongoDB\Driver\Cursor;
29
use MongoDB\Driver\Exception\Exception as DriverException;
30
use MongoDB\Driver\Exception\WriteException;
31
use function array_combine;
32
use function array_fill;
33
use function array_intersect_key;
34
use function array_keys;
35
use function array_map;
36
use function array_merge;
37
use function array_search;
38
use function array_slice;
39
use function array_values;
40
use function count;
41
use function explode;
42
use function get_class;
43
use function get_object_vars;
44
use function implode;
45
use function in_array;
46
use function is_array;
47
use function is_object;
48
use function is_scalar;
49
use function max;
50
use function spl_object_hash;
51
use function sprintf;
52
use function strpos;
53
use function strtolower;
54
55
/**
56
 * The DocumentPersister is responsible for persisting documents.
57
 *
58
 */
59
class DocumentPersister
60
{
61
    /**
62
     * The PersistenceBuilder instance.
63
     *
64
     * @var PersistenceBuilder
65
     */
66
    private $pb;
67
68
    /**
69
     * The DocumentManager instance.
70
     *
71
     * @var DocumentManager
72
     */
73
    private $dm;
74
75
    /**
76
     * The UnitOfWork instance.
77
     *
78
     * @var UnitOfWork
79
     */
80
    private $uow;
81
82
    /**
83
     * The ClassMetadata instance for the document type being persisted.
84
     *
85
     * @var ClassMetadata
86
     */
87
    private $class;
88
89
    /**
90
     * The MongoCollection instance for this document.
91
     *
92
     * @var Collection
93
     */
94
    private $collection;
95
96
    /**
97
     * Array of queued inserts for the persister to insert.
98
     *
99
     * @var array
100
     */
101
    private $queuedInserts = [];
102
103
    /**
104
     * Array of queued inserts for the persister to insert.
105
     *
106
     * @var array
107
     */
108
    private $queuedUpserts = [];
109
110
    /**
111
     * The CriteriaMerger instance.
112
     *
113
     * @var CriteriaMerger
114
     */
115
    private $cm;
116
117
    /**
118
     * The CollectionPersister instance.
119
     *
120
     * @var CollectionPersister
121
     */
122
    private $cp;
123
124
    /**
125
     * The HydratorFactory instance.
126
     *
127
     * @var HydratorFactory
128
     */
129
    private $hydratorFactory;
130
131
    /**
132
     * Initializes this instance.
133
     *
134
     */
135 1078
    public function __construct(
136
        PersistenceBuilder $pb,
137
        DocumentManager $dm,
138
        UnitOfWork $uow,
139
        HydratorFactory $hydratorFactory,
140
        ClassMetadata $class,
141
        ?CriteriaMerger $cm = null
142
    ) {
143 1078
        $this->pb = $pb;
144 1078
        $this->dm = $dm;
145 1078
        $this->cm = $cm ?: new CriteriaMerger();
146 1078
        $this->uow = $uow;
147 1078
        $this->hydratorFactory = $hydratorFactory;
148 1078
        $this->class = $class;
149 1078
        $this->collection = $dm->getDocumentCollection($class->name);
150 1078
        $this->cp = $this->uow->getCollectionPersister();
151 1078
    }
152
153
    /**
154
     * @return array
155
     */
156
    public function getInserts()
157
    {
158
        return $this->queuedInserts;
159
    }
160
161
    /**
162
     * @param object $document
163
     * @return bool
164
     */
165
    public function isQueuedForInsert($document)
166
    {
167
        return isset($this->queuedInserts[spl_object_hash($document)]);
168
    }
169
170
    /**
171
     * Adds a document to the queued insertions.
172
     * The document remains queued until {@link executeInserts} is invoked.
173
     *
174
     * @param object $document The document to queue for insertion.
175
     */
176 480
    public function addInsert($document)
177
    {
178 480
        $this->queuedInserts[spl_object_hash($document)] = $document;
179 480
    }
180
181
    /**
182
     * @return array
183
     */
184
    public function getUpserts()
185
    {
186
        return $this->queuedUpserts;
187
    }
188
189
    /**
190
     * @param object $document
191
     * @return bool
192
     */
193
    public function isQueuedForUpsert($document)
194
    {
195
        return isset($this->queuedUpserts[spl_object_hash($document)]);
196
    }
197
198
    /**
199
     * Adds a document to the queued upserts.
200
     * The document remains queued until {@link executeUpserts} is invoked.
201
     *
202
     * @param object $document The document to queue for insertion.
203
     */
204 83
    public function addUpsert($document)
205
    {
206 83
        $this->queuedUpserts[spl_object_hash($document)] = $document;
207 83
    }
208
209
    /**
210
     * Gets the ClassMetadata instance of the document class this persister is used for.
211
     *
212
     * @return ClassMetadata
213
     */
214
    public function getClassMetadata()
215
    {
216
        return $this->class;
217
    }
218
219
    /**
220
     * Executes all queued document insertions.
221
     *
222
     * Queued documents without an ID will inserted in a batch and queued
223
     * documents with an ID will be upserted individually.
224
     *
225
     * If no inserts are queued, invoking this method is a NOOP.
226
     *
227
     * @param array $options Options for batchInsert() and update() driver methods
228
     */
229 480
    public function executeInserts(array $options = [])
230
    {
231 480
        if (! $this->queuedInserts) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->queuedInserts of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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.

Loading history...
232
            return;
233
        }
234
235 480
        $inserts = [];
236 480
        $options = $this->getWriteOptions($options);
237 480
        foreach ($this->queuedInserts as $oid => $document) {
238 480
            $data = $this->pb->prepareInsertData($document);
239
240
            // Set the initial version for each insert
241 479
            if ($this->class->isVersioned) {
242 20
                $versionMapping = $this->class->fieldMappings[$this->class->versionField];
243 20
                $nextVersion = null;
244 20
                if ($versionMapping['type'] === 'int') {
245 18
                    $nextVersion = max(1, (int) $this->class->reflFields[$this->class->versionField]->getValue($document));
246 18
                    $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
247 2
                } elseif ($versionMapping['type'] === 'date') {
248 2
                    $nextVersionDateTime = new \DateTime();
249 2
                    $nextVersion = Type::convertPHPToDatabaseValue($nextVersionDateTime);
250 2
                    $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersionDateTime);
251
                }
252 20
                $data[$versionMapping['name']] = $nextVersion;
253
            }
254
255 479
            $inserts[] = $data;
256
        }
257
258 479
        if ($inserts) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $inserts of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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.

Loading history...
259
            try {
260 479
                $this->collection->insertMany($inserts, $options);
261 6
            } catch (DriverException $e) {
0 ignored issues
show
Bug introduced by
The class MongoDB\Driver\Exception\Exception does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
262 6
                $this->queuedInserts = [];
263 6
                throw $e;
264
            }
265
        }
266
267
        /* All collections except for ones using addToSet have already been
268
         * saved. We have left these to be handled separately to avoid checking
269
         * collection for uniqueness on PHP side.
270
         */
271 479
        foreach ($this->queuedInserts as $document) {
272 479
            $this->handleCollections($document, $options);
273
        }
274
275 479
        $this->queuedInserts = [];
276 479
    }
277
278
    /**
279
     * Executes all queued document upserts.
280
     *
281
     * Queued documents with an ID are upserted individually.
282
     *
283
     * If no upserts are queued, invoking this method is a NOOP.
284
     *
285
     * @param array $options Options for batchInsert() and update() driver methods
286
     */
287 83
    public function executeUpserts(array $options = [])
288
    {
289 83
        if (! $this->queuedUpserts) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->queuedUpserts of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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.

Loading history...
290
            return;
291
        }
292
293 83
        $options = $this->getWriteOptions($options);
294 83
        foreach ($this->queuedUpserts as $oid => $document) {
295
            try {
296 83
                $this->executeUpsert($document, $options);
297 83
                $this->handleCollections($document, $options);
298 83
                unset($this->queuedUpserts[$oid]);
299
            } catch (WriteException $e) {
0 ignored issues
show
Bug introduced by
The class MongoDB\Driver\Exception\WriteException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
300
                unset($this->queuedUpserts[$oid]);
301 83
                throw $e;
302
            }
303
        }
304 83
    }
305
306
    /**
307
     * Executes a single upsert in {@link executeUpserts}
308
     *
309
     * @param object $document
310
     * @param array  $options
311
     */
312 83
    private function executeUpsert($document, array $options)
313
    {
314 83
        $options['upsert'] = true;
315 83
        $criteria = $this->getQueryForDocument($document);
316
317 83
        $data = $this->pb->prepareUpsertData($document);
318
319
        // Set the initial version for each upsert
320 83
        if ($this->class->isVersioned) {
321 2
            $versionMapping = $this->class->fieldMappings[$this->class->versionField];
322 2
            $nextVersion = null;
323 2
            if ($versionMapping['type'] === 'int') {
324 1
                $nextVersion = max(1, (int) $this->class->reflFields[$this->class->versionField]->getValue($document));
325 1
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
326 1
            } elseif ($versionMapping['type'] === 'date') {
327 1
                $nextVersionDateTime = new \DateTime();
328 1
                $nextVersion = Type::convertPHPToDatabaseValue($nextVersionDateTime);
329 1
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersionDateTime);
330
            }
331 2
            $data['$set'][$versionMapping['name']] = $nextVersion;
332
        }
333
334 83
        foreach (array_keys($criteria) as $field) {
335 83
            unset($data['$set'][$field]);
336 83
            unset($data['$inc'][$field]);
337 83
            unset($data['$setOnInsert'][$field]);
338
        }
339
340
        // Do not send empty update operators
341 83
        foreach (['$set', '$inc', '$setOnInsert'] as $operator) {
342 83
            if (! empty($data[$operator])) {
343 68
                continue;
344
            }
345
346 83
            unset($data[$operator]);
347
        }
348
349
        /* If there are no modifiers remaining, we're upserting a document with
350
         * an identifier as its only field. Since a document with the identifier
351
         * may already exist, the desired behavior is "insert if not exists" and
352
         * NOOP otherwise. MongoDB 2.6+ does not allow empty modifiers, so $set
353
         * the identifier to the same value in our criteria.
354
         *
355
         * This will fail for versions before MongoDB 2.6, which require an
356
         * empty $set modifier. The best we can do (without attempting to check
357
         * server versions in advance) is attempt the 2.6+ behavior and retry
358
         * after the relevant exception.
359
         *
360
         * See: https://jira.mongodb.org/browse/SERVER-12266
361
         */
362 83
        if (empty($data)) {
363 16
            $retry = true;
364 16
            $data = ['$set' => ['_id' => $criteria['_id']]];
365
        }
366
367
        try {
368 83
            $this->collection->updateOne($criteria, $data, $options);
369 83
            return;
370
        } catch (WriteException $e) {
0 ignored issues
show
Bug introduced by
The class MongoDB\Driver\Exception\WriteException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
371
            if (empty($retry) || strpos($e->getMessage(), 'Mod on _id not allowed') === false) {
372
                throw $e;
373
            }
374
        }
375
376
        $this->collection->updateOne($criteria, ['$set' => new \stdClass()], $options);
377
    }
378
379
    /**
380
     * Updates the already persisted document if it has any new changesets.
381
     *
382
     * @param object $document
383
     * @param array  $options  Array of options to be used with update()
384
     * @throws LockException
385
     */
386 196
    public function update($document, array $options = [])
387
    {
388 196
        $update = $this->pb->prepareUpdateData($document);
389
390 196
        $query = $this->getQueryForDocument($document);
391
392 196
        foreach (array_keys($query) as $field) {
393 196
            unset($update['$set'][$field]);
394
        }
395
396 196
        if (empty($update['$set'])) {
397 90
            unset($update['$set']);
398
        }
399
400
        // Include versioning logic to set the new version value in the database
401
        // and to ensure the version has not changed since this document object instance
402
        // was fetched from the database
403 196
        $nextVersion = null;
404 196
        if ($this->class->isVersioned) {
405 13
            $versionMapping = $this->class->fieldMappings[$this->class->versionField];
406 13
            $currentVersion = $this->class->reflFields[$this->class->versionField]->getValue($document);
407 13
            if ($versionMapping['type'] === 'int') {
408 10
                $nextVersion = $currentVersion + 1;
409 10
                $update['$inc'][$versionMapping['name']] = 1;
410 10
                $query[$versionMapping['name']] = $currentVersion;
411 3
            } elseif ($versionMapping['type'] === 'date') {
412 3
                $nextVersion = new \DateTime();
413 3
                $update['$set'][$versionMapping['name']] = Type::convertPHPToDatabaseValue($nextVersion);
414 3
                $query[$versionMapping['name']] = Type::convertPHPToDatabaseValue($currentVersion);
415
            }
416
        }
417
418 196
        if (! empty($update)) {
419
            // Include locking logic so that if the document object in memory is currently
420
            // locked then it will remove it, otherwise it ensures the document is not locked.
421 129
            if ($this->class->isLockable) {
422 11
                $isLocked = $this->class->reflFields[$this->class->lockField]->getValue($document);
423 11
                $lockMapping = $this->class->fieldMappings[$this->class->lockField];
424 11
                if ($isLocked) {
425 2
                    $update['$unset'] = [$lockMapping['name'] => true];
426
                } else {
427 9
                    $query[$lockMapping['name']] = ['$exists' => false];
428
                }
429
            }
430
431 129
            $options = $this->getWriteOptions($options);
432
433 129
            $result = $this->collection->updateOne($query, $update, $options);
434
435 129
            if (($this->class->isVersioned || $this->class->isLockable) && $result->getModifiedCount() !== 1) {
436 4
                throw LockException::lockFailed($document);
437 125
            } elseif ($this->class->isVersioned) {
438 9
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
439
            }
440
        }
441
442 192
        $this->handleCollections($document, $options);
443 192
    }
444
445
    /**
446
     * Removes document from mongo
447
     *
448
     * @param mixed $document
449
     * @param array $options  Array of options to be used with remove()
450
     * @throws LockException
451
     */
452 34
    public function delete($document, array $options = [])
453
    {
454 34
        $query = $this->getQueryForDocument($document);
455
456 34
        if ($this->class->isLockable) {
457 2
            $query[$this->class->lockField] = ['$exists' => false];
458
        }
459
460 34
        $options = $this->getWriteOptions($options);
461
462 34
        $result = $this->collection->deleteOne($query, $options);
463
464 34
        if (($this->class->isVersioned || $this->class->isLockable) && ! $result->getDeletedCount()) {
465 2
            throw LockException::lockFailed($document);
466
        }
467 32
    }
468
469
    /**
470
     * Refreshes a managed document.
471
     *
472
     * @param object $document The document to refresh.
473
     */
474 20
    public function refresh($document)
475
    {
476 20
        $query = $this->getQueryForDocument($document);
477 20
        $data = $this->collection->findOne($query);
478 20
        if ($data === null) {
479
            throw MongoDBException::cannotRefreshDocument();
480
        }
481 20
        $data = $this->hydratorFactory->hydrate($document, (array) $data);
482 20
        $this->uow->setOriginalDocumentData($document, $data);
483 20
    }
484
485
    /**
486
     * Finds a document by a set of criteria.
487
     *
488
     * If a scalar or MongoDB\BSON\ObjectId is provided for $criteria, it will
489
     * be used to match an _id value.
490
     *
491
     * @param mixed  $criteria Query criteria
492
     * @param object $document Document to load the data into. If not specified, a new document is created.
493
     * @param array  $hints    Hints for document creation
494
     * @param int    $lockMode
495
     * @param array  $sort     Sort array for Cursor::sort()
496
     * @throws LockException
497
     * @return object|null The loaded and managed document instance or null if no document was found
498
     * @todo Check identity map? loadById method? Try to guess whether $criteria is the id?
499
     */
500 344
    public function load($criteria, $document = null, array $hints = [], $lockMode = 0, ?array $sort = null)
0 ignored issues
show
Unused Code introduced by
The parameter $lockMode is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
501
    {
502
        // TODO: remove this
503 344
        if ($criteria === null || is_scalar($criteria) || $criteria instanceof ObjectId) {
0 ignored issues
show
Bug introduced by
The class MongoDB\BSON\ObjectId does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
504
            $criteria = ['_id' => $criteria];
505
        }
506
507 344
        $criteria = $this->prepareQueryOrNewObj($criteria);
0 ignored issues
show
Bug introduced by
It seems like $criteria can also be of type object; however, Doctrine\ODM\MongoDB\Per...:prepareQueryOrNewObj() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
508 344
        $criteria = $this->addDiscriminatorToPreparedQuery($criteria);
509 344
        $criteria = $this->addFilterToPreparedQuery($criteria);
510
511 344
        $options = [];
512 344
        if ($sort !== null) {
513 92
            $options['sort'] = $this->prepareSort($sort);
514
        }
515 344
        $result = $this->collection->findOne($criteria, $options);
516 344
        $result = $result !== null ? (array) $result : null;
517
518 344
        if ($this->class->isLockable) {
519 1
            $lockMapping = $this->class->fieldMappings[$this->class->lockField];
520 1
            if (isset($result[$lockMapping['name']]) && $result[$lockMapping['name']] === LockMode::PESSIMISTIC_WRITE) {
521 1
                throw LockException::lockFailed($result);
522
            }
523
        }
524
525 343
        return $this->createDocument($result, $document, $hints);
526
    }
527
528
    /**
529
     * Finds documents by a set of criteria.
530
     *
531
     * @param array    $criteria Query criteria
532
     * @param array    $sort     Sort array for Cursor::sort()
533
     * @param int|null $limit    Limit for Cursor::limit()
534
     * @param int|null $skip     Skip for Cursor::skip()
535
     * @return Iterator
536
     */
537 22
    public function loadAll(array $criteria = [], ?array $sort = null, $limit = null, $skip = null)
538
    {
539 22
        $criteria = $this->prepareQueryOrNewObj($criteria);
540 22
        $criteria = $this->addDiscriminatorToPreparedQuery($criteria);
541 22
        $criteria = $this->addFilterToPreparedQuery($criteria);
542
543 22
        $options = [];
544 22
        if ($sort !== null) {
545 11
            $options['sort'] = $this->prepareSort($sort);
546
        }
547
548 22
        if ($limit !== null) {
549 10
            $options['limit'] = $limit;
550
        }
551
552 22
        if ($skip !== null) {
553 1
            $options['skip'] = $skip;
554
        }
555
556 22
        $baseCursor = $this->collection->find($criteria, $options);
557 22
        $cursor = $this->wrapCursor($baseCursor);
558
559 22
        return $cursor;
560
    }
561
562
    /**
563
     * @param object $document
564
     *
565
     * @return array
566
     * @throws MongoDBException
567
     */
568 270
    private function getShardKeyQuery($document)
569
    {
570 270
        if (! $this->class->isSharded()) {
571 266
            return [];
572
        }
573
574 4
        $shardKey = $this->class->getShardKey();
575 4
        $keys = array_keys($shardKey['keys']);
576 4
        $data = $this->uow->getDocumentActualData($document);
577
578 4
        $shardKeyQueryPart = [];
579 4
        foreach ($keys as $key) {
580 4
            $mapping = $this->class->getFieldMappingByDbFieldName($key);
581 4
            $this->guardMissingShardKey($document, $key, $data);
582
583 4
            if (isset($mapping['association']) && $mapping['association'] === ClassMetadata::REFERENCE_ONE) {
584 1
                $reference = $this->prepareReference(
585 1
                    $key,
586 1
                    $data[$mapping['fieldName']],
587 1
                    $mapping,
588 1
                    false
589
                );
590 1
                foreach ($reference as $keyValue) {
591 1
                    $shardKeyQueryPart[$keyValue[0]] = $keyValue[1];
592
                }
593
            } else {
594 3
                $value = Type::getType($mapping['type'])->convertToDatabaseValue($data[$mapping['fieldName']]);
595 4
                $shardKeyQueryPart[$key] = $value;
596
            }
597
        }
598
599 4
        return $shardKeyQueryPart;
600
    }
601
602
    /**
603
     * Wraps the supplied base cursor in the corresponding ODM class.
604
     *
605
     */
606 22
    private function wrapCursor(Cursor $baseCursor): Iterator
607
    {
608 22
        return new CachingIterator(new HydratingIterator($baseCursor, $this->dm->getUnitOfWork(), $this->class));
609
    }
610
611
    /**
612
     * Checks whether the given managed document exists in the database.
613
     *
614
     * @param object $document
615
     * @return bool TRUE if the document exists in the database, FALSE otherwise.
616
     */
617 3
    public function exists($document)
618
    {
619 3
        $id = $this->class->getIdentifierObject($document);
620 3
        return (bool) $this->collection->findOne(['_id' => $id], ['_id']);
621
    }
622
623
    /**
624
     * Locks document by storing the lock mode on the mapped lock field.
625
     *
626
     * @param object $document
627
     * @param int    $lockMode
628
     */
629 5
    public function lock($document, $lockMode)
630
    {
631 5
        $id = $this->uow->getDocumentIdentifier($document);
632 5
        $criteria = ['_id' => $this->class->getDatabaseIdentifierValue($id)];
633 5
        $lockMapping = $this->class->fieldMappings[$this->class->lockField];
634 5
        $this->collection->updateOne($criteria, ['$set' => [$lockMapping['name'] => $lockMode]]);
635 5
        $this->class->reflFields[$this->class->lockField]->setValue($document, $lockMode);
636 5
    }
637
638
    /**
639
     * Releases any lock that exists on this document.
640
     *
641
     * @param object $document
642
     */
643 1
    public function unlock($document)
644
    {
645 1
        $id = $this->uow->getDocumentIdentifier($document);
646 1
        $criteria = ['_id' => $this->class->getDatabaseIdentifierValue($id)];
647 1
        $lockMapping = $this->class->fieldMappings[$this->class->lockField];
648 1
        $this->collection->updateOne($criteria, ['$unset' => [$lockMapping['name'] => true]]);
649 1
        $this->class->reflFields[$this->class->lockField]->setValue($document, null);
650 1
    }
651
652
    /**
653
     * Creates or fills a single document object from an query result.
654
     *
655
     * @param array|null $result   The query result.
656
     * @param object     $document The document object to fill, if any.
657
     * @param array      $hints    Hints for document creation.
658
     * @return object|null The filled and managed document object or NULL, if the query result is empty.
659
     */
660 343
    private function createDocument($result, $document = null, array $hints = [])
661
    {
662 343
        if ($result === null) {
663 113
            return null;
664
        }
665
666 299
        if ($document !== null) {
667 24
            $hints[Query::HINT_REFRESH] = true;
668 24
            $id = $this->class->getPHPIdentifierValue($result['_id']);
669 24
            $this->uow->registerManaged($document, $id, $result);
670
        }
671
672 299
        return $this->uow->getOrCreateDocument($this->class->name, $result, $hints, $document);
673
    }
674
675
    /**
676
     * Loads a PersistentCollection data. Used in the initialize() method.
677
     *
678
     */
679 164
    public function loadCollection(PersistentCollectionInterface $collection)
680
    {
681 164
        $mapping = $collection->getMapping();
682 164
        switch ($mapping['association']) {
683
            case ClassMetadata::EMBED_MANY:
684 109
                $this->loadEmbedManyCollection($collection);
685 109
                break;
686
687
            case ClassMetadata::REFERENCE_MANY:
688 77
                if (isset($mapping['repositoryMethod']) && $mapping['repositoryMethod']) {
689 5
                    $this->loadReferenceManyWithRepositoryMethod($collection);
690
                } else {
691 73
                    if ($mapping['isOwningSide']) {
692 61
                        $this->loadReferenceManyCollectionOwningSide($collection);
693
                    } else {
694 17
                        $this->loadReferenceManyCollectionInverseSide($collection);
695
                    }
696
                }
697 77
                break;
698
        }
699 164
    }
700
701 109
    private function loadEmbedManyCollection(PersistentCollectionInterface $collection)
702
    {
703 109
        $embeddedDocuments = $collection->getMongoData();
704 109
        $mapping = $collection->getMapping();
705 109
        $owner = $collection->getOwner();
706 109
        if (! $embeddedDocuments) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $embeddedDocuments of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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.

Loading history...
707 58
            return;
708
        }
709
710 82
        foreach ($embeddedDocuments as $key => $embeddedDocument) {
711 82
            $className = $this->uow->getClassNameForAssociation($mapping, $embeddedDocument);
712 82
            $embeddedMetadata = $this->dm->getClassMetadata($className);
713 82
            $embeddedDocumentObject = $embeddedMetadata->newInstance();
714
715 82
            $this->uow->setParentAssociation($embeddedDocumentObject, $mapping, $owner, $mapping['name'] . '.' . $key);
716
717 82
            $data = $this->hydratorFactory->hydrate($embeddedDocumentObject, $embeddedDocument, $collection->getHints());
718 82
            $id = $data[$embeddedMetadata->identifier] ?? null;
719
720 82
            if (empty($collection->getHints()[Query::HINT_READ_ONLY])) {
721 81
                $this->uow->registerManaged($embeddedDocumentObject, $id, $data);
722
            }
723 82
            if (CollectionHelper::isHash($mapping['strategy'])) {
724 10
                $collection->set($key, $embeddedDocumentObject);
725
            } else {
726 82
                $collection->add($embeddedDocumentObject);
727
            }
728
        }
729 82
    }
730
731 61
    private function loadReferenceManyCollectionOwningSide(PersistentCollectionInterface $collection)
732
    {
733 61
        $hints = $collection->getHints();
734 61
        $mapping = $collection->getMapping();
735 61
        $groupedIds = [];
736
737 61
        $sorted = isset($mapping['sort']) && $mapping['sort'];
738
739 61
        foreach ($collection->getMongoData() as $key => $reference) {
740 55
            $className = $this->uow->getClassNameForAssociation($mapping, $reference);
741 55
            $identifier = ClassMetadata::getReferenceId($reference, $mapping['storeAs']);
742 55
            $id = $this->dm->getClassMetadata($className)->getPHPIdentifierValue($identifier);
743
744
            // create a reference to the class and id
745 55
            $reference = $this->dm->getReference($className, $id);
746
747
            // no custom sort so add the references right now in the order they are embedded
748 55
            if (! $sorted) {
749 54
                if (CollectionHelper::isHash($mapping['strategy'])) {
750 2
                    $collection->set($key, $reference);
751
                } else {
752 52
                    $collection->add($reference);
753
                }
754
            }
755
756
            // only query for the referenced object if it is not already initialized or the collection is sorted
757 55
            if (! (($reference instanceof Proxy && ! $reference->__isInitialized__)) && ! $sorted) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
758 22
                continue;
759
            }
760
761 40
            $groupedIds[$className][] = $identifier;
762
        }
763 61
        foreach ($groupedIds as $className => $ids) {
764 40
            $class = $this->dm->getClassMetadata($className);
765 40
            $mongoCollection = $this->dm->getDocumentCollection($className);
766 40
            $criteria = $this->cm->merge(
767 40
                ['_id' => ['$in' => array_values($ids)]],
768 40
                $this->dm->getFilterCollection()->getFilterCriteria($class),
769 40
                $mapping['criteria'] ?? []
770
            );
771 40
            $criteria = $this->uow->getDocumentPersister($className)->prepareQueryOrNewObj($criteria);
772
773 40
            $options = [];
774 40
            if (isset($mapping['sort'])) {
775 40
                $options['sort'] = $this->prepareSort($mapping['sort']);
776
            }
777 40
            if (isset($mapping['limit'])) {
778
                $options['limit'] = $mapping['limit'];
779
            }
780 40
            if (isset($mapping['skip'])) {
781
                $options['skip'] = $mapping['skip'];
782
            }
783 40
            if (! empty($hints[Query::HINT_READ_PREFERENCE])) {
784
                $options['readPreference'] = $hints[Query::HINT_READ_PREFERENCE];
785
            }
786
787 40
            $cursor = $mongoCollection->find($criteria, $options);
788 40
            $documents = $cursor->toArray();
789 40
            foreach ($documents as $documentData) {
790 39
                $document = $this->uow->getById($documentData['_id'], $class);
791 39
                if ($document instanceof Proxy && ! $document->__isInitialized()) {
792 39
                    $data = $this->hydratorFactory->hydrate($document, $documentData);
793 39
                    $this->uow->setOriginalDocumentData($document, $data);
794 39
                    $document->__isInitialized__ = true;
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
795
                }
796 39
                if (! $sorted) {
797 38
                    continue;
798
                }
799
800 40
                $collection->add($document);
801
            }
802
        }
803 61
    }
804
805 17
    private function loadReferenceManyCollectionInverseSide(PersistentCollectionInterface $collection)
806
    {
807 17
        $query = $this->createReferenceManyInverseSideQuery($collection);
808 17
        $documents = $query->execute()->toArray();
809 17
        foreach ($documents as $key => $document) {
810 16
            $collection->add($document);
811
        }
812 17
    }
813
814
    /**
815
     *
816
     * @return Query
817
     */
818 17
    public function createReferenceManyInverseSideQuery(PersistentCollectionInterface $collection)
819
    {
820 17
        $hints = $collection->getHints();
821 17
        $mapping = $collection->getMapping();
822 17
        $owner = $collection->getOwner();
823 17
        $ownerClass = $this->dm->getClassMetadata(get_class($owner));
824 17
        $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
825 17
        $mappedByMapping = $targetClass->fieldMappings[$mapping['mappedBy']] ?? [];
826 17
        $mappedByFieldName = ClassMetadata::getReferenceFieldName($mappedByMapping['storeAs'] ?? ClassMetadata::REFERENCE_STORE_AS_DB_REF, $mapping['mappedBy']);
827
828 17
        $criteria = $this->cm->merge(
829 17
            [$mappedByFieldName => $ownerClass->getIdentifierObject($owner)],
830 17
            $this->dm->getFilterCollection()->getFilterCriteria($targetClass),
831 17
            $mapping['criteria'] ?? []
832
        );
833 17
        $criteria = $this->uow->getDocumentPersister($mapping['targetDocument'])->prepareQueryOrNewObj($criteria);
834 17
        $qb = $this->dm->createQueryBuilder($mapping['targetDocument'])
835 17
            ->setQueryArray($criteria);
836
837 17
        if (isset($mapping['sort'])) {
838 17
            $qb->sort($mapping['sort']);
839
        }
840 17
        if (isset($mapping['limit'])) {
841 2
            $qb->limit($mapping['limit']);
842
        }
843 17
        if (isset($mapping['skip'])) {
844
            $qb->skip($mapping['skip']);
845
        }
846
847 17
        if (! empty($hints[Query::HINT_READ_PREFERENCE])) {
848
            $qb->setReadPreference($hints[Query::HINT_READ_PREFERENCE]);
849
        }
850
851 17
        foreach ($mapping['prime'] as $field) {
852 4
            $qb->field($field)->prime(true);
853
        }
854
855 17
        return $qb->getQuery();
856
    }
857
858 5
    private function loadReferenceManyWithRepositoryMethod(PersistentCollectionInterface $collection)
859
    {
860 5
        $cursor = $this->createReferenceManyWithRepositoryMethodCursor($collection);
861 5
        $mapping = $collection->getMapping();
862 5
        $documents = $cursor->toArray();
863 5
        foreach ($documents as $key => $obj) {
864 5
            if (CollectionHelper::isHash($mapping['strategy'])) {
865 1
                $collection->set($key, $obj);
866
            } else {
867 5
                $collection->add($obj);
868
            }
869
        }
870 5
    }
871
872
    /**
873
     * @return Iterator
874
     */
875 5
    public function createReferenceManyWithRepositoryMethodCursor(PersistentCollectionInterface $collection)
876
    {
877 5
        $mapping = $collection->getMapping();
878 5
        $repositoryMethod = $mapping['repositoryMethod'];
879 5
        $cursor = $this->dm->getRepository($mapping['targetDocument'])
880 5
            ->$repositoryMethod($collection->getOwner());
881
882 5
        if (! $cursor instanceof Iterator) {
883
            throw new \BadMethodCallException(sprintf('Expected repository method %s to return an iterable object', $repositoryMethod));
884
        }
885
886 5
        if (! empty($mapping['prime'])) {
887 1
            $referencePrimer = new ReferencePrimer($this->dm, $this->dm->getUnitOfWork());
888 1
            $primers = array_combine($mapping['prime'], array_fill(0, count($mapping['prime']), true));
889 1
            $class = $this->dm->getClassMetadata($mapping['targetDocument']);
890
891 1
            $cursor = new PrimingIterator($cursor, $class, $referencePrimer, $primers, $collection->getHints());
892
        }
893
894 5
        return $cursor;
895
    }
896
897
    /**
898
     * Prepare a projection array by converting keys, which are PHP property
899
     * names, to MongoDB field names.
900
     *
901
     * @param array $fields
902
     * @return array
903
     */
904 14
    public function prepareProjection(array $fields)
905
    {
906 14
        $preparedFields = [];
907
908 14
        foreach ($fields as $key => $value) {
909 14
            $preparedFields[$this->prepareFieldName($key)] = $value;
910
        }
911
912 14
        return $preparedFields;
913
    }
914
915
    /**
916
     * @param string|int $sort
917
     * @return string|int
918
     */
919 25
    private function getSortDirection($sort)
920
    {
921 25
        switch (strtolower((string) $sort)) {
922 25
            case 'desc':
923 15
                return -1;
924
925 22
            case 'asc':
926 13
                return 1;
927
        }
928
929 12
        return $sort;
930
    }
931
932
    /**
933
     * Prepare a sort specification array by converting keys to MongoDB field
934
     * names and changing direction strings to int.
935
     *
936
     * @param array $fields
937
     * @return array
938
     */
939 140
    public function prepareSort(array $fields)
940
    {
941 140
        $sortFields = [];
942
943 140
        foreach ($fields as $key => $value) {
944 25
            $sortFields[$this->prepareFieldName($key)] = $this->getSortDirection($value);
945
        }
946
947 140
        return $sortFields;
948
    }
949
950
    /**
951
     * Prepare a mongodb field name and convert the PHP property names to MongoDB field names.
952
     *
953
     * @param string $fieldName
954
     * @return string
955
     */
956 433
    public function prepareFieldName($fieldName)
957
    {
958 433
        $fieldNames = $this->prepareQueryElement($fieldName, null, null, false);
959
960 433
        return $fieldNames[0][0];
961
    }
962
963
    /**
964
     * Adds discriminator 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 before
968
     * {@link DocumentPerister::addFilterToPreparedQuery()}.
969
     *
970
     * @param array $preparedQuery
971
     * @return array
972
     */
973 497
    public function addDiscriminatorToPreparedQuery(array $preparedQuery)
974
    {
975
        /* If the class has a discriminator field, which is not already in the
976
         * criteria, inject it now. The field/values need no preparation.
977
         */
978 497
        if ($this->class->hasDiscriminator() && ! isset($preparedQuery[$this->class->discriminatorField])) {
979 27
            $discriminatorValues = $this->getClassDiscriminatorValues($this->class);
980 27
            if (count($discriminatorValues) === 1) {
981 19
                $preparedQuery[$this->class->discriminatorField] = $discriminatorValues[0];
982
            } else {
983 10
                $preparedQuery[$this->class->discriminatorField] = ['$in' => $discriminatorValues];
984
            }
985
        }
986
987 497
        return $preparedQuery;
988
    }
989
990
    /**
991
     * Adds filter criteria to an already-prepared query.
992
     *
993
     * This method should be used once for query criteria and not be used for
994
     * nested expressions. It should be called after
995
     * {@link DocumentPerister::addDiscriminatorToPreparedQuery()}.
996
     *
997
     * @param array $preparedQuery
998
     * @return array
999
     */
1000 498
    public function addFilterToPreparedQuery(array $preparedQuery)
1001
    {
1002
        /* If filter criteria exists for this class, prepare it and merge
1003
         * over the existing query.
1004
         *
1005
         * @todo Consider recursive merging in case the filter criteria and
1006
         * prepared query both contain top-level $and/$or operators.
1007
         */
1008 498
        $filterCriteria = $this->dm->getFilterCollection()->getFilterCriteria($this->class);
1009 498
        if ($filterCriteria) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $filterCriteria of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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.

Loading history...
1010 18
            $preparedQuery = $this->cm->merge($preparedQuery, $this->prepareQueryOrNewObj($filterCriteria));
1011
        }
1012
1013 498
        return $preparedQuery;
1014
    }
1015
1016
    /**
1017
     * Prepares the query criteria or new document object.
1018
     *
1019
     * PHP field names and types will be converted to those used by MongoDB.
1020
     *
1021
     * @param array $query
1022
     * @param bool  $isNewObj
1023
     * @return array
1024
     */
1025 530
    public function prepareQueryOrNewObj(array $query, $isNewObj = false)
1026
    {
1027 530
        $preparedQuery = [];
1028
1029 530
        foreach ($query as $key => $value) {
1030
            // Recursively prepare logical query clauses
1031 488
            if (in_array($key, ['$and', '$or', '$nor']) && is_array($value)) {
1032 20
                foreach ($value as $k2 => $v2) {
1033 20
                    $preparedQuery[$key][$k2] = $this->prepareQueryOrNewObj($v2, $isNewObj);
1034
                }
1035 20
                continue;
1036
            }
1037
1038 488
            if (isset($key[0]) && $key[0] === '$' && is_array($value)) {
1039 38
                $preparedQuery[$key] = $this->prepareQueryOrNewObj($value, $isNewObj);
1040 38
                continue;
1041
            }
1042
1043 488
            $preparedQueryElements = $this->prepareQueryElement((string) $key, $value, null, true, $isNewObj);
1044 488
            foreach ($preparedQueryElements as list($preparedKey, $preparedValue)) {
1045 488
                $preparedQuery[$preparedKey] = is_array($preparedValue)
1046 134
                    ? array_map('\Doctrine\ODM\MongoDB\Types\Type::convertPHPToDatabaseValue', $preparedValue)
1047 488
                    : Type::convertPHPToDatabaseValue($preparedValue);
1048
            }
1049
        }
1050
1051 530
        return $preparedQuery;
1052
    }
1053
1054
    /**
1055
     * Prepares a query value and converts the PHP value to the database value
1056
     * if it is an identifier.
1057
     *
1058
     * It also handles converting $fieldName to the database name if they are different.
1059
     *
1060
     * @param string        $fieldName
1061
     * @param mixed         $value
1062
     * @param ClassMetadata $class        Defaults to $this->class
1063
     * @param bool          $prepareValue Whether or not to prepare the value
1064
     * @param bool          $inNewObj     Whether or not newObj is being prepared
1065
     * @return array An array of tuples containing prepared field names and values
1066
     */
1067 881
    private function prepareQueryElement($fieldName, $value = null, $class = null, $prepareValue = true, $inNewObj = false)
1068
    {
1069 881
        $class = $class ?? $this->class;
1070
1071
        // @todo Consider inlining calls to ClassMetadata methods
1072
1073
        // Process all non-identifier fields by translating field names
1074 881
        if ($class->hasField($fieldName) && ! $class->isIdentifier($fieldName)) {
1075 247
            $mapping = $class->fieldMappings[$fieldName];
1076 247
            $fieldName = $mapping['name'];
1077
1078 247
            if (! $prepareValue) {
1079 52
                return [[$fieldName, $value]];
1080
            }
1081
1082
            // Prepare mapped, embedded objects
1083 205
            if (! empty($mapping['embedded']) && is_object($value) &&
1084 205
                ! $this->dm->getMetadataFactory()->isTransient(get_class($value))) {
1085 3
                return [[$fieldName, $this->pb->prepareEmbeddedDocumentValue($mapping, $value)]];
1086
            }
1087
1088 203
            if (! empty($mapping['reference']) && is_object($value) && ! ($value instanceof ObjectId)) {
0 ignored issues
show
Bug introduced by
The class MongoDB\BSON\ObjectId does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
1089
                try {
1090 14
                    return $this->prepareReference($fieldName, $value, $mapping, $inNewObj);
1091 1
                } catch (MappingException $e) {
0 ignored issues
show
Bug introduced by
The class Doctrine\Common\Persiste...apping\MappingException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
1092
                    // do nothing in case passed object is not mapped document
1093
                }
1094
            }
1095
1096
            // No further preparation unless we're dealing with a simple reference
1097
            // We can't have expressions in empty() with PHP < 5.5, so store it in a variable
1098 190
            $arrayValue = (array) $value;
1099 190
            if (empty($mapping['reference']) || $mapping['storeAs'] !== ClassMetadata::REFERENCE_STORE_AS_ID || empty($arrayValue)) {
1100 126
                return [[$fieldName, $value]];
1101
            }
1102
1103
            // Additional preparation for one or more simple reference values
1104 91
            $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
1105
1106 91
            if (! is_array($value)) {
1107 87
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1108
            }
1109
1110
            // Objects without operators or with DBRef fields can be converted immediately
1111 6
            if (! $this->hasQueryOperators($value) || $this->hasDBRefFields($value)) {
1112 3
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1113
            }
1114
1115 6
            return [[$fieldName, $this->prepareQueryExpression($value, $targetClass)]];
1116
        }
1117
1118
        // Process identifier fields
1119 794
        if (($class->hasField($fieldName) && $class->isIdentifier($fieldName)) || $fieldName === '_id') {
1120 336
            $fieldName = '_id';
1121
1122 336
            if (! $prepareValue) {
1123 42
                return [[$fieldName, $value]];
1124
            }
1125
1126 297
            if (! is_array($value)) {
1127 271
                return [[$fieldName, $class->getDatabaseIdentifierValue($value)]];
1128
            }
1129
1130
            // Objects without operators or with DBRef fields can be converted immediately
1131 62
            if (! $this->hasQueryOperators($value) || $this->hasDBRefFields($value)) {
1132 6
                return [[$fieldName, $class->getDatabaseIdentifierValue($value)]];
1133
            }
1134
1135 57
            return [[$fieldName, $this->prepareQueryExpression($value, $class)]];
1136
        }
1137
1138
        // No processing for unmapped, non-identifier, non-dotted field names
1139 555
        if (strpos($fieldName, '.') === false) {
1140 413
            return [[$fieldName, $value]];
1141
        }
1142
1143
        /* Process "fieldName.objectProperty" queries (on arrays or objects).
1144
         *
1145
         * We can limit parsing here, since at most three segments are
1146
         * significant: "fieldName.objectProperty" with an optional index or key
1147
         * for collections stored as either BSON arrays or objects.
1148
         */
1149 154
        $e = explode('.', $fieldName, 4);
1150
1151
        // No further processing for unmapped fields
1152 154
        if (! isset($class->fieldMappings[$e[0]])) {
1153 6
            return [[$fieldName, $value]];
1154
        }
1155
1156 149
        $mapping = $class->fieldMappings[$e[0]];
1157 149
        $e[0] = $mapping['name'];
1158
1159
        // Hash and raw fields will not be prepared beyond the field name
1160 149
        if ($mapping['type'] === Type::HASH || $mapping['type'] === Type::RAW) {
1161 1
            $fieldName = implode('.', $e);
1162
1163 1
            return [[$fieldName, $value]];
1164
        }
1165
1166 148
        if ($mapping['type'] === 'many' && CollectionHelper::isHash($mapping['strategy'])
1167 148
                && isset($e[2])) {
1168 1
            $objectProperty = $e[2];
1169 1
            $objectPropertyPrefix = $e[1] . '.';
1170 1
            $nextObjectProperty = implode('.', array_slice($e, 3));
1171 147
        } elseif ($e[1] !== '$') {
1172 146
            $fieldName = $e[0] . '.' . $e[1];
1173 146
            $objectProperty = $e[1];
1174 146
            $objectPropertyPrefix = '';
1175 146
            $nextObjectProperty = implode('.', array_slice($e, 2));
1176 1
        } elseif (isset($e[2])) {
1177 1
            $fieldName = $e[0] . '.' . $e[1] . '.' . $e[2];
1178 1
            $objectProperty = $e[2];
1179 1
            $objectPropertyPrefix = $e[1] . '.';
1180 1
            $nextObjectProperty = implode('.', array_slice($e, 3));
1181
        } else {
1182 1
            $fieldName = $e[0] . '.' . $e[1];
1183
1184 1
            return [[$fieldName, $value]];
1185
        }
1186
1187
        // No further processing for fields without a targetDocument mapping
1188 148
        if (! isset($mapping['targetDocument'])) {
1189 3
            if ($nextObjectProperty) {
1190
                $fieldName .= '.' . $nextObjectProperty;
1191
            }
1192
1193 3
            return [[$fieldName, $value]];
1194
        }
1195
1196 145
        $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
1197
1198
        // No further processing for unmapped targetDocument fields
1199 145
        if (! $targetClass->hasField($objectProperty)) {
1200 25
            if ($nextObjectProperty) {
1201
                $fieldName .= '.' . $nextObjectProperty;
1202
            }
1203
1204 25
            return [[$fieldName, $value]];
1205
        }
1206
1207 125
        $targetMapping = $targetClass->getFieldMapping($objectProperty);
1208 125
        $objectPropertyIsId = $targetClass->isIdentifier($objectProperty);
1209
1210
        // Prepare DBRef identifiers or the mapped field's property path
1211 125
        $fieldName = ($objectPropertyIsId && ! empty($mapping['reference']) && $mapping['storeAs'] !== ClassMetadata::REFERENCE_STORE_AS_ID)
1212 105
            ? ClassMetadata::getReferenceFieldName($mapping['storeAs'], $e[0])
1213 125
            : $e[0] . '.' . $objectPropertyPrefix . $targetMapping['name'];
1214
1215
        // Process targetDocument identifier fields
1216 125
        if ($objectPropertyIsId) {
1217 106
            if (! $prepareValue) {
1218 7
                return [[$fieldName, $value]];
1219
            }
1220
1221 99
            if (! is_array($value)) {
1222 85
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1223
            }
1224
1225
            // Objects without operators or with DBRef fields can be converted immediately
1226 16
            if (! $this->hasQueryOperators($value) || $this->hasDBRefFields($value)) {
1227 6
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1228
            }
1229
1230 16
            return [[$fieldName, $this->prepareQueryExpression($value, $targetClass)]];
1231
        }
1232
1233
        /* The property path may include a third field segment, excluding the
1234
         * collection item pointer. If present, this next object property must
1235
         * be processed recursively.
1236
         */
1237 19
        if ($nextObjectProperty) {
1238
            // Respect the targetDocument's class metadata when recursing
1239 16
            $nextTargetClass = isset($targetMapping['targetDocument'])
1240 10
                ? $this->dm->getClassMetadata($targetMapping['targetDocument'])
1241 16
                : null;
1242
1243 16
            if (empty($targetMapping['reference'])) {
1244 14
                $fieldNames = $this->prepareQueryElement($nextObjectProperty, $value, $nextTargetClass, $prepareValue);
1245
            } else {
1246
                // No recursive processing for references as most probably somebody is querying DBRef or alike
1247 4
                if ($nextObjectProperty[0] !== '$' && in_array($targetMapping['storeAs'], [ClassMetadata::REFERENCE_STORE_AS_DB_REF_WITH_DB, ClassMetadata::REFERENCE_STORE_AS_DB_REF])) {
1248 1
                    $nextObjectProperty = '$' . $nextObjectProperty;
1249
                }
1250 4
                $fieldNames = [[$nextObjectProperty, $value]];
1251
            }
1252
1253
            return array_map(function ($preparedTuple) use ($fieldName) {
1254 16
                list($key, $value) = $preparedTuple;
1255
1256 16
                return [$fieldName . '.' . $key, $value];
1257 16
            }, $fieldNames);
1258
        }
1259
1260 5
        return [[$fieldName, $value]];
1261
    }
1262
1263
    /**
1264
     * Prepares a query expression.
1265
     *
1266
     * @param array|object  $expression
1267
     * @param ClassMetadata $class
1268
     * @return array|object
1269
     */
1270 79
    private function prepareQueryExpression($expression, $class)
1271
    {
1272 79
        foreach ($expression as $k => $v) {
1273
            // Ignore query operators whose arguments need no type conversion
1274 79
            if (in_array($k, ['$exists', '$type', '$mod', '$size'])) {
1275 16
                continue;
1276
            }
1277
1278
            // Process query operators whose argument arrays need type conversion
1279 79
            if (in_array($k, ['$in', '$nin', '$all']) && is_array($v)) {
1280 77
                foreach ($v as $k2 => $v2) {
1281 77
                    $expression[$k][$k2] = $class->getDatabaseIdentifierValue($v2);
1282
                }
1283 77
                continue;
1284
            }
1285
1286
            // Recursively process expressions within a $not operator
1287 18
            if ($k === '$not' && is_array($v)) {
1288 15
                $expression[$k] = $this->prepareQueryExpression($v, $class);
1289 15
                continue;
1290
            }
1291
1292 18
            $expression[$k] = $class->getDatabaseIdentifierValue($v);
1293
        }
1294
1295 79
        return $expression;
1296
    }
1297
1298
    /**
1299
     * Checks whether the value has DBRef fields.
1300
     *
1301
     * This method doesn't check if the the value is a complete DBRef object,
1302
     * although it should return true for a DBRef. Rather, we're checking that
1303
     * the value has one or more fields for a DBref. In practice, this could be
1304
     * $elemMatch criteria for matching a DBRef.
1305
     *
1306
     * @param mixed $value
1307
     * @return bool
1308
     */
1309 80
    private function hasDBRefFields($value)
1310
    {
1311 80
        if (! is_array($value) && ! is_object($value)) {
1312
            return false;
1313
        }
1314
1315 80
        if (is_object($value)) {
1316
            $value = get_object_vars($value);
1317
        }
1318
1319 80
        foreach ($value as $key => $_) {
1320 80
            if ($key === '$ref' || $key === '$id' || $key === '$db') {
1321 80
                return true;
1322
            }
1323
        }
1324
1325 79
        return false;
1326
    }
1327
1328
    /**
1329
     * Checks whether the value has query operators.
1330
     *
1331
     * @param mixed $value
1332
     * @return bool
1333
     */
1334 84
    private function hasQueryOperators($value)
1335
    {
1336 84
        if (! is_array($value) && ! is_object($value)) {
1337
            return false;
1338
        }
1339
1340 84
        if (is_object($value)) {
1341
            $value = get_object_vars($value);
1342
        }
1343
1344 84
        foreach ($value as $key => $_) {
1345 84
            if (isset($key[0]) && $key[0] === '$') {
1346 84
                return true;
1347
            }
1348
        }
1349
1350 11
        return false;
1351
    }
1352
1353
    /**
1354
     * Gets the array of discriminator values for the given ClassMetadata
1355
     *
1356
     * @return array
1357
     */
1358 27
    private function getClassDiscriminatorValues(ClassMetadata $metadata)
1359
    {
1360 27
        $discriminatorValues = [$metadata->discriminatorValue];
1361 27
        foreach ($metadata->subClasses as $className) {
1362 8
            $key = array_search($className, $metadata->discriminatorMap);
1363 8
            if (! $key) {
1364
                continue;
1365
            }
1366
1367 8
            $discriminatorValues[] = $key;
1368
        }
1369
1370
        // If a defaultDiscriminatorValue is set and it is among the discriminators being queries, add NULL to the list
1371 27
        if ($metadata->defaultDiscriminatorValue && array_search($metadata->defaultDiscriminatorValue, $discriminatorValues) !== false) {
1372 2
            $discriminatorValues[] = null;
1373
        }
1374
1375 27
        return $discriminatorValues;
1376
    }
1377
1378 550
    private function handleCollections($document, $options)
1379
    {
1380
        // Collection deletions (deletions of complete collections)
1381 550
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
1382 104
            if (! $this->uow->isCollectionScheduledForDeletion($coll)) {
1383 94
                continue;
1384
            }
1385
1386 30
            $this->cp->delete($coll, $options);
1387
        }
1388
        // Collection updates (deleteRows, updateRows, insertRows)
1389 550
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
1390 104
            if (! $this->uow->isCollectionScheduledForUpdate($coll)) {
1391 28
                continue;
1392
            }
1393
1394 97
            $this->cp->update($coll, $options);
1395
        }
1396
        // Take new snapshots from visited collections
1397 550
        foreach ($this->uow->getVisitedCollections($document) as $coll) {
1398 229
            $coll->takeSnapshot();
1399
        }
1400 550
    }
1401
1402
    /**
1403
     * If the document is new, ignore shard key field value, otherwise throw an exception.
1404
     * Also, shard key field should be present in actual document data.
1405
     *
1406
     * @param object $document
1407
     * @param string $shardKeyField
1408
     * @param array  $actualDocumentData
1409
     *
1410
     * @throws MongoDBException
1411
     */
1412 4
    private function guardMissingShardKey($document, $shardKeyField, $actualDocumentData)
1413
    {
1414 4
        $dcs = $this->uow->getDocumentChangeSet($document);
1415 4
        $isUpdate = $this->uow->isScheduledForUpdate($document);
1416
1417 4
        $fieldMapping = $this->class->getFieldMappingByDbFieldName($shardKeyField);
1418 4
        $fieldName = $fieldMapping['fieldName'];
1419
1420 4
        if ($isUpdate && isset($dcs[$fieldName]) && $dcs[$fieldName][0] !== $dcs[$fieldName][1]) {
1421
            throw MongoDBException::shardKeyFieldCannotBeChanged($shardKeyField, $this->class->getName());
0 ignored issues
show
Bug introduced by
Consider using $this->class->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
1422
        }
1423
1424 4
        if (! isset($actualDocumentData[$fieldName])) {
1425
            throw MongoDBException::shardKeyFieldMissing($shardKeyField, $this->class->getName());
0 ignored issues
show
Bug introduced by
Consider using $this->class->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
1426
        }
1427 4
    }
1428
1429
    /**
1430
     * Get shard key aware query for single document.
1431
     *
1432
     * @param object $document
1433
     *
1434
     * @return array
1435
     */
1436 266
    private function getQueryForDocument($document)
1437
    {
1438 266
        $id = $this->uow->getDocumentIdentifier($document);
1439 266
        $id = $this->class->getDatabaseIdentifierValue($id);
1440
1441 266
        $shardKeyQueryPart = $this->getShardKeyQuery($document);
1442 266
        $query = array_merge(['_id' => $id], $shardKeyQueryPart);
1443
1444 266
        return $query;
1445
    }
1446
1447
    /**
1448
     * @param array $options
1449
     *
1450
     * @return array
1451
     */
1452 551
    private function getWriteOptions(array $options = [])
1453
    {
1454 551
        $defaultOptions = $this->dm->getConfiguration()->getDefaultCommitOptions();
1455 551
        $documentOptions = [];
1456 551
        if ($this->class->hasWriteConcern()) {
1457 9
            $documentOptions['w'] = $this->class->getWriteConcern();
1458
        }
1459
1460 551
        return array_merge($defaultOptions, $documentOptions, $options);
1461
    }
1462
1463
    /**
1464
     * @param string $fieldName
1465
     * @param mixed  $value
1466
     * @param array  $mapping
1467
     * @param bool   $inNewObj
1468
     * @return array
1469
     */
1470 15
    private function prepareReference($fieldName, $value, array $mapping, $inNewObj)
1471
    {
1472 15
        $reference = $this->dm->createReference($value, $mapping);
1473 14
        if ($inNewObj || $mapping['storeAs'] === ClassMetadata::REFERENCE_STORE_AS_ID) {
1474 8
            return [[$fieldName, $reference]];
1475
        }
1476
1477 6
        switch ($mapping['storeAs']) {
1478
            case ClassMetadata::REFERENCE_STORE_AS_REF:
1479
                $keys = ['id' => true];
1480
                break;
1481
1482
            case ClassMetadata::REFERENCE_STORE_AS_DB_REF:
1483
            case ClassMetadata::REFERENCE_STORE_AS_DB_REF_WITH_DB:
1484 6
                $keys = ['$ref' => true, '$id' => true, '$db' => true];
1485
1486 6
                if ($mapping['storeAs'] === ClassMetadata::REFERENCE_STORE_AS_DB_REF) {
1487 5
                    unset($keys['$db']);
1488
                }
1489
1490 6
                if (isset($mapping['targetDocument'])) {
1491 4
                    unset($keys['$ref'], $keys['$db']);
1492
                }
1493 6
                break;
1494
1495
            default:
1496
                throw new \InvalidArgumentException(sprintf('Reference type %s is invalid.', $mapping['storeAs']));
1497
        }
1498
1499 6
        if ($mapping['type'] === 'many') {
1500 2
            return [[$fieldName, ['$elemMatch' => array_intersect_key($reference, $keys)]]];
1501
        }
1502
1503 4
        return array_map(
1504
            function ($key) use ($reference, $fieldName) {
1505 4
                return [$fieldName . '.' . $key, $reference[$key]];
1506 4
            },
1507 4
            array_keys($keys)
1508
        );
1509
    }
1510
}
1511