Completed
Push — 1.0.x ( 06c4ca...183ee0 )
by Maciej
11s
created

DocumentPersister::load()   C

Complexity

Conditions 8
Paths 12

Size

Total Lines 28
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 8.0109

Importance

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