Completed
Push — master ( 92ca1d...eb8ea0 )
by Maciej
24:43 queued 20:14
created

DocumentPersister::addUpsert()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1
Metric Value
dl 0
loc 4
ccs 3
cts 3
cp 1
rs 10
cc 1
eloc 2
nc 1
nop 1
crap 1
1
<?php
2
/*
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license. For more information, see
17
 * <http://www.doctrine-project.org>.
18
 */
19
20
namespace Doctrine\ODM\MongoDB\Persisters;
21
22
use Doctrine\Common\EventManager;
23
use Doctrine\Common\Persistence\Mapping\MappingException;
24
use Doctrine\MongoDB\CursorInterface;
25
use Doctrine\ODM\MongoDB\Cursor;
26
use Doctrine\ODM\MongoDB\DocumentManager;
27
use Doctrine\ODM\MongoDB\Utility\CollectionHelper;
28
use Doctrine\ODM\MongoDB\Hydrator\HydratorFactory;
29
use Doctrine\ODM\MongoDB\LockException;
30
use Doctrine\ODM\MongoDB\LockMode;
31
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
32
use Doctrine\ODM\MongoDB\PersistentCollection;
33
use Doctrine\ODM\MongoDB\Proxy\Proxy;
34
use Doctrine\ODM\MongoDB\Query\CriteriaMerger;
35
use Doctrine\ODM\MongoDB\Query\Query;
36
use Doctrine\ODM\MongoDB\Types\Type;
37
use Doctrine\ODM\MongoDB\UnitOfWork;
38
39
/**
40
 * The DocumentPersister is responsible for persisting documents.
41
 *
42
 * @since       1.0
43
 */
44
class DocumentPersister
45
{
46
    /**
47
     * The PersistenceBuilder instance.
48
     *
49
     * @var PersistenceBuilder
50
     */
51
    private $pb;
52
53
    /**
54
     * The DocumentManager instance.
55
     *
56
     * @var DocumentManager
57
     */
58
    private $dm;
59
60
    /**
61
     * The EventManager instance
62
     *
63
     * @var EventManager
64
     */
65
    private $evm;
66
67
    /**
68
     * The UnitOfWork instance.
69
     *
70
     * @var UnitOfWork
71
     */
72
    private $uow;
73
74
    /**
75
     * The ClassMetadata instance for the document type being persisted.
76
     *
77
     * @var ClassMetadata
78
     */
79
    private $class;
80
81
    /**
82
     * The MongoCollection instance for this document.
83
     *
84
     * @var \MongoCollection
85
     */
86
    private $collection;
87
88
    /**
89
     * Array of queued inserts for the persister to insert.
90
     *
91
     * @var array
92
     */
93
    private $queuedInserts = array();
94
95
    /**
96
     * Array of queued inserts for the persister to insert.
97
     *
98
     * @var array
99
     */
100
    private $queuedUpserts = array();
101
102
    /**
103
     * The CriteriaMerger instance.
104
     *
105
     * @var CriteriaMerger
106
     */
107
    private $cm;
108
109
    /**
110
     * The CollectionPersister instance.
111
     *
112
     * @var CollectionPersister
113
     */
114
    private $cp;
115
116
    /**
117
     * Initializes this instance.
118
     *
119
     * @param PersistenceBuilder $pb
120
     * @param DocumentManager $dm
121
     * @param EventManager $evm
122
     * @param UnitOfWork $uow
123
     * @param HydratorFactory $hydratorFactory
124
     * @param ClassMetadata $class
125
     * @param CriteriaMerger $cm
126
     */
127 685
    public function __construct(
128
        PersistenceBuilder $pb,
129
        DocumentManager $dm,
130
        EventManager $evm,
131
        UnitOfWork $uow,
132
        HydratorFactory $hydratorFactory,
133
        ClassMetadata $class,
134
        CriteriaMerger $cm = null
135
    ) {
136 685
        $this->pb = $pb;
137 685
        $this->dm = $dm;
138 685
        $this->evm = $evm;
139 685
        $this->cm = $cm ?: new CriteriaMerger();
140 685
        $this->uow = $uow;
141 685
        $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...
142 685
        $this->class = $class;
143 685
        $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...
144 685
        $this->cp = $this->uow->getCollectionPersister();
145 685
    }
146
147
    /**
148
     * @return array
149
     */
150
    public function getInserts()
151
    {
152
        return $this->queuedInserts;
153
    }
154
155
    /**
156
     * @param object $document
157
     * @return bool
158
     */
159
    public function isQueuedForInsert($document)
160
    {
161
        return isset($this->queuedInserts[spl_object_hash($document)]);
162
    }
163
164
    /**
165
     * Adds a document to the queued insertions.
166
     * The document remains queued until {@link executeInserts} is invoked.
167
     *
168
     * @param object $document The document to queue for insertion.
169
     */
170 486
    public function addInsert($document)
171
    {
172 486
        $this->queuedInserts[spl_object_hash($document)] = $document;
173 486
    }
174
175
    /**
176
     * @return array
177
     */
178
    public function getUpserts()
179
    {
180
        return $this->queuedUpserts;
181
    }
182
183
    /**
184
     * @param object $document
185
     * @return boolean
186
     */
187
    public function isQueuedForUpsert($document)
188
    {
189
        return isset($this->queuedUpserts[spl_object_hash($document)]);
190
    }
191
192
    /**
193
     * Adds a document to the queued upserts.
194
     * The document remains queued until {@link executeUpserts} is invoked.
195
     *
196
     * @param object $document The document to queue for insertion.
197
     */
198 76
    public function addUpsert($document)
199
    {
200 76
        $this->queuedUpserts[spl_object_hash($document)] = $document;
201 76
    }
202
203
    /**
204
     * Gets the ClassMetadata instance of the document class this persister is used for.
205
     *
206
     * @return ClassMetadata
207
     */
208
    public function getClassMetadata()
209
    {
210
        return $this->class;
211
    }
212
213
    /**
214
     * Executes all queued document insertions.
215
     *
216
     * Queued documents without an ID will inserted in a batch and queued
217
     * documents with an ID will be upserted individually.
218
     *
219
     * If no inserts are queued, invoking this method is a NOOP.
220
     *
221
     * @param array $options Options for batchInsert() and update() driver methods
222
     */
223 486
    public function executeInserts(array $options = array())
224
    {
225 486
        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...
226
            return;
227
        }
228
229 486
        $inserts = array();
230 486
        foreach ($this->queuedInserts as $oid => $document) {
231 486
            $data = $this->pb->prepareInsertData($document);
232
233
            // Set the initial version for each insert
234 485 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...
235 39
                $versionMapping = $this->class->fieldMappings[$this->class->versionField];
236 39
                if ($versionMapping['type'] === 'int') {
237 37
                    $nextVersion = max(1, (int) $this->class->reflFields[$this->class->versionField]->getValue($document));
238 37
                    $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
239 39
                } elseif ($versionMapping['type'] === 'date') {
240 2
                    $nextVersionDateTime = new \DateTime();
241 2
                    $nextVersion = new \MongoDate($nextVersionDateTime->getTimestamp());
242 2
                    $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersionDateTime);
243 2
                }
244 39
                $data[$versionMapping['name']] = $nextVersion;
0 ignored issues
show
Bug introduced by
The variable $nextVersion does not seem to be defined for all execution paths leading up to this point.

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

Let’s take a look at an example:

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

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

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

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

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

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