Completed
Pull Request — 1.0.x (#1416)
by Maciej
09:32
created

DocumentPersister::loadAll()   B

Complexity

Conditions 4
Paths 8

Size

Total Lines 23
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 4

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 23
ccs 16
cts 16
cp 1
rs 8.7972
cc 4
eloc 13
nc 8
nop 4
crap 4
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 676
    public function __construct(PersistenceBuilder $pb, DocumentManager $dm, EventManager $evm, UnitOfWork $uow, HydratorFactory $hydratorFactory, ClassMetadata $class, CriteriaMerger $cm = null)
128
    {
129 676
        $this->pb = $pb;
130 676
        $this->dm = $dm;
131 676
        $this->evm = $evm;
132 676
        $this->cm = $cm ?: new CriteriaMerger();
133 676
        $this->uow = $uow;
134 676
        $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 676
        $this->class = $class;
136 676
        $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 676
        $this->cp = $this->uow->getCollectionPersister();
138 676
    }
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 479
    public function addInsert($document)
164
    {
165 479
        $this->queuedInserts[spl_object_hash($document)] = $document;
166 479
    }
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 479
    public function executeInserts(array $options = array())
217
    {
218 479
        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 479
        $inserts = array();
223 479
        foreach ($this->queuedInserts as $oid => $document) {
224 479
            $data = $this->pb->prepareInsertData($document);
225
226
            // Set the initial version for each insert
227 478 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 478
            $inserts[$oid] = $data;
241 478
        }
242
243 478
        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 478
                $this->collection->batchInsert($inserts, $options);
246 478
            } catch (\MongoException $e) {
247 7
                $this->queuedInserts = array();
248 7
                throw $e;
249
            }
250 478
        }
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 478
        foreach ($this->queuedInserts as $document) {
257 478
            $this->handleCollections($document, $options);
258 478
        }
259
260 478
        $this->queuedInserts = array();
261 478
    }
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 14
            $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
            }
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 1
    {
463
        // TODO: remove this
464 349
        if ($criteria === null || is_scalar($criteria) || $criteria instanceof \MongoId) {
465
            $criteria = array('_id' => $criteria);
466
        }
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
                $data = $this->hydratorFactory->hydrate($document, $documentData);
717 32
                $this->uow->setOriginalDocumentData($document, $data);
718 32
                $document->__isInitialized__ = true;
719 32
                if ($sorted) {
720 1
                    $collection->add($document);
721 1
                }
722 33
            }
723 50
        }
724 50
    }
725
726 14
    private function loadReferenceManyCollectionInverseSide(PersistentCollection $collection)
727
    {
728 14
        $query = $this->createReferenceManyInverseSideQuery($collection);
729 14
        $documents = $query->execute()->toArray(false);
730 14
        foreach ($documents as $key => $document) {
731 13
            $collection->add($document);
732 14
        }
733 14
    }
734
735
    /**
736
     * @param PersistentCollection $collection
737
     *
738
     * @return Query
739
     */
740 16
    public function createReferenceManyInverseSideQuery(PersistentCollection $collection)
741
    {
742 16
        $hints = $collection->getHints();
743 16
        $mapping = $collection->getMapping();
744 16
        $owner = $collection->getOwner();
745 16
        $ownerClass = $this->dm->getClassMetadata(get_class($owner));
746 16
        $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
747 16
        $mappedByMapping = isset($targetClass->fieldMappings[$mapping['mappedBy']]) ? $targetClass->fieldMappings[$mapping['mappedBy']] : array();
748 16
        $mappedByFieldName = isset($mappedByMapping['simple']) && $mappedByMapping['simple'] ? $mapping['mappedBy'] : $mapping['mappedBy'] . '.$id';
749 16
        $criteria = $this->cm->merge(
750 16
            array($mappedByFieldName => $ownerClass->getIdentifierObject($owner)),
751 16
            $this->dm->getFilterCollection()->getFilterCriteria($targetClass),
752 16
            isset($mapping['criteria']) ? $mapping['criteria'] : array()
753 16
        );
754 16
        $criteria = $this->uow->getDocumentPersister($mapping['targetDocument'])->prepareQueryOrNewObj($criteria);
755 16
        $qb = $this->dm->createQueryBuilder($mapping['targetDocument'])
756 16
            ->setQueryArray($criteria);
757
758 16
        if (isset($mapping['sort'])) {
759 16
            $qb->sort($mapping['sort']);
760 16
        }
761 16
        if (isset($mapping['limit'])) {
762 1
            $qb->limit($mapping['limit']);
763 1
        }
764 16
        if (isset($mapping['skip'])) {
765
            $qb->skip($mapping['skip']);
766
        }
767 16
        if ( ! empty($hints[Query::HINT_SLAVE_OKAY])) {
768
            $qb->slaveOkay(true);
769
        }
770 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...
771
            $qb->setReadPreference($hints[Query::HINT_READ_PREFERENCE], $hints[Query::HINT_READ_PREFERENCE_TAGS]);
772
        }
773
774 16
        return $qb->getQuery();
775
    }
776
777 3
    private function loadReferenceManyWithRepositoryMethod(PersistentCollection $collection)
778
    {
779 3
        $cursor = $this->createReferenceManyWithRepositoryMethodCursor($collection);
780 3
        $mapping = $collection->getMapping();        
781 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...
782 3
        foreach ($documents as $key => $obj) {
783 3
            if (CollectionHelper::isHash($mapping['strategy'])) {
784 1
                $collection->set($key, $obj);
785 1
            } else {
786 2
                $collection->add($obj);
787
            }
788 3
        }
789 3
    }
790
791
    /**
792
     * @param PersistentCollection $collection
793
     *
794
     * @return CursorInterface
795
     */
796 3
    public function createReferenceManyWithRepositoryMethodCursor(PersistentCollection $collection)
797
    {
798 3
        $hints = $collection->getHints();
799 3
        $mapping = $collection->getMapping();
800 3
        $cursor = $this->dm->getRepository($mapping['targetDocument'])
801 3
            ->$mapping['repositoryMethod']($collection->getOwner());
802
803 3
        if ( ! $cursor instanceof CursorInterface) {
804
            throw new \BadMethodCallException("Expected repository method {$mapping['repositoryMethod']} to return a CursorInterface");
805
        }
806
807 3
        if (isset($mapping['sort'])) {
808 3
            $cursor->sort($mapping['sort']);
809 3
        }
810 3
        if (isset($mapping['limit'])) {
811
            $cursor->limit($mapping['limit']);
812
        }
813 3
        if (isset($mapping['skip'])) {
814
            $cursor->skip($mapping['skip']);
815
        }
816 3
        if ( ! empty($hints[Query::HINT_SLAVE_OKAY])) {
817
            $cursor->slaveOkay(true);
818
        }
819 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...
820
            $cursor->setReadPreference($hints[Query::HINT_READ_PREFERENCE], $hints[Query::HINT_READ_PREFERENCE_TAGS]);
821
        }
822
823 3
        return $cursor;
824
    }
825
826
    /**
827
     * Prepare a sort or projection array by converting keys, which are PHP
828
     * property names, to MongoDB field names.
829
     *
830
     * @param array $fields
831
     * @return array
832
     */
833 137
    public function prepareSortOrProjection(array $fields)
834
    {
835 137
        $preparedFields = array();
836
837 137
        foreach ($fields as $key => $value) {
838 33
            $preparedFields[$this->prepareFieldName($key)] = $value;
839 137
        }
840
841 137
        return $preparedFields;
842
    }
843
844
    /**
845
     * Prepare a mongodb field name and convert the PHP property names to MongoDB field names.
846
     *
847
     * @param string $fieldName
848
     * @return string
849
     */
850 85
    public function prepareFieldName($fieldName)
851
    {
852 85
        list($fieldName) = $this->prepareQueryElement($fieldName, null, null, false);
853
854 85
        return $fieldName;
855
    }
856
857
    /**
858
     * Adds discriminator criteria to an already-prepared query.
859
     *
860
     * This method should be used once for query criteria and not be used for
861
     * nested expressions. It should be called before
862
     * {@link DocumentPerister::addFilterToPreparedQuery()}.
863
     *
864
     * @param array $preparedQuery
865
     * @return array
866
     */
867 473
    public function addDiscriminatorToPreparedQuery(array $preparedQuery)
868
    {
869
        /* If the class has a discriminator field, which is not already in the
870
         * criteria, inject it now. The field/values need no preparation.
871
         */
872 473
        if ($this->class->hasDiscriminator() && ! isset($preparedQuery[$this->class->discriminatorField])) {
873 21
            $discriminatorValues = $this->getClassDiscriminatorValues($this->class);
874 21
            if ((count($discriminatorValues) === 1)) {
875 13
                $preparedQuery[$this->class->discriminatorField] = $discriminatorValues[0];
876 13
            } else {
877 10
                $preparedQuery[$this->class->discriminatorField] = array('$in' => $discriminatorValues);
878
            }
879 21
        }
880
881 473
        return $preparedQuery;
882
    }
883
884
    /**
885
     * Adds filter criteria to an already-prepared query.
886
     *
887
     * This method should be used once for query criteria and not be used for
888
     * nested expressions. It should be called after
889
     * {@link DocumentPerister::addDiscriminatorToPreparedQuery()}.
890
     *
891
     * @param array $preparedQuery
892
     * @return array
893
     */
894 474
    public function addFilterToPreparedQuery(array $preparedQuery)
895
    {
896
        /* If filter criteria exists for this class, prepare it and merge
897
         * over the existing query.
898
         *
899
         * @todo Consider recursive merging in case the filter criteria and
900
         * prepared query both contain top-level $and/$or operators.
901
         */
902 474
        if ($filterCriteria = $this->dm->getFilterCollection()->getFilterCriteria($this->class)) {
903 16
            $preparedQuery = $this->cm->merge($preparedQuery, $this->prepareQueryOrNewObj($filterCriteria));
904 16
        }
905
906 474
        return $preparedQuery;
907
    }
908
909
    /**
910
     * Prepares the query criteria or new document object.
911
     *
912
     * PHP field names and types will be converted to those used by MongoDB.
913
     *
914
     * @param array $query
915
     * @return array
916
     */
917 507
    public function prepareQueryOrNewObj(array $query)
918
    {
919 507
        $preparedQuery = array();
920
921 507
        foreach ($query as $key => $value) {
922
            // Recursively prepare logical query clauses
923 469
            if (in_array($key, array('$and', '$or', '$nor')) && is_array($value)) {
924 20
                foreach ($value as $k2 => $v2) {
925 20
                    $preparedQuery[$key][$k2] = $this->prepareQueryOrNewObj($v2);
926 20
                }
927 20
                continue;
928
            }
929
930 469
            if (isset($key[0]) && $key[0] === '$' && is_array($value)) {
931 20
                $preparedQuery[$key] = $this->prepareQueryOrNewObj($value);
932 20
                continue;
933
            }
934
935 469
            list($key, $value) = $this->prepareQueryElement($key, $value, null, true);
936
937 469
            $preparedQuery[$key] = is_array($value)
938 469
                ? array_map('Doctrine\ODM\MongoDB\Types\Type::convertPHPToDatabaseValue', $value)
939 469
                : Type::convertPHPToDatabaseValue($value);
940 507
        }
941
942 507
        return $preparedQuery;
943
    }
944
945
    /**
946
     * Prepares a query value and converts the PHP value to the database value
947
     * if it is an identifier.
948
     *
949
     * It also handles converting $fieldName to the database name if they are different.
950
     *
951
     * @param string $fieldName
952
     * @param mixed $value
953
     * @param ClassMetadata $class        Defaults to $this->class
954
     * @param boolean $prepareValue Whether or not to prepare the value
955
     * @return array        Prepared field name and value
956
     */
957 500
    private function prepareQueryElement($fieldName, $value = null, $class = null, $prepareValue = true)
958
    {
959 500
        $class = isset($class) ? $class : $this->class;
960
961
        // @todo Consider inlining calls to ClassMetadataInfo methods
962
963
        // Process all non-identifier fields by translating field names
964 500
        if ($class->hasField($fieldName) && ! $class->isIdentifier($fieldName)) {
965 231
            $mapping = $class->fieldMappings[$fieldName];
966 231
            $fieldName = $mapping['name'];
967
968 231
            if ( ! $prepareValue) {
969 62
                return array($fieldName, $value);
970
            }
971
972
            // Prepare mapped, embedded objects
973 189
            if ( ! empty($mapping['embedded']) && is_object($value) &&
974 189
                ! $this->dm->getMetadataFactory()->isTransient(get_class($value))) {
975 1
                return array($fieldName, $this->pb->prepareEmbeddedDocumentValue($mapping, $value));
976
            }
977
978
            // No further preparation unless we're dealing with a simple reference
979
            // We can't have expressions in empty() with PHP < 5.5, so store it in a variable
980 189
            $arrayValue = (array) $value;
981 189
            if (empty($mapping['reference']) || empty($mapping['simple']) || empty($arrayValue)) {
982 116
                return array($fieldName, $value);
983
            }
984
985
            // Additional preparation for one or more simple reference values
986 101
            $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
987
988 101
            if ( ! is_array($value)) {
989 97
                return array($fieldName, $targetClass->getDatabaseIdentifierValue($value));
990
            }
991
992
            // Objects without operators or with DBRef fields can be converted immediately
993 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...
994 3
                return array($fieldName, $targetClass->getDatabaseIdentifierValue($value));
995
            }
996
997 6
            return array($fieldName, $this->prepareQueryExpression($value, $targetClass));
998
        }
999
1000
        // Process identifier fields
1001 379
        if (($class->hasField($fieldName) && $class->isIdentifier($fieldName)) || $fieldName === '_id') {
1002 314
            $fieldName = '_id';
1003
1004 314
            if ( ! $prepareValue) {
1005 16
                return array($fieldName, $value);
1006
            }
1007
1008 300
            if ( ! is_array($value)) {
1009 279
                return array($fieldName, $class->getDatabaseIdentifierValue($value));
1010
            }
1011
1012
            // Objects without operators or with DBRef fields can be converted immediately
1013 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...
1014 6
                return array($fieldName, $class->getDatabaseIdentifierValue($value));
1015
            }
1016
1017 47
            return array($fieldName, $this->prepareQueryExpression($value, $class));
1018
        }
1019
1020
        // No processing for unmapped, non-identifier, non-dotted field names
1021 98
        if (strpos($fieldName, '.') === false) {
1022 42
            return array($fieldName, $value);
1023
        }
1024
1025
        /* Process "fieldName.objectProperty" queries (on arrays or objects).
1026
         *
1027
         * We can limit parsing here, since at most three segments are
1028
         * significant: "fieldName.objectProperty" with an optional index or key
1029
         * for collections stored as either BSON arrays or objects.
1030
         */
1031 61
        $e = explode('.', $fieldName, 4);
1032
1033
        // No further processing for unmapped fields
1034 61
        if ( ! isset($class->fieldMappings[$e[0]])) {
1035 3
            return array($fieldName, $value);
1036
        }
1037
1038 59
        $mapping = $class->fieldMappings[$e[0]];
1039 59
        $e[0] = $mapping['name'];
1040
1041
        // Hash and raw fields will not be prepared beyond the field name
1042 59
        if ($mapping['type'] === Type::HASH || $mapping['type'] === Type::RAW) {
1043 1
            $fieldName = implode('.', $e);
1044
1045 1
            return array($fieldName, $value);
1046
        }
1047
1048 58
        if (isset($mapping['strategy']) && CollectionHelper::isHash($mapping['strategy'])
1049 58
                && isset($e[2])) {
1050 1
            $objectProperty = $e[2];
1051 1
            $objectPropertyPrefix = $e[1] . '.';
1052 1
            $nextObjectProperty = implode('.', array_slice($e, 3));
1053 58
        } elseif ($e[1] != '$') {
1054 56
            $fieldName = $e[0] . '.' . $e[1];
1055 56
            $objectProperty = $e[1];
1056 56
            $objectPropertyPrefix = '';
1057 56
            $nextObjectProperty = implode('.', array_slice($e, 2));
1058 57
        } elseif (isset($e[2])) {
1059 1
            $fieldName = $e[0] . '.' . $e[1] . '.' . $e[2];
1060 1
            $objectProperty = $e[2];
1061 1
            $objectPropertyPrefix = $e[1] . '.';
1062 1
            $nextObjectProperty = implode('.', array_slice($e, 3));
1063 1
        } else {
1064 1
            $fieldName = $e[0] . '.' . $e[1];
1065
1066 1
            return array($fieldName, $value);
1067
        }
1068
1069
        // No further processing for fields without a targetDocument mapping
1070 58
        if ( ! isset($mapping['targetDocument'])) {
1071 2
            if ($nextObjectProperty) {
1072
                $fieldName .= '.'.$nextObjectProperty;
1073
            }
1074
1075 2
            return array($fieldName, $value);
1076
        }
1077
1078 56
        $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
1079
1080
        // No further processing for unmapped targetDocument fields
1081 56
        if ( ! $targetClass->hasField($objectProperty)) {
1082 24
            if ($nextObjectProperty) {
1083
                $fieldName .= '.'.$nextObjectProperty;
1084
            }
1085
1086 24
            return array($fieldName, $value);
1087
        }
1088
1089 35
        $targetMapping = $targetClass->getFieldMapping($objectProperty);
1090 35
        $objectPropertyIsId = $targetClass->isIdentifier($objectProperty);
1091
1092
        // Prepare DBRef identifiers or the mapped field's property path
1093 35
        $fieldName = ($objectPropertyIsId && ! empty($mapping['reference']) && empty($mapping['simple']))
1094 35
            ? $e[0] . '.$id'
1095 35
            : $e[0] . '.' . $objectPropertyPrefix . $targetMapping['name'];
1096
1097
        // Process targetDocument identifier fields
1098 35
        if ($objectPropertyIsId) {
1099 14
            if ( ! $prepareValue) {
1100 1
                return array($fieldName, $value);
1101
            }
1102
1103 13
            if ( ! is_array($value)) {
1104 2
                return array($fieldName, $targetClass->getDatabaseIdentifierValue($value));
1105
            }
1106
1107
            // Objects without operators or with DBRef fields can be converted immediately
1108 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...
1109 3
                return array($fieldName, $targetClass->getDatabaseIdentifierValue($value));
1110
            }
1111
1112 12
            return array($fieldName, $this->prepareQueryExpression($value, $targetClass));
1113
        }
1114
1115
        /* The property path may include a third field segment, excluding the
1116
         * collection item pointer. If present, this next object property must
1117
         * be processed recursively.
1118
         */
1119 21
        if ($nextObjectProperty) {
1120
            // Respect the targetDocument's class metadata when recursing
1121 14
            $nextTargetClass = isset($targetMapping['targetDocument'])
1122 14
                ? $this->dm->getClassMetadata($targetMapping['targetDocument'])
1123 14
                : null;
1124
1125 14
            list($key, $value) = $this->prepareQueryElement($nextObjectProperty, $value, $nextTargetClass, $prepareValue);
1126
1127 14
            $fieldName .= '.' . $key;
1128 14
        }
1129
1130 21
        return array($fieldName, $value);
1131
    }
1132
1133
    /**
1134
     * Prepares a query expression.
1135
     *
1136
     * @param array|object  $expression
1137
     * @param ClassMetadata $class
1138
     * @return array
1139
     */
1140 65
    private function prepareQueryExpression($expression, $class)
1141
    {
1142 65
        foreach ($expression as $k => $v) {
1143
            // Ignore query operators whose arguments need no type conversion
1144 65
            if (in_array($k, array('$exists', '$type', '$mod', '$size'))) {
1145 12
                continue;
1146
            }
1147
1148
            // Process query operators whose argument arrays need type conversion
1149 65
            if (in_array($k, array('$in', '$nin', '$all')) && is_array($v)) {
1150 63
                foreach ($v as $k2 => $v2) {
1151 63
                    $expression[$k][$k2] = $class->getDatabaseIdentifierValue($v2);
1152 63
                }
1153 63
                continue;
1154
            }
1155
1156
            // Recursively process expressions within a $not operator
1157 14
            if ($k === '$not' && is_array($v)) {
1158 11
                $expression[$k] = $this->prepareQueryExpression($v, $class);
1159 11
                continue;
1160
            }
1161
1162 14
            $expression[$k] = $class->getDatabaseIdentifierValue($v);
1163 65
        }
1164
1165 65
        return $expression;
1166
    }
1167
1168
    /**
1169
     * Checks whether the value has DBRef fields.
1170
     *
1171
     * This method doesn't check if the the value is a complete DBRef object,
1172
     * although it should return true for a DBRef. Rather, we're checking that
1173
     * the value has one or more fields for a DBref. In practice, this could be
1174
     * $elemMatch criteria for matching a DBRef.
1175
     *
1176
     * @param mixed $value
1177
     * @return boolean
1178
     */
1179 66
    private function hasDBRefFields($value)
1180
    {
1181 66
        if ( ! is_array($value) && ! is_object($value)) {
1182
            return false;
1183
        }
1184
1185 66
        if (is_object($value)) {
1186
            $value = get_object_vars($value);
1187
        }
1188
1189 66
        foreach ($value as $key => $_) {
1190 66
            if ($key === '$ref' || $key === '$id' || $key === '$db') {
1191 3
                return true;
1192
            }
1193 65
        }
1194
1195 65
        return false;
1196
    }
1197
1198
    /**
1199
     * Checks whether the value has query operators.
1200
     *
1201
     * @param mixed $value
1202
     * @return boolean
1203
     */
1204 70
    private function hasQueryOperators($value)
1205
    {
1206 70
        if ( ! is_array($value) && ! is_object($value)) {
1207
            return false;
1208
        }
1209
1210 70
        if (is_object($value)) {
1211
            $value = get_object_vars($value);
1212
        }
1213
1214 70
        foreach ($value as $key => $_) {
1215 70
            if (isset($key[0]) && $key[0] === '$') {
1216 66
                return true;
1217
            }
1218 9
        }
1219
1220 9
        return false;
1221
    }
1222
1223
    /**
1224
     * Gets the array of discriminator values for the given ClassMetadata
1225
     *
1226
     * @param ClassMetadata $metadata
1227
     * @return array
1228
     */
1229 21
    private function getClassDiscriminatorValues(ClassMetadata $metadata)
1230
    {
1231 21
        $discriminatorValues = array($metadata->discriminatorValue);
1232 21
        foreach ($metadata->subClasses as $className) {
1233 8
            if ($key = array_search($className, $metadata->discriminatorMap)) {
1234 8
                $discriminatorValues[] = $key;
1235 8
            }
1236 21
        }
1237
1238
        // If a defaultDiscriminatorValue is set and it is among the discriminators being queries, add NULL to the list
1239 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...
1240 2
            $discriminatorValues[] = null;
1241 2
        }
1242
1243 21
        return $discriminatorValues;
1244
    }
1245
1246 542
    private function handleCollections($document, $options)
1247
    {
1248
        // Collection deletions (deletions of complete collections)
1249 542
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
1250 99
            if ($this->uow->isCollectionScheduledForDeletion($coll)) {
1251 30
                $this->cp->delete($coll, $options);
1252 30
            }
1253 542
        }
1254
        // Collection updates (deleteRows, updateRows, insertRows)
1255 542
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
1256 99
            if ($this->uow->isCollectionScheduledForUpdate($coll)) {
1257 92
                $this->cp->update($coll, $options);
1258 92
            }
1259 542
        }
1260
        // Take new snapshots from visited collections
1261 542
        foreach ($this->uow->getVisitedCollections($document) as $coll) {
1262 227
            $coll->takeSnapshot();
1263 542
        }
1264 542
    }
1265
}
1266