Completed
Push — master ( f0fe3a...13ea99 )
by Andreas
12s
created

DocumentPersister::delete()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 6

Importance

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