Completed
Push — 1.0.x ( 55bff8...8c7398 )
by
unknown
09:45
created

DocumentPersister::prepareQueryElement()   F

Complexity

Conditions 44
Paths 304

Size

Total Lines 175
Code Lines 85

Duplication

Lines 9
Ratio 5.14 %

Code Coverage

Tests 84
CRAP Score 44.1824

Importance

Changes 2
Bugs 1 Features 0
Metric Value
c 2
b 1
f 0
dl 9
loc 175
ccs 84
cts 88
cp 0.9545
rs 3.3333
cc 44
eloc 85
nc 304
nop 4
crap 44.1824

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 668
    public function __construct(PersistenceBuilder $pb, DocumentManager $dm, EventManager $evm, UnitOfWork $uow, HydratorFactory $hydratorFactory, ClassMetadata $class, CriteriaMerger $cm = null)
128
    {
129 668
        $this->pb = $pb;
130 668
        $this->dm = $dm;
131 668
        $this->evm = $evm;
132 668
        $this->cm = $cm ?: new CriteriaMerger();
133 668
        $this->uow = $uow;
134 668
        $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 668
        $this->class = $class;
136 668
        $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 668
        $this->cp = $this->uow->getCollectionPersister();
138 668
    }
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 474
    public function addInsert($document)
164
    {
165 474
        $this->queuedInserts[spl_object_hash($document)] = $document;
166 474
    }
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 474
    public function executeInserts(array $options = array())
217
    {
218 474
        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 474
        $inserts = array();
223 474
        foreach ($this->queuedInserts as $oid => $document) {
224 474
            $data = $this->pb->prepareInsertData($document);
225
226
            // Set the initial version for each insert
227 473 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 473
            $inserts[$oid] = $data;
241 473
        }
242
243 473
        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 473
                $this->collection->batchInsert($inserts, $options);
246 473
            } catch (\MongoException $e) {
247 7
                $this->queuedInserts = array();
248 7
                throw $e;
249
            }
250 473
        }
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 473
        foreach ($this->queuedInserts as $document) {
257 473
            $this->handleCollections($document, $options);
258 473
        }
259
260 473
        $this->queuedInserts = array();
261 473
    }
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 206
    public function update($document, array $options = array())
361
    {
362 206
        $id = $this->uow->getDocumentIdentifier($document);
363 206
        $update = $this->pb->prepareUpdateData($document);
364
365 206
        $id = $this->class->getDatabaseIdentifierValue($id);
366 206
        $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 206
        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 206
        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 146
            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 146
            $result = $this->collection->update($query, $update, $options);
401
402 146 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 142
        }
406
407 202
        $this->handleCollections($document, $options);
408 202
    }
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 348
    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 348
        if ($criteria === null || is_scalar($criteria) || $criteria instanceof \MongoId) {
465
            $criteria = array('_id' => $criteria);
466
        }
467
468 348
        $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 348
        $criteria = $this->addDiscriminatorToPreparedQuery($criteria);
470 348
        $criteria = $this->addFilterToPreparedQuery($criteria);
471
472 348
        $cursor = $this->collection->find($criteria);
473
474 348
        if (null !== $sort) {
475 100
            $cursor->sort($this->prepareSortOrProjection($sort));
476 100
        }
477
478 348
        $result = $cursor->getSingleResult();
479
480 348
        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 347
        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 20
    public function loadAll(array $criteria = array(), array $sort = null, $limit = null, $skip = null)
500
    {
501 20
        $criteria = $this->prepareQueryOrNewObj($criteria);
502 20
        $criteria = $this->addDiscriminatorToPreparedQuery($criteria);
503 20
        $criteria = $this->addFilterToPreparedQuery($criteria);
504
505 20
        $baseCursor = $this->collection->find($criteria);
506 20
        $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 20
        if (null !== $sort) {
509 3
            $cursor->sort($sort);
510 3
        }
511
512 20
        if (null !== $limit) {
513 2
            $cursor->limit($limit);
514 2
        }
515
516 20
        if (null !== $skip) {
517 2
            $cursor->skip($skip);
518 2
        }
519
520 20
        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 20
    private function wrapCursor(CursorInterface $baseCursor)
530
    {
531 20
        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 347
    private function createDocument($result, $document = null, array $hints = array())
584
    {
585 347
        if ($result === null) {
586 114
            return null;
587
        }
588
589 295
        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 295
        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 157
    public function loadCollection(PersistentCollection $collection)
604
    {
605 157
        $mapping = $collection->getMapping();
606 157
        switch ($mapping['association']) {
607 157
            case ClassMetadata::EMBED_MANY:
608 111
                $this->loadEmbedManyCollection($collection);
609 111
                break;
610
611 62
            case ClassMetadata::REFERENCE_MANY:
612 62
                if (isset($mapping['repositoryMethod']) && $mapping['repositoryMethod']) {
613 3
                    $this->loadReferenceManyWithRepositoryMethod($collection);
614 3
                } else {
615 59
                    if ($mapping['isOwningSide']) {
616 49
                        $this->loadReferenceManyCollectionOwningSide($collection);
617 49
                    } else {
618 14
                        $this->loadReferenceManyCollectionInverseSide($collection);
619
                    }
620
                }
621 62
                break;
622 157
        }
623 157
    }
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 49
    private function loadReferenceManyCollectionOwningSide(PersistentCollection $collection)
654
    {
655 49
        $hints = $collection->getHints();
656 49
        $mapping = $collection->getMapping();
657 49
        $groupedIds = array();
658
659 49
        $sorted = isset($mapping['sort']) && $mapping['sort'];
660
661 49
        foreach ($collection->getMongoData() as $key => $reference) {
662 44
            if (isset($mapping['simple']) && $mapping['simple']) {
663 4
                $className = $mapping['targetDocument'];
664 4
                $mongoId = $reference;
665 4
            } else {
666 40
                $className = $this->uow->getClassNameForAssociation($mapping, $reference);
667 40
                $mongoId = $reference['$id'];
668
            }
669 44
            $id = $this->dm->getClassMetadata($className)->getPHPIdentifierValue($mongoId);
670
671
            // create a reference to the class and id
672 44
            $reference = $this->dm->getReference($className, $id);
673
674
            // no custom sort so add the references right now in the order they are embedded
675 44
            if ( ! $sorted) {
676 43
                if (CollectionHelper::isHash($mapping['strategy'])) {
677 2
                    $collection->set($key, $reference);
678 2
                } else {
679 41
                    $collection->add($reference);
680
                }
681 43
            }
682
683
            // only query for the referenced object if it is not already initialized or the collection is sorted
684 44
            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 32
                $groupedIds[$className][] = $mongoId;
686 32
            }
687 49
        }
688 49
        foreach ($groupedIds as $className => $ids) {
689 32
            $class = $this->dm->getClassMetadata($className);
690 32
            $mongoCollection = $this->dm->getDocumentCollection($className);
691 32
            $criteria = $this->cm->merge(
692 32
                array('_id' => array('$in' => array_values($ids))),
693 32
                $this->dm->getFilterCollection()->getFilterCriteria($class),
694 32
                isset($mapping['criteria']) ? $mapping['criteria'] : array()
695 32
            );
696 32
            $criteria = $this->uow->getDocumentPersister($className)->prepareQueryOrNewObj($criteria);
697 32
            $cursor = $mongoCollection->find($criteria);
698 32
            if (isset($mapping['sort'])) {
699 32
                $cursor->sort($mapping['sort']);
700 32
            }
701 32
            if (isset($mapping['limit'])) {
702
                $cursor->limit($mapping['limit']);
703
            }
704 32
            if (isset($mapping['skip'])) {
705
                $cursor->skip($mapping['skip']);
706
            }
707 32
            if ( ! empty($hints[Query::HINT_SLAVE_OKAY])) {
708
                $cursor->slaveOkay(true);
709
            }
710 32 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 32
            $documents = $cursor->toArray(false);
714 32
            foreach ($documents as $documentData) {
715 31
                $document = $this->uow->getById($documentData['_id'], $class);
716 31
                $data = $this->hydratorFactory->hydrate($document, $documentData);
717 31
                $this->uow->setOriginalDocumentData($document, $data);
718 31
                $document->__isInitialized__ = true;
719 31
                if ($sorted) {
720 1
                    $collection->add($document);
721 1
                }
722 32
            }
723 49
        }
724 49
    }
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 136
    public function prepareSortOrProjection(array $fields)
834
    {
835 136
        $preparedFields = array();
836
837 136
        foreach ($fields as $key => $value) {
838 32
            $preparedFields[$this->prepareFieldName($key)] = $value;
839 136
        }
840
841 136
        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 84
    public function prepareFieldName($fieldName)
851
    {
852 84
        list($fieldName) = $this->prepareQueryElement($fieldName, null, null, false);
853
854 84
        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 467
    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 467
        if ($this->class->hasDiscriminator() && ! isset($preparedQuery[$this->class->discriminatorField])) {
873 18
            $discriminatorValues = $this->getClassDiscriminatorValues($this->class);
874 18
            $preparedQuery[$this->class->discriminatorField] = array('$in' => $discriminatorValues);
875 18
        }
876
877 467
        return $preparedQuery;
878
    }
879
880
    /**
881
     * Adds filter criteria to an already-prepared query.
882
     *
883
     * This method should be used once for query criteria and not be used for
884
     * nested expressions. It should be called after
885
     * {@link DocumentPerister::addDiscriminatorToPreparedQuery()}.
886
     *
887
     * @param array $preparedQuery
888
     * @return array
889
     */
890 468
    public function addFilterToPreparedQuery(array $preparedQuery)
891
    {
892
        /* If filter criteria exists for this class, prepare it and merge
893
         * over the existing query.
894
         *
895
         * @todo Consider recursive merging in case the filter criteria and
896
         * prepared query both contain top-level $and/$or operators.
897
         */
898 468
        if ($filterCriteria = $this->dm->getFilterCollection()->getFilterCriteria($this->class)) {
899 16
            $preparedQuery = $this->cm->merge($preparedQuery, $this->prepareQueryOrNewObj($filterCriteria));
900 16
        }
901
902 468
        return $preparedQuery;
903
    }
904
905
    /**
906
     * Prepares the query criteria or new document object.
907
     *
908
     * PHP field names and types will be converted to those used by MongoDB.
909
     *
910
     * @param array $query
911
     * @return array
912
     */
913 501
    public function prepareQueryOrNewObj(array $query)
914
    {
915 501
        $preparedQuery = array();
916
917 501
        foreach ($query as $key => $value) {
918
            // Recursively prepare logical query clauses
919 463
            if (in_array($key, array('$and', '$or', '$nor')) && is_array($value)) {
920 20
                foreach ($value as $k2 => $v2) {
921 20
                    $preparedQuery[$key][$k2] = $this->prepareQueryOrNewObj($v2);
922 20
                }
923 20
                continue;
924
            }
925
926 463
            if (isset($key[0]) && $key[0] === '$' && is_array($value)) {
927 17
                $preparedQuery[$key] = $this->prepareQueryOrNewObj($value);
928 17
                continue;
929
            }
930
931 463
            list($key, $value) = $this->prepareQueryElement($key, $value, null, true);
932
933 463
            $preparedQuery[$key] = is_array($value)
934 463
                ? array_map('Doctrine\ODM\MongoDB\Types\Type::convertPHPToDatabaseValue', $value)
935 463
                : Type::convertPHPToDatabaseValue($value);
936 501
        }
937
938 501
        return $preparedQuery;
939
    }
940
941
    /**
942
     * Prepares a query value and converts the PHP value to the database value
943
     * if it is an identifier.
944
     *
945
     * It also handles converting $fieldName to the database name if they are different.
946
     *
947
     * @param string $fieldName
948
     * @param mixed $value
949
     * @param ClassMetadata $class        Defaults to $this->class
950
     * @param boolean $prepareValue Whether or not to prepare the value
951
     * @return array        Prepared field name and value
952
     */
953 494
    private function prepareQueryElement($fieldName, $value = null, $class = null, $prepareValue = true)
954
    {
955 494
        $class = isset($class) ? $class : $this->class;
956
957
        // @todo Consider inlining calls to ClassMetadataInfo methods
958
959
        // Process all non-identifier fields by translating field names
960 494
        if ($class->hasField($fieldName) && ! $class->isIdentifier($fieldName)) {
961 226
            $mapping = $class->fieldMappings[$fieldName];
962 226
            $fieldName = $mapping['name'];
963
964 226
            if ( ! $prepareValue) {
965 62
                return array($fieldName, $value);
966
            }
967
968
            // Prepare mapped, embedded objects
969 184
            if ( ! empty($mapping['embedded']) && is_object($value) &&
970 184
                ! $this->dm->getMetadataFactory()->isTransient(get_class($value))) {
971 1
                return array($fieldName, $this->pb->prepareEmbeddedDocumentValue($mapping, $value));
972
            }
973
974
            // No further preparation unless we're dealing with a simple reference
975
            // We can't have expressions in empty() with PHP < 5.5, so store it in a variable
976 184
            $arrayValue = (array) $value;
977 184
            if (empty($mapping['reference']) || empty($mapping['simple']) || empty($arrayValue)) {
978 113
                return array($fieldName, $value);
979
            }
980
981
            // Additional preparation for one or more simple reference values
982 99
            $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
983
984 99
            if ( ! is_array($value)) {
985 94
                return array($fieldName, $targetClass->getDatabaseIdentifierValue($value));
986
            }
987
988
            // Objects without operators or with DBRef fields can be converted immediately
989 7 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...
990 3
                return array($fieldName, $targetClass->getDatabaseIdentifierValue($value));
991
            }
992
993 7
            return array($fieldName, $this->prepareQueryExpression($value, $targetClass));
994
        }
995
996
        // Process identifier fields
997 376
        if (($class->hasField($fieldName) && $class->isIdentifier($fieldName)) || $fieldName === '_id') {
998 312
            $fieldName = '_id';
999
1000 312
            if ( ! $prepareValue) {
1001 15
                return array($fieldName, $value);
1002
            }
1003
1004 299
            if ( ! is_array($value)) {
1005 278
                return array($fieldName, $class->getDatabaseIdentifierValue($value));
1006
            }
1007
1008
            // Objects without operators or with DBRef fields can be converted immediately
1009 51 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...
1010 6
                return array($fieldName, $class->getDatabaseIdentifierValue($value));
1011
            }
1012
1013 46
            return array($fieldName, $this->prepareQueryExpression($value, $class));
1014
        }
1015
1016
        // No processing for unmapped, non-identifier, non-dotted field names
1017 97
        if (strpos($fieldName, '.') === false) {
1018 41
            return array($fieldName, $value);
1019
        }
1020
1021
        /* Process "fieldName.objectProperty" queries (on arrays or objects).
1022
         *
1023
         * We can limit parsing here, since at most three segments are
1024
         * significant: "fieldName.objectProperty" with an optional index or key
1025
         * for collections stored as either BSON arrays or objects.
1026
         */
1027 61
        $e = explode('.', $fieldName, 4);
1028
1029
        // No further processing for unmapped fields
1030 61
        if ( ! isset($class->fieldMappings[$e[0]])) {
1031 3
            return array($fieldName, $value);
1032
        }
1033
1034 59
        $mapping = $class->fieldMappings[$e[0]];
1035 59
        $e[0] = $mapping['name'];
1036
1037
        // Hash and raw fields will not be prepared beyond the field name
1038 59
        if ($mapping['type'] === Type::HASH || $mapping['type'] === Type::RAW) {
1039 1
            $fieldName = implode('.', $e);
1040
1041 1
            return array($fieldName, $value);
1042
        }
1043
1044 58
        if (isset($mapping['strategy']) && CollectionHelper::isHash($mapping['strategy'])
1045 58
                && isset($e[2])) {
1046 1
            $objectProperty = $e[2];
1047 1
            $objectPropertyPrefix = $e[1] . '.';
1048 1
            $nextObjectProperty = implode('.', array_slice($e, 3));
1049 58
        } elseif ($e[1] != '$') {
1050 56
            $fieldName = $e[0] . '.' . $e[1];
1051 56
            $objectProperty = $e[1];
1052 56
            $objectPropertyPrefix = '';
1053 56
            $nextObjectProperty = implode('.', array_slice($e, 2));
1054 57
        } elseif (isset($e[2])) {
1055 1
            $fieldName = $e[0] . '.' . $e[1] . '.' . $e[2];
1056 1
            $objectProperty = $e[2];
1057 1
            $objectPropertyPrefix = $e[1] . '.';
1058 1
            $nextObjectProperty = implode('.', array_slice($e, 3));
1059 1
        } else {
1060 1
            $fieldName = $e[0] . '.' . $e[1];
1061
1062 1
            return array($fieldName, $value);
1063
        }
1064
1065
        // No further processing for fields without a targetDocument mapping
1066 58
        if ( ! isset($mapping['targetDocument'])) {
1067 2
            if ($nextObjectProperty) {
1068
                $fieldName .= '.'.$nextObjectProperty;
1069
            }
1070
1071 2
            return array($fieldName, $value);
1072
        }
1073
1074 56
        $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
1075
1076
        // No further processing for unmapped targetDocument fields
1077 56
        if ( ! $targetClass->hasField($objectProperty)) {
1078 24
            if ($nextObjectProperty) {
1079
                $fieldName .= '.'.$nextObjectProperty;
1080
            }
1081
1082 24
            return array($fieldName, $value);
1083
        }
1084
1085 35
        $targetMapping = $targetClass->getFieldMapping($objectProperty);
1086 35
        $objectPropertyIsId = $targetClass->isIdentifier($objectProperty);
1087
1088
        // Prepare DBRef identifiers or the mapped field's property path
1089 35
        $fieldName = ($objectPropertyIsId && ! empty($mapping['reference']) && empty($mapping['simple']))
1090 35
            ? $e[0] . '.$id'
1091 35
            : $e[0] . '.' . $objectPropertyPrefix . $targetMapping['name'];
1092
1093
        // Process targetDocument identifier fields
1094 35
        if ($objectPropertyIsId) {
1095 14
            if ( ! $prepareValue) {
1096 1
                return array($fieldName, $value);
1097
            }
1098
1099 13
            if ( ! is_array($value)) {
1100 2
                return array($fieldName, $targetClass->getDatabaseIdentifierValue($value));
1101
            }
1102
1103
            // Objects without operators or with DBRef fields can be converted immediately
1104 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...
1105 3
                return array($fieldName, $targetClass->getDatabaseIdentifierValue($value));
1106
            }
1107
1108 12
            return array($fieldName, $this->prepareQueryExpression($value, $targetClass));
1109
        }
1110
1111
        /* The property path may include a third field segment, excluding the
1112
         * collection item pointer. If present, this next object property must
1113
         * be processed recursively.
1114
         */
1115 21
        if ($nextObjectProperty) {
1116
            // Respect the targetDocument's class metadata when recursing
1117 14
            $nextTargetClass = isset($targetMapping['targetDocument'])
1118 14
                ? $this->dm->getClassMetadata($targetMapping['targetDocument'])
1119 14
                : null;
1120
1121 14
            list($key, $value) = $this->prepareQueryElement($nextObjectProperty, $value, $nextTargetClass, $prepareValue);
1122
1123 14
            $fieldName .= '.' . $key;
1124 14
        }
1125
1126 21
        return array($fieldName, $value);
1127
    }
1128
1129
    /**
1130
     * Prepares a query expression.
1131
     *
1132
     * @param array|object  $expression
1133
     * @param ClassMetadata $class
1134
     * @return array
1135
     */
1136 65
    private function prepareQueryExpression($expression, $class)
1137
    {
1138 65
        foreach ($expression as $k => $v) {
1139
            // Ignore query operators whose arguments need no type conversion
1140 65
            if (in_array($k, array('$exists', '$type', '$mod', '$size'))) {
1141 12
                continue;
1142
            }
1143
1144
            // Process query operators whose argument arrays need type conversion
1145 65
            if (in_array($k, array('$in', '$nin', '$all')) && is_array($v)) {
1146 62
                foreach ($v as $k2 => $v2) {
1147 62
                    $expression[$k][$k2] = $class->getDatabaseIdentifierValue($v2);
1148 62
                }
1149 62
                continue;
1150
            }
1151
1152
            // Recursively process expressions within a $not operator
1153 15
            if ($k === '$not' && is_array($v)) {
1154 11
                $expression[$k] = $this->prepareQueryExpression($v, $class);
1155 11
                continue;
1156
            }
1157
1158 15
            $expression[$k] = $class->getDatabaseIdentifierValue($v);
1159 65
        }
1160
1161 65
        return $expression;
1162
    }
1163
1164
    /**
1165
     * Checks whether the value has DBRef fields.
1166
     *
1167
     * This method doesn't check if the the value is a complete DBRef object,
1168
     * although it should return true for a DBRef. Rather, we're checking that
1169
     * the value has one or more fields for a DBref. In practice, this could be
1170
     * $elemMatch criteria for matching a DBRef.
1171
     *
1172
     * @param mixed $value
1173
     * @return boolean
1174
     */
1175 66
    private function hasDBRefFields($value)
1176
    {
1177 66
        if ( ! is_array($value) && ! is_object($value)) {
1178
            return false;
1179
        }
1180
1181 66
        if (is_object($value)) {
1182
            $value = get_object_vars($value);
1183
        }
1184
1185 66
        foreach ($value as $key => $_) {
1186 66
            if ($key === '$ref' || $key === '$id' || $key === '$db') {
1187 3
                return true;
1188
            }
1189 65
        }
1190
1191 65
        return false;
1192
    }
1193
1194
    /**
1195
     * Checks whether the value has query operators.
1196
     *
1197
     * @param mixed $value
1198
     * @return boolean
1199
     */
1200 70
    private function hasQueryOperators($value)
1201
    {
1202 70
        if ( ! is_array($value) && ! is_object($value)) {
1203
            return false;
1204
        }
1205
1206 70
        if (is_object($value)) {
1207
            $value = get_object_vars($value);
1208
        }
1209
1210 70
        foreach ($value as $key => $_) {
1211 70
            if (isset($key[0]) && $key[0] === '$') {
1212 66
                return true;
1213
            }
1214 9
        }
1215
1216 9
        return false;
1217
    }
1218
1219
    /**
1220
     * Gets the array of discriminator values for the given ClassMetadata
1221
     *
1222
     * @param ClassMetadata $metadata
1223
     * @return array
1224
     */
1225 18
    private function getClassDiscriminatorValues(ClassMetadata $metadata)
1226
    {
1227 18
        $discriminatorValues = array($metadata->discriminatorValue);
1228 18
        foreach ($metadata->subClasses as $className) {
1229 7
            if ($key = array_search($className, $metadata->discriminatorMap)) {
1230 7
                $discriminatorValues[] = $key;
1231 7
            }
1232 18
        }
1233
1234
        // If a defaultDiscriminatorValue is set and it is among the discriminators being queries, add NULL to the list
1235 18 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...
1236 2
            $discriminatorValues[] = null;
1237 2
        }
1238
1239 18
        return $discriminatorValues;
1240
    }
1241
1242 537
    private function handleCollections($document, $options)
1243
    {
1244
        // Collection deletions (deletions of complete collections)
1245 537
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
1246 97
            if ($this->uow->isCollectionScheduledForDeletion($coll)) {
1247 30
                $this->cp->delete($coll, $options);
1248 30
            }
1249 537
        }
1250
        // Collection updates (deleteRows, updateRows, insertRows)
1251 537
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
1252 97
            if ($this->uow->isCollectionScheduledForUpdate($coll)) {
1253 90
                $this->cp->update($coll, $options);
1254 90
            }
1255 537
        }
1256
        // Take new snapshots from visited collections
1257 537
        foreach ($this->uow->getVisitedCollections($document) as $coll) {
1258 225
            $coll->takeSnapshot();
1259 537
        }
1260 537
    }
1261
}
1262