Completed
Push — master ( f66da8...61f8e0 )
by Maciej
19:24 queued 07:02
created

DocumentPersister::load()   C

Complexity

Conditions 8
Paths 12

Size

Total Lines 28
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 8.2518
Metric Value
dl 0
loc 28
ccs 16
cts 19
cp 0.8421
rs 5.3846
cc 8
eloc 15
nc 12
nop 5
crap 8.2518
1
<?php
2
/*
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license. For more information, see
17
 * <http://www.doctrine-project.org>.
18
 */
19
20
namespace Doctrine\ODM\MongoDB\Persisters;
21
22
use Doctrine\Common\EventManager;
23
use Doctrine\MongoDB\CursorInterface;
24
use Doctrine\ODM\MongoDB\Cursor;
25
use Doctrine\ODM\MongoDB\DocumentManager;
26
use Doctrine\ODM\MongoDB\Utility\CollectionHelper;
27
use Doctrine\ODM\MongoDB\Hydrator\HydratorFactory;
28
use Doctrine\ODM\MongoDB\LockException;
29
use Doctrine\ODM\MongoDB\LockMode;
30
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
31
use Doctrine\ODM\MongoDB\PersistentCollection;
32
use Doctrine\ODM\MongoDB\Proxy\Proxy;
33
use Doctrine\ODM\MongoDB\Query\CriteriaMerger;
34
use Doctrine\ODM\MongoDB\Query\Query;
35
use Doctrine\ODM\MongoDB\Types\Type;
36
use Doctrine\ODM\MongoDB\UnitOfWork;
37
38
/**
39
 * The DocumentPersister is responsible for persisting documents.
40
 *
41
 * @since       1.0
42
 */
43
class DocumentPersister
44
{
45
    /**
46
     * The PersistenceBuilder instance.
47
     *
48
     * @var PersistenceBuilder
49
     */
50
    private $pb;
51
52
    /**
53
     * The DocumentManager instance.
54
     *
55
     * @var DocumentManager
56
     */
57
    private $dm;
58
59
    /**
60
     * The EventManager instance
61
     *
62
     * @var EventManager
63
     */
64
    private $evm;
65
66
    /**
67
     * The UnitOfWork instance.
68
     *
69
     * @var UnitOfWork
70
     */
71
    private $uow;
72
73
    /**
74
     * The ClassMetadata instance for the document type being persisted.
75
     *
76
     * @var ClassMetadata
77
     */
78
    private $class;
79
80
    /**
81
     * The MongoCollection instance for this document.
82
     *
83
     * @var \MongoCollection
84
     */
85
    private $collection;
86
87
    /**
88
     * Array of queued inserts for the persister to insert.
89
     *
90
     * @var array
91
     */
92
    private $queuedInserts = array();
93
94
    /**
95
     * Array of queued inserts for the persister to insert.
96
     *
97
     * @var array
98
     */
99
    private $queuedUpserts = array();
100
101
    /**
102
     * The CriteriaMerger instance.
103
     *
104
     * @var CriteriaMerger
105
     */
106
    private $cm;
107
108
    /**
109
     * The CollectionPersister instance.
110
     *
111
     * @var CollectionPersister
112
     */
113
    private $cp;
114
115
    /**
116
     * Initializes a new DocumentPersister instance.
117
     *
118
     * @param PersistenceBuilder $pb
119
     * @param DocumentManager $dm
120
     * @param EventManager $evm
121
     * @param UnitOfWork $uow
122
     * @param HydratorFactory $hydratorFactory
123
     * @param ClassMetadata $class
124
     */
125 679
    public function __construct(PersistenceBuilder $pb, DocumentManager $dm, EventManager $evm, UnitOfWork $uow, HydratorFactory $hydratorFactory, ClassMetadata $class, CriteriaMerger $cm = null)
126
    {
127 679
        $this->pb = $pb;
128 679
        $this->dm = $dm;
129 679
        $this->evm = $evm;
130 679
        $this->cm = $cm ?: new CriteriaMerger();
131 679
        $this->uow = $uow;
132 679
        $this->hydratorFactory = $hydratorFactory;
0 ignored issues
show
Bug introduced by
The property hydratorFactory does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
133 679
        $this->class = $class;
134 679
        $this->collection = $dm->getDocumentCollection($class->name);
0 ignored issues
show
Documentation Bug introduced by
It seems like $dm->getDocumentCollection($class->name) of type object<Doctrine\MongoDB\Collection> is incompatible with the declared type object<MongoCollection> of property $collection.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

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