Completed
Push — master ( 2f1eb7...23b3b5 )
by Maciej
10s
created

DocumentPersister::isQueuedForUpsert()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
cc 1
eloc 2
nc 1
nop 1
crap 1
1
<?php
2
/*
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license. For more information, see
17
 * <http://www.doctrine-project.org>.
18
 */
19
20
namespace Doctrine\ODM\MongoDB\Persisters;
21
22
use Doctrine\Common\EventManager;
23
use Doctrine\Common\Persistence\Mapping\MappingException;
24
use Doctrine\MongoDB\CursorInterface;
25
use Doctrine\ODM\MongoDB\Cursor;
26
use Doctrine\ODM\MongoDB\DocumentManager;
27
use Doctrine\ODM\MongoDB\Mapping\ClassMetadataInfo;
28
use Doctrine\ODM\MongoDB\Utility\CollectionHelper;
29
use Doctrine\ODM\MongoDB\Hydrator\HydratorFactory;
30
use Doctrine\ODM\MongoDB\LockException;
31
use Doctrine\ODM\MongoDB\LockMode;
32
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
33
use Doctrine\ODM\MongoDB\MongoDBException;
34
use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionInterface;
35
use Doctrine\ODM\MongoDB\Proxy\Proxy;
36
use Doctrine\ODM\MongoDB\Query\CriteriaMerger;
37
use Doctrine\ODM\MongoDB\Query\Query;
38
use Doctrine\ODM\MongoDB\Types\Type;
39
use Doctrine\ODM\MongoDB\UnitOfWork;
40
41
/**
42
 * The DocumentPersister is responsible for persisting documents.
43
 *
44
 * @since       1.0
45
 */
46
class DocumentPersister
47
{
48
    /**
49
     * The PersistenceBuilder instance.
50
     *
51
     * @var PersistenceBuilder
52
     */
53
    private $pb;
54
55
    /**
56
     * The DocumentManager instance.
57
     *
58
     * @var DocumentManager
59
     */
60
    private $dm;
61
62
    /**
63
     * The EventManager instance
64
     *
65
     * @var EventManager
66
     */
67
    private $evm;
68
69
    /**
70
     * The UnitOfWork instance.
71
     *
72
     * @var UnitOfWork
73
     */
74
    private $uow;
75
76
    /**
77
     * The ClassMetadata instance for the document type being persisted.
78
     *
79
     * @var ClassMetadata
80
     */
81
    private $class;
82
83
    /**
84
     * The MongoCollection instance for this document.
85
     *
86
     * @var \MongoCollection
87
     */
88
    private $collection;
89
90
    /**
91
     * Array of queued inserts for the persister to insert.
92
     *
93
     * @var array
94
     */
95
    private $queuedInserts = array();
96
97
    /**
98
     * Array of queued inserts for the persister to insert.
99
     *
100
     * @var array
101
     */
102
    private $queuedUpserts = array();
103
104
    /**
105
     * The CriteriaMerger instance.
106
     *
107
     * @var CriteriaMerger
108
     */
109
    private $cm;
110
111
    /**
112
     * The CollectionPersister instance.
113
     *
114
     * @var CollectionPersister
115
     */
116
    private $cp;
117
118
    /**
119
     * Initializes this instance.
120
     *
121
     * @param PersistenceBuilder $pb
122
     * @param DocumentManager $dm
123
     * @param EventManager $evm
124
     * @param UnitOfWork $uow
125
     * @param HydratorFactory $hydratorFactory
126
     * @param ClassMetadata $class
127
     * @param CriteriaMerger $cm
128
     */
129 743
    public function __construct(
130
        PersistenceBuilder $pb,
131
        DocumentManager $dm,
132
        EventManager $evm,
133
        UnitOfWork $uow,
134
        HydratorFactory $hydratorFactory,
135
        ClassMetadata $class,
136
        CriteriaMerger $cm = null
137
    ) {
138 743
        $this->pb = $pb;
139 743
        $this->dm = $dm;
140 743
        $this->evm = $evm;
141 743
        $this->cm = $cm ?: new CriteriaMerger();
142 743
        $this->uow = $uow;
143 743
        $this->hydratorFactory = $hydratorFactory;
0 ignored issues
show
Bug introduced by
The property hydratorFactory does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
144 743
        $this->class = $class;
145 743
        $this->collection = $dm->getDocumentCollection($class->name);
0 ignored issues
show
Documentation Bug introduced by
It seems like $dm->getDocumentCollection($class->name) of type object<Doctrine\MongoDB\Collection> is incompatible with the declared type object<MongoCollection> of property $collection.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
146 743
        $this->cp = $this->uow->getCollectionPersister();
147 743
    }
148
149
    /**
150
     * @return array
151
     */
152
    public function getInserts()
153
    {
154
        return $this->queuedInserts;
155
    }
156
157
    /**
158
     * @param object $document
159
     * @return bool
160
     */
161
    public function isQueuedForInsert($document)
162
    {
163
        return isset($this->queuedInserts[spl_object_hash($document)]);
164
    }
165
166
    /**
167
     * Adds a document to the queued insertions.
168
     * The document remains queued until {@link executeInserts} is invoked.
169
     *
170
     * @param object $document The document to queue for insertion.
171
     */
172 524
    public function addInsert($document)
173
    {
174 524
        $this->queuedInserts[spl_object_hash($document)] = $document;
175 524
    }
176
177
    /**
178
     * @return array
179
     */
180
    public function getUpserts()
181
    {
182
        return $this->queuedUpserts;
183
    }
184
185
    /**
186
     * @param object $document
187
     * @return boolean
188
     */
189 55
    public function isQueuedForUpsert($document)
190
    {
191 55
        return isset($this->queuedUpserts[spl_object_hash($document)]);
192
    }
193
194
    /**
195
     * Adds a document to the queued upserts.
196
     * The document remains queued until {@link executeUpserts} is invoked.
197
     *
198
     * @param object $document The document to queue for insertion.
199
     */
200 85
    public function addUpsert($document)
201
    {
202 85
        $this->queuedUpserts[spl_object_hash($document)] = $document;
203 85
    }
204
205
    /**
206
     * Gets the ClassMetadata instance of the document class this persister is used for.
207
     *
208
     * @return ClassMetadata
209
     */
210
    public function getClassMetadata()
211
    {
212
        return $this->class;
213
    }
214
215
    /**
216
     * Executes all queued document insertions.
217
     *
218
     * Queued documents without an ID will inserted in a batch and queued
219
     * documents with an ID will be upserted individually.
220
     *
221
     * If no inserts are queued, invoking this method is a NOOP.
222
     *
223
     * @param array $options Options for batchInsert() and update() driver methods
224
     */
225 524
    public function executeInserts(array $options = array())
226
    {
227 524
        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...
228
            return;
229
        }
230
231 524
        $inserts = array();
232 524
        $options = $this->getWriteOptions($options);
233 524
        foreach ($this->queuedInserts as $oid => $document) {
234 524
            $data = $this->pb->prepareInsertData($document);
235
236
            // Set the initial version for each insert
237 523 View Code Duplication
            if ($this->class->isVersioned) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
238 39
                $versionMapping = $this->class->fieldMappings[$this->class->versionField];
239 39
                if ($versionMapping['type'] === 'int') {
240 37
                    $nextVersion = max(1, (int) $this->class->reflFields[$this->class->versionField]->getValue($document));
241 37
                    $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
242 39
                } elseif ($versionMapping['type'] === 'date') {
243 2
                    $nextVersionDateTime = new \DateTime();
244 2
                    $nextVersion = new \MongoDate($nextVersionDateTime->getTimestamp());
245 2
                    $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersionDateTime);
246 2
                }
247 39
                $data[$versionMapping['name']] = $nextVersion;
0 ignored issues
show
Bug introduced by
The variable $nextVersion does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
248 39
            }
249
250 523
            $inserts[$oid] = $data;
251 523
        }
252
253 523
        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...
254
            try {
255 523
                $this->collection->batchInsert($inserts, $options);
256 523
            } catch (\MongoException $e) {
257 7
                $this->queuedInserts = array();
258 7
                throw $e;
259
            }
260 523
        }
261
262
        /* All collections except for ones using addToSet have already been
263
         * saved. We have left these to be handled separately to avoid checking
264
         * collection for uniqueness on PHP side.
265
         */
266 523
        foreach ($this->queuedInserts as $document) {
267 523
            $this->handleCollections($document, $options);
268 523
        }
269
270 523
        $this->queuedInserts = array();
271 523
    }
272
273
    /**
274
     * Executes all queued document upserts.
275
     *
276
     * Queued documents with an ID are upserted individually.
277
     *
278
     * If no upserts are queued, invoking this method is a NOOP.
279
     *
280
     * @param array $options Options for batchInsert() and update() driver methods
281
     */
282 85
    public function executeUpserts(array $options = array())
283
    {
284 85
        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...
285
            return;
286
        }
287
288 85
        $options = $this->getWriteOptions($options);
289 85
        foreach ($this->queuedUpserts as $oid => $document) {
290
            try {
291 85
                $this->executeUpsert($document, $options);
292 85
                $this->handleCollections($document, $options);
293 85
                unset($this->queuedUpserts[$oid]);
294 85
            } catch (\MongoException $e) {
295
                unset($this->queuedUpserts[$oid]);
296
                throw $e;
297
            }
298 85
        }
299 85
    }
300
301
    /**
302
     * Executes a single upsert in {@link executeUpserts}
303
     *
304
     * @param object $document
305
     * @param array  $options
306
     */
307 85
    private function executeUpsert($document, array $options)
308
    {
309 85
        $options['upsert'] = true;
310 85
        $criteria = $this->getQueryForDocument($document);
311
312 85
        $data = $this->pb->prepareUpsertData($document);
313
314
        // Set the initial version for each upsert
315 85 View Code Duplication
        if ($this->class->isVersioned) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
316 3
            $versionMapping = $this->class->fieldMappings[$this->class->versionField];
317 3
            if ($versionMapping['type'] === 'int') {
318 2
                $nextVersion = max(1, (int) $this->class->reflFields[$this->class->versionField]->getValue($document));
319 2
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
320 3
            } elseif ($versionMapping['type'] === 'date') {
321 1
                $nextVersionDateTime = new \DateTime();
322 1
                $nextVersion = new \MongoDate($nextVersionDateTime->getTimestamp());
323 1
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersionDateTime);
324 1
            }
325 3
            $data['$set'][$versionMapping['name']] = $nextVersion;
0 ignored issues
show
Bug introduced by
The variable $nextVersion does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
326 3
        }
327
328 85
        foreach (array_keys($criteria) as $field) {
329 85
            unset($data['$set'][$field]);
330 85
        }
331
332
        // Do not send an empty $set modifier
333 85
        if (empty($data['$set'])) {
334 17
            unset($data['$set']);
335 17
        }
336
337
        /* If there are no modifiers remaining, we're upserting a document with
338
         * an identifier as its only field. Since a document with the identifier
339
         * may already exist, the desired behavior is "insert if not exists" and
340
         * NOOP otherwise. MongoDB 2.6+ does not allow empty modifiers, so $set
341
         * the identifier to the same value in our criteria.
342
         *
343
         * This will fail for versions before MongoDB 2.6, which require an
344
         * empty $set modifier. The best we can do (without attempting to check
345
         * server versions in advance) is attempt the 2.6+ behavior and retry
346
         * after the relevant exception.
347
         *
348
         * See: https://jira.mongodb.org/browse/SERVER-12266
349
         */
350 85
        if (empty($data)) {
351 18
            $retry = true;
352 17
            $data = array('$set' => array('_id' => $criteria['_id']));
353 17
        }
354
355
        try {
356 85
            $this->collection->update($criteria, $data, $options);
357 85
            return;
358
        } catch (\MongoCursorException $e) {
359
            if (empty($retry) || strpos($e->getMessage(), 'Mod on _id not allowed') === false) {
360
                throw $e;
361
            }
362
        }
363
364
        $this->collection->update($criteria, array('$set' => new \stdClass), $options);
365
    }
366
367
    /**
368
     * Updates the already persisted document if it has any new changesets.
369
     *
370
     * @param object $document
371
     * @param array $options Array of options to be used with update()
372
     * @throws \Doctrine\ODM\MongoDB\LockException
373
     */
374 224
    public function update($document, array $options = array())
375
    {
376 224
        $update = $this->pb->prepareUpdateData($document);
377
378 224
        $query = $this->getQueryForDocument($document);
379
380 222
        foreach (array_keys($query) as $field) {
381 222
            unset($update['$set'][$field]);
382 222
        }
383
384 222
        if (empty($update['$set'])) {
385 94
            unset($update['$set']);
386 94
        }
387
388
389
        // Include versioning logic to set the new version value in the database
390
        // and to ensure the version has not changed since this document object instance
391
        // was fetched from the database
392 222
        if ($this->class->isVersioned) {
393 31
            $versionMapping = $this->class->fieldMappings[$this->class->versionField];
394 31
            $currentVersion = $this->class->reflFields[$this->class->versionField]->getValue($document);
395 31
            if ($versionMapping['type'] === 'int') {
396 28
                $nextVersion = $currentVersion + 1;
397 28
                $update['$inc'][$versionMapping['name']] = 1;
398 28
                $query[$versionMapping['name']] = $currentVersion;
399 28
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
400 31
            } elseif ($versionMapping['type'] === 'date') {
401 3
                $nextVersion = new \DateTime();
402 3
                $update['$set'][$versionMapping['name']] = new \MongoDate($nextVersion->getTimestamp());
403 3
                $query[$versionMapping['name']] = new \MongoDate($currentVersion->getTimestamp());
404 3
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
405 3
            }
406 31
        }
407
408 222
        if ( ! empty($update)) {
409
            // Include locking logic so that if the document object in memory is currently
410
            // locked then it will remove it, otherwise it ensures the document is not locked.
411 154
            if ($this->class->isLockable) {
412 11
                $isLocked = $this->class->reflFields[$this->class->lockField]->getValue($document);
413 11
                $lockMapping = $this->class->fieldMappings[$this->class->lockField];
414 11
                if ($isLocked) {
415 2
                    $update['$unset'] = array($lockMapping['name'] => true);
416 2
                } else {
417 9
                    $query[$lockMapping['name']] = array('$exists' => false);
418
                }
419 11
            }
420
421 154
            $options = $this->getWriteOptions($options);
422
423 154
            $result = $this->collection->update($query, $update, $options);
424
425 154 View Code Duplication
            if (($this->class->isVersioned || $this->class->isLockable) && ! $result['n']) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
426 5
                throw LockException::lockFailed($document);
427
            }
428 150
        }
429
430 218
        $this->handleCollections($document, $options);
431 218
    }
432
433
    /**
434
     * Removes document from mongo
435
     *
436
     * @param mixed $document
437
     * @param array $options Array of options to be used with remove()
438
     * @throws \Doctrine\ODM\MongoDB\LockException
439
     */
440 34
    public function delete($document, array $options = array())
441
    {
442 34
        $query = $this->getQueryForDocument($document);
443
444 34
        if ($this->class->isLockable) {
445 2
            $query[$this->class->lockField] = array('$exists' => false);
446 2
        }
447
448 34
        $options = $this->getWriteOptions($options);
449
450 34
        $result = $this->collection->remove($query, $options);
451
452 34 View Code Duplication
        if (($this->class->isVersioned || $this->class->isLockable) && ! $result['n']) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
453 2
            throw LockException::lockFailed($document);
454
        }
455 32
    }
456
457
    /**
458
     * Refreshes a managed document.
459
     *
460
     * @param string $id
461
     * @param object $document The document to refresh.
462
     *
463
     * @deprecated The first argument is deprecated.
464
     */
465 22
    public function refresh($id, $document)
0 ignored issues
show
Unused Code introduced by
The parameter $id 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...
466
    {
467 21
        $query = $this->getQueryForDocument($document);
468 21
        $data = $this->collection->findOne($query);
469 22
        $data = $this->hydratorFactory->hydrate($document, $data);
470 22
        $this->uow->setOriginalDocumentData($document, $data);
471 21
    }
472
473
    /**
474
     * Finds a document by a set of criteria.
475
     *
476
     * If a scalar or MongoId is provided for $criteria, it will be used to
477
     * match an _id value.
478
     *
479
     * @param mixed   $criteria Query criteria
480
     * @param object  $document Document to load the data into. If not specified, a new document is created.
481
     * @param array   $hints    Hints for document creation
482
     * @param integer $lockMode
483
     * @param array   $sort     Sort array for Cursor::sort()
484
     * @throws \Doctrine\ODM\MongoDB\LockException
485
     * @return object|null The loaded and managed document instance or null if no document was found
486
     * @todo Check identity map? loadById method? Try to guess whether $criteria is the id?
487
     */
488 373
    public function load($criteria, $document = null, array $hints = array(), $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...
489
    {
490
        // TODO: remove this
491 373
        if ($criteria === null || is_scalar($criteria) || $criteria instanceof \MongoId) {
492
            $criteria = array('_id' => $criteria);
493
        }
494
495 373
        $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...
496 373
        $criteria = $this->addDiscriminatorToPreparedQuery($criteria);
497 373
        $criteria = $this->addFilterToPreparedQuery($criteria);
498
499 373
        $cursor = $this->collection->find($criteria);
500
501 373
        if (null !== $sort) {
502 102
            $cursor->sort($this->prepareSortOrProjection($sort));
503 102
        }
504
505 373
        $result = $cursor->getSingleResult();
506
507 373
        if ($this->class->isLockable) {
508 1
            $lockMapping = $this->class->fieldMappings[$this->class->lockField];
509 1
            if (isset($result[$lockMapping['name']]) && $result[$lockMapping['name']] === LockMode::PESSIMISTIC_WRITE) {
510 1
                throw LockException::lockFailed($result);
511
            }
512
        }
513
514 372
        return $this->createDocument($result, $document, $hints);
515
    }
516
517
    /**
518
     * Finds documents by a set of criteria.
519
     *
520
     * @param array        $criteria Query criteria
521
     * @param array        $sort     Sort array for Cursor::sort()
522
     * @param integer|null $limit    Limit for Cursor::limit()
523
     * @param integer|null $skip     Skip for Cursor::skip()
524
     * @return Cursor
525
     */
526 23
    public function loadAll(array $criteria = array(), array $sort = null, $limit = null, $skip = null)
527
    {
528 23
        $criteria = $this->prepareQueryOrNewObj($criteria);
529 23
        $criteria = $this->addDiscriminatorToPreparedQuery($criteria);
530 23
        $criteria = $this->addFilterToPreparedQuery($criteria);
531
532 23
        $baseCursor = $this->collection->find($criteria);
533 23
        $cursor = $this->wrapCursor($baseCursor);
0 ignored issues
show
Documentation introduced by
$baseCursor is of type object<MongoCursor>, but the function expects a object<Doctrine\MongoDB\CursorInterface>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
534
535 23
        if (null !== $sort) {
536 3
            $cursor->sort($sort);
537 3
        }
538
539 23
        if (null !== $limit) {
540 2
            $cursor->limit($limit);
541 2
        }
542
543 23
        if (null !== $skip) {
544 2
            $cursor->skip($skip);
545 2
        }
546
547 23
        return $cursor;
548
    }
549
550
    /**
551
     * @param object $document
552
     *
553
     * @return array
554
     * @throws MongoDBException
555
     */
556 299
    private function getShardKeyQuery($document)
557
    {
558 299
        if ( ! $this->class->isSharded()) {
559 290
            return array();
560
        }
561
562 9
        $shardKey = $this->class->getShardKey();
563 9
        $keys = array_keys($shardKey['keys']);
564 9
        $data = $this->uow->getDocumentActualData($document);
565
566 9
        $shardKeyQueryPart = array();
567 9
        foreach ($keys as $key) {
568 9
            $mapping = $this->class->getFieldMappingByDbFieldName($key);
569 9
            $this->guardMissingShardKey($document, $key, $data);
570 7
            $value = Type::getType($mapping['type'])->convertToDatabaseValue($data[$mapping['fieldName']]);
571 7
            $shardKeyQueryPart[$key] = $value;
572 7
        }
573
574 7
        return $shardKeyQueryPart;
575
    }
576
577
    /**
578
     * Wraps the supplied base cursor in the corresponding ODM class.
579
     *
580
     * @param CursorInterface $baseCursor
581
     * @return Cursor
582
     */
583 23
    private function wrapCursor(CursorInterface $baseCursor)
584
    {
585 23
        return new Cursor($baseCursor, $this->dm->getUnitOfWork(), $this->class);
586
    }
587
588
    /**
589
     * Checks whether the given managed document exists in the database.
590
     *
591
     * @param object $document
592
     * @return boolean TRUE if the document exists in the database, FALSE otherwise.
593
     */
594 3
    public function exists($document)
595
    {
596 3
        $id = $this->class->getIdentifierObject($document);
597 3
        return (boolean) $this->collection->findOne(array('_id' => $id), array('_id'));
598
    }
599
600
    /**
601
     * Locks document by storing the lock mode on the mapped lock field.
602
     *
603
     * @param object $document
604
     * @param int $lockMode
605
     */
606 5
    public function lock($document, $lockMode)
607
    {
608 5
        $id = $this->uow->getDocumentIdentifier($document);
609 5
        $criteria = array('_id' => $this->class->getDatabaseIdentifierValue($id));
610 5
        $lockMapping = $this->class->fieldMappings[$this->class->lockField];
611 5
        $this->collection->update($criteria, array('$set' => array($lockMapping['name'] => $lockMode)));
612 5
        $this->class->reflFields[$this->class->lockField]->setValue($document, $lockMode);
613 5
    }
614
615
    /**
616
     * Releases any lock that exists on this document.
617
     *
618
     * @param object $document
619
     */
620 1
    public function unlock($document)
621
    {
622 1
        $id = $this->uow->getDocumentIdentifier($document);
623 1
        $criteria = array('_id' => $this->class->getDatabaseIdentifierValue($id));
624 1
        $lockMapping = $this->class->fieldMappings[$this->class->lockField];
625 1
        $this->collection->update($criteria, array('$unset' => array($lockMapping['name'] => true)));
626 1
        $this->class->reflFields[$this->class->lockField]->setValue($document, null);
627 1
    }
628
629
    /**
630
     * Creates or fills a single document object from an query result.
631
     *
632
     * @param object $result The query result.
633
     * @param object $document The document object to fill, if any.
634
     * @param array $hints Hints for document creation.
635
     * @return object The filled and managed document object or NULL, if the query result is empty.
636
     */
637 372
    private function createDocument($result, $document = null, array $hints = array())
638
    {
639 372
        if ($result === null) {
640 118
            return null;
641
        }
642
643 319
        if ($document !== null) {
644 38
            $hints[Query::HINT_REFRESH] = true;
645 38
            $id = $this->class->getPHPIdentifierValue($result['_id']);
646 38
            $this->uow->registerManaged($document, $id, $result);
0 ignored issues
show
Documentation introduced by
$result is of type object, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
647 38
        }
648
649 319
        return $this->uow->getOrCreateDocument($this->class->name, $result, $hints, $document);
0 ignored issues
show
Documentation introduced by
$result is of type object, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
650
    }
651
652
    /**
653
     * Loads a PersistentCollection data. Used in the initialize() method.
654
     *
655
     * @param PersistentCollectionInterface $collection
656
     */
657 169
    public function loadCollection(PersistentCollectionInterface $collection)
658
    {
659 169
        $mapping = $collection->getMapping();
660 169
        switch ($mapping['association']) {
661 169
            case ClassMetadata::EMBED_MANY:
662 118
                $this->loadEmbedManyCollection($collection);
663 118
                break;
664
665 68
            case ClassMetadata::REFERENCE_MANY:
666 68
                if (isset($mapping['repositoryMethod']) && $mapping['repositoryMethod']) {
667 3
                    $this->loadReferenceManyWithRepositoryMethod($collection);
668 3
                } else {
669 65
                    if ($mapping['isOwningSide']) {
670 55
                        $this->loadReferenceManyCollectionOwningSide($collection);
671 55
                    } else {
672 14
                        $this->loadReferenceManyCollectionInverseSide($collection);
673
                    }
674
                }
675 68
                break;
676 169
        }
677 169
    }
678
679 118
    private function loadEmbedManyCollection(PersistentCollectionInterface $collection)
680
    {
681 118
        $embeddedDocuments = $collection->getMongoData();
682 118
        $mapping = $collection->getMapping();
683 118
        $owner = $collection->getOwner();
684 118
        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...
685 89
            foreach ($embeddedDocuments as $key => $embeddedDocument) {
686 89
                $className = $this->uow->getClassNameForAssociation($mapping, $embeddedDocument);
687 89
                $embeddedMetadata = $this->dm->getClassMetadata($className);
688 89
                $embeddedDocumentObject = $embeddedMetadata->newInstance();
689
690 89
                $this->uow->setParentAssociation($embeddedDocumentObject, $mapping, $owner, $mapping['name'] . '.' . $key);
691
692 89
                $data = $this->hydratorFactory->hydrate($embeddedDocumentObject, $embeddedDocument, $collection->getHints());
693 89
                $id = $embeddedMetadata->identifier && isset($data[$embeddedMetadata->identifier])
694 89
                    ? $data[$embeddedMetadata->identifier]
695 89
                    : null;
696
                
697 89
                if (empty($collection->getHints()[Query::HINT_READ_ONLY])) {
698 88
                    $this->uow->registerManaged($embeddedDocumentObject, $id, $data);
699 88
                }
700 89
                if (CollectionHelper::isHash($mapping['strategy'])) {
701 25
                    $collection->set($key, $embeddedDocumentObject);
702 25
                } else {
703 71
                    $collection->add($embeddedDocumentObject);
704
                }
705 89
            }
706 89
        }
707 118
    }
708
709 55
    private function loadReferenceManyCollectionOwningSide(PersistentCollectionInterface $collection)
710
    {
711 55
        $hints = $collection->getHints();
712 55
        $mapping = $collection->getMapping();
713 55
        $groupedIds = array();
714
715 55
        $sorted = isset($mapping['sort']) && $mapping['sort'];
716
717 55
        foreach ($collection->getMongoData() as $key => $reference) {
718 50
            if (isset($mapping['storeAs']) && $mapping['storeAs'] === ClassMetadataInfo::REFERENCE_STORE_AS_ID) {
719 5
                $className = $mapping['targetDocument'];
720 5
                $mongoId = $reference;
721 5
            } else {
722 46
                $className = $this->uow->getClassNameForAssociation($mapping, $reference);
723 46
                $mongoId = $reference['$id'];
724
            }
725 50
            $id = $this->dm->getClassMetadata($className)->getPHPIdentifierValue($mongoId);
726
727
            // create a reference to the class and id
728 50
            $reference = $this->dm->getReference($className, $id);
729
730
            // no custom sort so add the references right now in the order they are embedded
731 50
            if ( ! $sorted) {
732 49
                if (CollectionHelper::isHash($mapping['strategy'])) {
733 2
                    $collection->set($key, $reference);
734 2
                } else {
735 47
                    $collection->add($reference);
736
                }
737 49
            }
738
739
            // only query for the referenced object if it is not already initialized or the collection is sorted
740 50
            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...
741 35
                $groupedIds[$className][] = $mongoId;
742 35
            }
743 55
        }
744 55
        foreach ($groupedIds as $className => $ids) {
745 35
            $class = $this->dm->getClassMetadata($className);
746 35
            $mongoCollection = $this->dm->getDocumentCollection($className);
747 35
            $criteria = $this->cm->merge(
748 35
                array('_id' => array('$in' => array_values($ids))),
749 35
                $this->dm->getFilterCollection()->getFilterCriteria($class),
750 35
                isset($mapping['criteria']) ? $mapping['criteria'] : array()
751 35
            );
752 35
            $criteria = $this->uow->getDocumentPersister($className)->prepareQueryOrNewObj($criteria);
753 35
            $cursor = $mongoCollection->find($criteria);
754 35
            if (isset($mapping['sort'])) {
755 35
                $cursor->sort($mapping['sort']);
756 35
            }
757 35
            if (isset($mapping['limit'])) {
758
                $cursor->limit($mapping['limit']);
759
            }
760 35
            if (isset($mapping['skip'])) {
761
                $cursor->skip($mapping['skip']);
762
            }
763 35
            if ( ! empty($hints[Query::HINT_SLAVE_OKAY])) {
764
                $cursor->slaveOkay(true);
765
            }
766 35 View Code Duplication
            if ( ! empty($hints[Query::HINT_READ_PREFERENCE])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
767
                $cursor->setReadPreference($hints[Query::HINT_READ_PREFERENCE], $hints[Query::HINT_READ_PREFERENCE_TAGS]);
768
            }
769 35
            $documents = $cursor->toArray(false);
770 35
            foreach ($documents as $documentData) {
771 34
                $document = $this->uow->getById($documentData['_id'], $class);
772 34
                if ($document instanceof Proxy && ! $document->__isInitialized()) {
773 34
                    $data = $this->hydratorFactory->hydrate($document, $documentData);
774 35
                    $this->uow->setOriginalDocumentData($document, $data);
775 34
                    $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...
776 34
                }
777 34
                if ($sorted) {
778 1
                    $collection->add($document);
779 1
                }
780 35
            }
781 55
        }
782 55
    }
783
784 14
    private function loadReferenceManyCollectionInverseSide(PersistentCollectionInterface $collection)
785
    {
786 14
        $query = $this->createReferenceManyInverseSideQuery($collection);
787 14
        $documents = $query->execute()->toArray(false);
788 14
        foreach ($documents as $key => $document) {
789 13
            $collection->add($document);
790 14
        }
791 14
    }
792
793
    /**
794
     * @param PersistentCollectionInterface $collection
795
     *
796
     * @return Query
797
     */
798 16
    public function createReferenceManyInverseSideQuery(PersistentCollectionInterface $collection)
799
    {
800 16
        $hints = $collection->getHints();
801 16
        $mapping = $collection->getMapping();
802 16
        $owner = $collection->getOwner();
803 16
        $ownerClass = $this->dm->getClassMetadata(get_class($owner));
804 16
        $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
805 16
        $mappedByMapping = isset($targetClass->fieldMappings[$mapping['mappedBy']]) ? $targetClass->fieldMappings[$mapping['mappedBy']] : array();
806 16
        $mappedByFieldName = isset($mappedByMapping['storeAs']) && $mappedByMapping['storeAs'] === ClassMetadataInfo::REFERENCE_STORE_AS_ID ? $mapping['mappedBy'] : $mapping['mappedBy'] . '.$id';
807 16
        $criteria = $this->cm->merge(
808 16
            array($mappedByFieldName => $ownerClass->getIdentifierObject($owner)),
809 16
            $this->dm->getFilterCollection()->getFilterCriteria($targetClass),
810 16
            isset($mapping['criteria']) ? $mapping['criteria'] : array()
811 16
        );
812 16
        $criteria = $this->uow->getDocumentPersister($mapping['targetDocument'])->prepareQueryOrNewObj($criteria);
813 16
        $qb = $this->dm->createQueryBuilder($mapping['targetDocument'])
814 16
            ->setQueryArray($criteria);
815
816 16
        if (isset($mapping['sort'])) {
817 16
            $qb->sort($mapping['sort']);
818 16
        }
819 16
        if (isset($mapping['limit'])) {
820 1
            $qb->limit($mapping['limit']);
821 1
        }
822 16
        if (isset($mapping['skip'])) {
823
            $qb->skip($mapping['skip']);
824
        }
825 16
        if ( ! empty($hints[Query::HINT_SLAVE_OKAY])) {
826
            $qb->slaveOkay(true);
827
        }
828 16 View Code Duplication
        if ( ! empty($hints[Query::HINT_READ_PREFERENCE])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
829
            $qb->setReadPreference($hints[Query::HINT_READ_PREFERENCE], $hints[Query::HINT_READ_PREFERENCE_TAGS]);
830
        }
831
832 16
        return $qb->getQuery();
833
    }
834
835 3
    private function loadReferenceManyWithRepositoryMethod(PersistentCollectionInterface $collection)
836
    {
837 3
        $cursor = $this->createReferenceManyWithRepositoryMethodCursor($collection);
838 3
        $mapping = $collection->getMapping();
839 3
        $documents = $cursor->toArray(false);
0 ignored issues
show
Unused Code introduced by
The call to CursorInterface::toArray() has too many arguments starting with false.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
840 3
        foreach ($documents as $key => $obj) {
841 3
            if (CollectionHelper::isHash($mapping['strategy'])) {
842 1
                $collection->set($key, $obj);
843 1
            } else {
844 2
                $collection->add($obj);
845
            }
846 3
        }
847 3
    }
848
849
    /**
850
     * @param PersistentCollectionInterface $collection
851
     *
852
     * @return CursorInterface
853
     */
854 3
    public function createReferenceManyWithRepositoryMethodCursor(PersistentCollectionInterface $collection)
855
    {
856 3
        $hints = $collection->getHints();
857 3
        $mapping = $collection->getMapping();
858 3
        $repositoryMethod = $mapping['repositoryMethod'];
859 3
        $cursor = $this->dm->getRepository($mapping['targetDocument'])
860 3
            ->$repositoryMethod($collection->getOwner());
861
862 3
        if ( ! $cursor instanceof CursorInterface) {
863
            throw new \BadMethodCallException("Expected repository method {$repositoryMethod} to return a CursorInterface");
864
        }
865
866 3
        if (isset($mapping['sort'])) {
867 3
            $cursor->sort($mapping['sort']);
868 3
        }
869 3
        if (isset($mapping['limit'])) {
870
            $cursor->limit($mapping['limit']);
871
        }
872 3
        if (isset($mapping['skip'])) {
873
            $cursor->skip($mapping['skip']);
874
        }
875 3
        if ( ! empty($hints[Query::HINT_SLAVE_OKAY])) {
876
            $cursor->slaveOkay(true);
877
        }
878 3 View Code Duplication
        if ( ! empty($hints[Query::HINT_READ_PREFERENCE])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
879
            $cursor->setReadPreference($hints[Query::HINT_READ_PREFERENCE], $hints[Query::HINT_READ_PREFERENCE_TAGS]);
880
        }
881
882 3
        return $cursor;
883
    }
884
885
    /**
886
     * Prepare a sort or projection array by converting keys, which are PHP
887
     * property names, to MongoDB field names.
888
     *
889
     * @param array $fields
890
     * @return array
891
     */
892 141
    public function prepareSortOrProjection(array $fields)
893
    {
894 141
        $preparedFields = array();
895
896 141
        foreach ($fields as $key => $value) {
897 35
            $preparedFields[$this->prepareFieldName($key)] = $value;
898 141
        }
899
900 141
        return $preparedFields;
901
    }
902
903
    /**
904
     * Prepare a mongodb field name and convert the PHP property names to MongoDB field names.
905
     *
906
     * @param string $fieldName
907
     * @return string
908
     */
909 89
    public function prepareFieldName($fieldName)
910
    {
911 89
        $fieldNames = $this->prepareQueryElement($fieldName, null, null, false);
912
913 89
        return $fieldNames[0][0];
914
    }
915
916
    /**
917
     * Adds discriminator criteria to an already-prepared query.
918
     *
919
     * This method should be used once for query criteria and not be used for
920
     * nested expressions. It should be called before
921
     * {@link DocumentPerister::addFilterToPreparedQuery()}.
922
     *
923
     * @param array $preparedQuery
924
     * @return array
925
     */
926 511
    public function addDiscriminatorToPreparedQuery(array $preparedQuery)
927
    {
928
        /* If the class has a discriminator field, which is not already in the
929
         * criteria, inject it now. The field/values need no preparation.
930
         */
931 511
        if ($this->class->hasDiscriminator() && ! isset($preparedQuery[$this->class->discriminatorField])) {
932 23
            $discriminatorValues = $this->getClassDiscriminatorValues($this->class);
933 23
            if (count($discriminatorValues) === 1) {
934 15
                $preparedQuery[$this->class->discriminatorField] = $discriminatorValues[0];
935 15
            } else {
936 10
                $preparedQuery[$this->class->discriminatorField] = array('$in' => $discriminatorValues);
937
            }
938 23
        }
939
940 511
        return $preparedQuery;
941
    }
942
943
    /**
944
     * Adds filter criteria to an already-prepared query.
945
     *
946
     * This method should be used once for query criteria and not be used for
947
     * nested expressions. It should be called after
948
     * {@link DocumentPerister::addDiscriminatorToPreparedQuery()}.
949
     *
950
     * @param array $preparedQuery
951
     * @return array
952
     */
953 512
    public function addFilterToPreparedQuery(array $preparedQuery)
954
    {
955
        /* If filter criteria exists for this class, prepare it and merge
956
         * over the existing query.
957
         *
958
         * @todo Consider recursive merging in case the filter criteria and
959
         * prepared query both contain top-level $and/$or operators.
960
         */
961 512
        if ($filterCriteria = $this->dm->getFilterCollection()->getFilterCriteria($this->class)) {
962 18
            $preparedQuery = $this->cm->merge($preparedQuery, $this->prepareQueryOrNewObj($filterCriteria));
963 18
        }
964
965 512
        return $preparedQuery;
966
    }
967
968
    /**
969
     * Prepares the query criteria or new document object.
970
     *
971
     * PHP field names and types will be converted to those used by MongoDB.
972
     *
973
     * @param array $query
974
     * @return array
975
     */
976 542
    public function prepareQueryOrNewObj(array $query)
977
    {
978 542
        $preparedQuery = array();
979
980 542
        foreach ($query as $key => $value) {
981
            // Recursively prepare logical query clauses
982 502
            if (in_array($key, array('$and', '$or', '$nor')) && is_array($value)) {
983 20
                foreach ($value as $k2 => $v2) {
984 20
                    $preparedQuery[$key][$k2] = $this->prepareQueryOrNewObj($v2);
985 20
                }
986 20
                continue;
987
            }
988
989 502
            if (isset($key[0]) && $key[0] === '$' && is_array($value)) {
990 20
                $preparedQuery[$key] = $this->prepareQueryOrNewObj($value);
991 20
                continue;
992
            }
993
994 502
            $preparedQueryElements = $this->prepareQueryElement($key, $value, null, true);
995 502
            foreach ($preparedQueryElements as list($preparedKey, $preparedValue)) {
996 502
                $preparedQuery[$preparedKey] = is_array($preparedValue)
997 502
                    ? array_map('\Doctrine\ODM\MongoDB\Types\Type::convertPHPToDatabaseValue', $preparedValue)
998 502
                    : Type::convertPHPToDatabaseValue($preparedValue);
999 502
            }
1000 542
        }
1001
1002 542
        return $preparedQuery;
1003
    }
1004
1005
    /**
1006
     * Prepares a query value and converts the PHP value to the database value
1007
     * if it is an identifier.
1008
     *
1009
     * It also handles converting $fieldName to the database name if they are different.
1010
     *
1011
     * @param string $fieldName
1012
     * @param mixed $value
1013
     * @param ClassMetadata $class        Defaults to $this->class
1014
     * @param boolean $prepareValue Whether or not to prepare the value
1015
     * @return array An array of tuples containing prepared field names and values
1016
     */
1017 534
    private function prepareQueryElement($fieldName, $value = null, $class = null, $prepareValue = true)
1018
    {
1019 534
        $class = isset($class) ? $class : $this->class;
1020
1021
        // @todo Consider inlining calls to ClassMetadataInfo methods
1022
1023
        // Process all non-identifier fields by translating field names
1024 534
        if ($class->hasField($fieldName) && ! $class->isIdentifier($fieldName)) {
1025 246
            $mapping = $class->fieldMappings[$fieldName];
1026 246
            $fieldName = $mapping['name'];
1027
1028 246
            if ( ! $prepareValue) {
1029 65
                return [[$fieldName, $value]];
1030
            }
1031
1032
            // Prepare mapped, embedded objects
1033 202
            if ( ! empty($mapping['embedded']) && is_object($value) &&
1034 202
                ! $this->dm->getMetadataFactory()->isTransient(get_class($value))) {
1035 3
                return [[$fieldName, $this->pb->prepareEmbeddedDocumentValue($mapping, $value)]];
1036
            }
1037
1038 200
            if (! empty($mapping['reference']) && is_object($value) && ! ($value instanceof \MongoId)) {
1039
                try {
1040 7
                    return $this->prepareDbRefElement($fieldName, $value, $mapping);
1041 1
                } catch (MappingException $e) {
1042
                    // do nothing in case passed object is not mapped document
1043
                }
1044 1
            }
1045
1046
            // No further preparation unless we're dealing with a simple reference
1047
            // We can't have expressions in empty() with PHP < 5.5, so store it in a variable
1048 194
            $arrayValue = (array) $value;
1049 194
            if (empty($mapping['reference']) || $mapping['storeAs'] !== ClassMetadataInfo::REFERENCE_STORE_AS_ID || empty($arrayValue)) {
1050 119
                return [[$fieldName, $value]];
1051
            }
1052
1053
            // Additional preparation for one or more simple reference values
1054 103
            $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
1055
1056 103
            if ( ! is_array($value)) {
1057 99
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1058
            }
1059
1060
            // Objects without operators or with DBRef fields can be converted immediately
1061 6 View Code Duplication
            if ( ! $this->hasQueryOperators($value) || $this->hasDBRefFields($value)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1062 3
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1063
            }
1064
1065 6
            return [[$fieldName, $this->prepareQueryExpression($value, $targetClass)]];
1066
        }
1067
1068
        // Process identifier fields
1069 402
        if (($class->hasField($fieldName) && $class->isIdentifier($fieldName)) || $fieldName === '_id') {
1070 333
            $fieldName = '_id';
1071
1072 333
            if ( ! $prepareValue) {
1073 19
                return [[$fieldName, $value]];
1074
            }
1075
1076 317
            if ( ! is_array($value)) {
1077 294
                return [[$fieldName, $class->getDatabaseIdentifierValue($value)]];
1078
            }
1079
1080
            // Objects without operators or with DBRef fields can be converted immediately
1081 56 View Code Duplication
            if ( ! $this->hasQueryOperators($value) || $this->hasDBRefFields($value)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1082 6
                return [[$fieldName, $class->getDatabaseIdentifierValue($value)]];
1083
            }
1084
1085 51
            return [[$fieldName, $this->prepareQueryExpression($value, $class)]];
1086
        }
1087
1088
        // No processing for unmapped, non-identifier, non-dotted field names
1089 105
        if (strpos($fieldName, '.') === false) {
1090 48
            return [[$fieldName, $value]];
1091
        }
1092
1093
        /* Process "fieldName.objectProperty" queries (on arrays or objects).
1094
         *
1095
         * We can limit parsing here, since at most three segments are
1096
         * significant: "fieldName.objectProperty" with an optional index or key
1097
         * for collections stored as either BSON arrays or objects.
1098
         */
1099 63
        $e = explode('.', $fieldName, 4);
1100
1101
        // No further processing for unmapped fields
1102 63
        if ( ! isset($class->fieldMappings[$e[0]])) {
1103 4
            return [[$fieldName, $value]];
1104
        }
1105
1106 60
        $mapping = $class->fieldMappings[$e[0]];
1107 60
        $e[0] = $mapping['name'];
1108
1109
        // Hash and raw fields will not be prepared beyond the field name
1110 60
        if ($mapping['type'] === Type::HASH || $mapping['type'] === Type::RAW) {
1111 1
            $fieldName = implode('.', $e);
1112
1113 1
            return [[$fieldName, $value]];
1114
        }
1115
1116 59
        if ($mapping['type'] == 'many' && CollectionHelper::isHash($mapping['strategy'])
1117 59
                && isset($e[2])) {
1118 1
            $objectProperty = $e[2];
1119 1
            $objectPropertyPrefix = $e[1] . '.';
1120 1
            $nextObjectProperty = implode('.', array_slice($e, 3));
1121 59
        } elseif ($e[1] != '$') {
1122 57
            $fieldName = $e[0] . '.' . $e[1];
1123 57
            $objectProperty = $e[1];
1124 57
            $objectPropertyPrefix = '';
1125 57
            $nextObjectProperty = implode('.', array_slice($e, 2));
1126 58
        } elseif (isset($e[2])) {
1127 1
            $fieldName = $e[0] . '.' . $e[1] . '.' . $e[2];
1128 1
            $objectProperty = $e[2];
1129 1
            $objectPropertyPrefix = $e[1] . '.';
1130 1
            $nextObjectProperty = implode('.', array_slice($e, 3));
1131 1
        } else {
1132 1
            $fieldName = $e[0] . '.' . $e[1];
1133
1134 1
            return [[$fieldName, $value]];
1135
        }
1136
1137
        // No further processing for fields without a targetDocument mapping
1138 59
        if ( ! isset($mapping['targetDocument'])) {
1139 3
            if ($nextObjectProperty) {
1140
                $fieldName .= '.'.$nextObjectProperty;
1141
            }
1142
1143 3
            return [[$fieldName, $value]];
1144
        }
1145
1146 56
        $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
1147
1148
        // No further processing for unmapped targetDocument fields
1149 56
        if ( ! $targetClass->hasField($objectProperty)) {
1150 24
            if ($nextObjectProperty) {
1151
                $fieldName .= '.'.$nextObjectProperty;
1152
            }
1153
1154 24
            return [[$fieldName, $value]];
1155
        }
1156
1157 35
        $targetMapping = $targetClass->getFieldMapping($objectProperty);
1158 35
        $objectPropertyIsId = $targetClass->isIdentifier($objectProperty);
1159
1160
        // Prepare DBRef identifiers or the mapped field's property path
1161 35
        $fieldName = ($objectPropertyIsId && ! empty($mapping['reference']) && $mapping['storeAs'] !== ClassMetadataInfo::REFERENCE_STORE_AS_ID)
1162 35
            ? $e[0] . '.$id'
1163 35
            : $e[0] . '.' . $objectPropertyPrefix . $targetMapping['name'];
1164
1165
        // Process targetDocument identifier fields
1166 35
        if ($objectPropertyIsId) {
1167 14
            if ( ! $prepareValue) {
1168 1
                return [[$fieldName, $value]];
1169
            }
1170
1171 13
            if ( ! is_array($value)) {
1172 2
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1173
            }
1174
1175
            // Objects without operators or with DBRef fields can be converted immediately
1176 12 View Code Duplication
            if ( ! $this->hasQueryOperators($value) || $this->hasDBRefFields($value)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1177 3
                return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
1178
            }
1179
1180 12
            return [[$fieldName, $this->prepareQueryExpression($value, $targetClass)]];
1181
        }
1182
1183
        /* The property path may include a third field segment, excluding the
1184
         * collection item pointer. If present, this next object property must
1185
         * be processed recursively.
1186
         */
1187 21
        if ($nextObjectProperty) {
1188
            // Respect the targetDocument's class metadata when recursing
1189 14
            $nextTargetClass = isset($targetMapping['targetDocument'])
1190 14
                ? $this->dm->getClassMetadata($targetMapping['targetDocument'])
1191 14
                : null;
1192
1193 14
            $fieldNames = $this->prepareQueryElement($nextObjectProperty, $value, $nextTargetClass, $prepareValue);
1194
1195
            return array_map(function ($preparedTuple) use ($fieldName) {
1196 14
                list($key, $value) = $preparedTuple;
1197
1198 14
                return [$fieldName . '.' . $key, $value];
1199 14
            }, $fieldNames);
1200
        }
1201
1202 9
        return [[$fieldName, $value]];
1203
    }
1204
1205
    /**
1206
     * Prepares a query expression.
1207
     *
1208
     * @param array|object  $expression
1209
     * @param ClassMetadata $class
1210
     * @return array
1211
     */
1212 69
    private function prepareQueryExpression($expression, $class)
1213
    {
1214 69
        foreach ($expression as $k => $v) {
1215
            // Ignore query operators whose arguments need no type conversion
1216 69
            if (in_array($k, array('$exists', '$type', '$mod', '$size'))) {
1217 12
                continue;
1218
            }
1219
1220
            // Process query operators whose argument arrays need type conversion
1221 69
            if (in_array($k, array('$in', '$nin', '$all')) && is_array($v)) {
1222 67
                foreach ($v as $k2 => $v2) {
1223 67
                    $expression[$k][$k2] = $class->getDatabaseIdentifierValue($v2);
1224 67
                }
1225 67
                continue;
1226
            }
1227
1228
            // Recursively process expressions within a $not operator
1229 14
            if ($k === '$not' && is_array($v)) {
1230 11
                $expression[$k] = $this->prepareQueryExpression($v, $class);
1231 11
                continue;
1232
            }
1233
1234 14
            $expression[$k] = $class->getDatabaseIdentifierValue($v);
1235 69
        }
1236
1237 69
        return $expression;
1238
    }
1239
1240
    /**
1241
     * Checks whether the value has DBRef fields.
1242
     *
1243
     * This method doesn't check if the the value is a complete DBRef object,
1244
     * although it should return true for a DBRef. Rather, we're checking that
1245
     * the value has one or more fields for a DBref. In practice, this could be
1246
     * $elemMatch criteria for matching a DBRef.
1247
     *
1248
     * @param mixed $value
1249
     * @return boolean
1250
     */
1251 70
    private function hasDBRefFields($value)
1252
    {
1253 70
        if ( ! is_array($value) && ! is_object($value)) {
1254
            return false;
1255
        }
1256
1257 70
        if (is_object($value)) {
1258
            $value = get_object_vars($value);
1259
        }
1260
1261 70
        foreach ($value as $key => $_) {
1262 70
            if ($key === '$ref' || $key === '$id' || $key === '$db') {
1263 3
                return true;
1264
            }
1265 69
        }
1266
1267 69
        return false;
1268
    }
1269
1270
    /**
1271
     * Checks whether the value has query operators.
1272
     *
1273
     * @param mixed $value
1274
     * @return boolean
1275
     */
1276 74
    private function hasQueryOperators($value)
1277
    {
1278 74
        if ( ! is_array($value) && ! is_object($value)) {
1279
            return false;
1280
        }
1281
1282 74
        if (is_object($value)) {
1283
            $value = get_object_vars($value);
1284
        }
1285
1286 74
        foreach ($value as $key => $_) {
1287 74
            if (isset($key[0]) && $key[0] === '$') {
1288 70
                return true;
1289
            }
1290 9
        }
1291
1292 9
        return false;
1293
    }
1294
1295
    /**
1296
     * Gets the array of discriminator values for the given ClassMetadata
1297
     *
1298
     * @param ClassMetadata $metadata
1299
     * @return array
1300
     */
1301 23
    private function getClassDiscriminatorValues(ClassMetadata $metadata)
1302
    {
1303 23
        $discriminatorValues = array($metadata->discriminatorValue);
1304 23
        foreach ($metadata->subClasses as $className) {
1305 8
            if ($key = array_search($className, $metadata->discriminatorMap)) {
1306 8
                $discriminatorValues[] = $key;
1307 8
            }
1308 23
        }
1309
1310
        // If a defaultDiscriminatorValue is set and it is among the discriminators being queries, add NULL to the list
1311 23 View Code Duplication
        if ($metadata->defaultDiscriminatorValue && array_search($metadata->defaultDiscriminatorValue, $discriminatorValues) !== false) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1312 2
            $discriminatorValues[] = null;
1313 2
        }
1314
1315 23
        return $discriminatorValues;
1316
    }
1317
1318 596
    private function handleCollections($document, $options)
1319
    {
1320
        // Collection deletions (deletions of complete collections)
1321 596
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
1322 106
            if ($this->uow->isCollectionScheduledForDeletion($coll)) {
1323 31
                $this->cp->delete($coll, $options);
1324 31
            }
1325 596
        }
1326
        // Collection updates (deleteRows, updateRows, insertRows)
1327 596
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
1328 106
            if ($this->uow->isCollectionScheduledForUpdate($coll)) {
1329 98
                $this->cp->update($coll, $options);
1330 98
            }
1331 596
        }
1332
        // Take new snapshots from visited collections
1333 596
        foreach ($this->uow->getVisitedCollections($document) as $coll) {
1334 249
            $coll->takeSnapshot();
1335 596
        }
1336 596
    }
1337
1338
    /**
1339
     * If the document is new, ignore shard key field value, otherwise throw an exception.
1340
     * Also, shard key field should be present in actual document data.
1341
     *
1342
     * @param object $document
1343
     * @param string $shardKeyField
1344
     * @param array  $actualDocumentData
1345
     *
1346
     * @throws MongoDBException
1347
     */
1348 9
    private function guardMissingShardKey($document, $shardKeyField, $actualDocumentData)
1349
    {
1350 9
        $dcs = $this->uow->getDocumentChangeSet($document);
1351 9
        $isUpdate = $this->uow->isScheduledForUpdate($document);
1352
1353 9
        $fieldMapping = $this->class->getFieldMappingByDbFieldName($shardKeyField);
1354 9
        $fieldName = $fieldMapping['fieldName'];
1355
1356 9
        if ($isUpdate && isset($dcs[$fieldName]) && $dcs[$fieldName][0] != $dcs[$fieldName][1]) {
1357 2
            throw MongoDBException::shardKeyFieldCannotBeChanged($shardKeyField, $this->class->getName());
1358
        }
1359
1360 7
        if (!isset($actualDocumentData[$fieldName])) {
1361
            throw MongoDBException::shardKeyFieldMissing($shardKeyField, $this->class->getName());
1362
        }
1363 7
    }
1364
1365
    /**
1366
     * Get shard key aware query for single document.
1367
     *
1368
     * @param object $document
1369
     *
1370
     * @return array
1371
     */
1372 296
    private function getQueryForDocument($document)
1373
    {
1374 296
        $id = $this->uow->getDocumentIdentifier($document);
1375 296
        $id = $this->class->getDatabaseIdentifierValue($id);
1376
1377 296
        $shardKeyQueryPart = $this->getShardKeyQuery($document);
1378 294
        $query = array_merge(array('_id' => $id), $shardKeyQueryPart);
1379
1380 294
        return $query;
1381
    }
1382
1383
    /**
1384
     * @param array $options
1385
     *
1386
     * @return array
1387
     */
1388 597
    private function getWriteOptions(array $options = array())
1389
    {
1390 597
        $defaultOptions = $this->dm->getConfiguration()->getDefaultCommitOptions();
1391 597
        $documentOptions = [];
1392 597
        if ($this->class->hasWriteConcern()) {
1393 9
            $documentOptions['w'] = $this->class->getWriteConcern();
1394 9
        }
1395
1396 597
        return array_merge($defaultOptions, $documentOptions, $options);
1397
    }
1398
1399
    /**
1400
     * @param string $fieldName
1401
     * @param mixed $value
1402
     * @param array $mapping
1403
     * @return array
1404
     */
1405 7
    private function prepareDbRefElement($fieldName, $value, array $mapping)
1406
    {
1407 7
        $dbRef = $this->dm->createDBRef($value, $mapping);
1408 6
        $keys = ['$ref' => true, '$id' => true, '$db' => true];
1409 6
        if ($mapping['storeAs'] === ClassMetadataInfo::REFERENCE_STORE_AS_ID) {
1410 2
            unset($keys['$db']);
1411 2
        }
1412 6
        if (isset($mapping['targetDocument'])) {
1413 5
            unset($keys['$ref'], $keys['$db']);
1414 5
        }
1415
1416 6
        if ($mapping['storeAs'] === ClassMetadataInfo::REFERENCE_STORE_AS_ID) {
1417 2
            return [[$fieldName, $dbRef]];
1418 4
        } elseif ($mapping['type'] === 'many') {
1419 2
            return [[$fieldName, ['$elemMatch' => array_intersect_key($dbRef, $keys)]]];
1420
        } else {
1421 2
            return array_map(
1422 2
                function ($key) use ($dbRef, $fieldName) {
1423 2
                    return [$fieldName . '.' . $key, $dbRef[$key]];
1424 2
                },
1425 2
                array_keys($keys)
1426 2
            );
1427
        }
1428
    }
1429
}
1430