Completed
Push — master ( fbbe09...5ec3f1 )
by Maciej
14:40 queued 05:02
created

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