Completed
Push — 1.0.x ( 17be5e...3a014b )
by Maciej
08:48
created

DocumentPersister::refresh()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1
Metric Value
dl 0
loc 7
ccs 6
cts 6
cp 1
rs 9.4286
cc 1
eloc 5
nc 1
nop 2
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\MongoDB\CursorInterface;
24
use Doctrine\ODM\MongoDB\Cursor;
25
use Doctrine\ODM\MongoDB\DocumentManager;
26
use Doctrine\ODM\MongoDB\Utility\CollectionHelper;
27
use Doctrine\ODM\MongoDB\Hydrator\HydratorFactory;
28
use Doctrine\ODM\MongoDB\LockException;
29
use Doctrine\ODM\MongoDB\LockMode;
30
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
31
use Doctrine\ODM\MongoDB\PersistentCollection;
32
use Doctrine\ODM\MongoDB\Proxy\Proxy;
33
use Doctrine\ODM\MongoDB\Query\CriteriaMerger;
34
use Doctrine\ODM\MongoDB\Query\Query;
35
use Doctrine\ODM\MongoDB\Types\Type;
36
use Doctrine\ODM\MongoDB\UnitOfWork;
37
38
/**
39
 * The DocumentPersister is responsible for persisting documents.
40
 *
41
 * @since       1.0
42
 * @author      Jonathan H. Wage <[email protected]>
43
 * @author      Bulat Shakirzyanov <[email protected]>
44
 */
45
class DocumentPersister
46
{
47
    /**
48
     * The PersistenceBuilder instance.
49
     *
50
     * @var PersistenceBuilder
51
     */
52
    private $pb;
53
54
    /**
55
     * The DocumentManager instance.
56
     *
57
     * @var DocumentManager
58
     */
59
    private $dm;
60
61
    /**
62
     * The EventManager instance
63
     *
64
     * @var EventManager
65
     */
66
    private $evm;
67
68
    /**
69
     * The UnitOfWork instance.
70
     *
71
     * @var UnitOfWork
72
     */
73
    private $uow;
74
75
    /**
76
     * The ClassMetadata instance for the document type being persisted.
77
     *
78
     * @var ClassMetadata
79
     */
80
    private $class;
81
82
    /**
83
     * The MongoCollection instance for this document.
84
     *
85
     * @var \MongoCollection
86
     */
87
    private $collection;
88
89
    /**
90
     * Array of queued inserts for the persister to insert.
91
     *
92
     * @var array
93
     */
94
    private $queuedInserts = array();
95
96
    /**
97
     * Array of queued inserts for the persister to insert.
98
     *
99
     * @var array
100
     */
101
    private $queuedUpserts = array();
102
103
    /**
104
     * The CriteriaMerger instance.
105
     *
106
     * @var CriteriaMerger
107
     */
108
    private $cm;
109
110
    /**
111
     * The CollectionPersister instance.
112
     *
113
     * @var CollectionPersister
114
     */
115
    private $cp;
116
117
    /**
118
     * Initializes a new DocumentPersister instance.
119
     *
120
     * @param PersistenceBuilder $pb
121
     * @param DocumentManager $dm
122
     * @param EventManager $evm
123
     * @param UnitOfWork $uow
124
     * @param HydratorFactory $hydratorFactory
125
     * @param ClassMetadata $class
126
     */
127 665
    public function __construct(PersistenceBuilder $pb, DocumentManager $dm, EventManager $evm, UnitOfWork $uow, HydratorFactory $hydratorFactory, ClassMetadata $class, CriteriaMerger $cm = null)
128
    {
129 665
        $this->pb = $pb;
130 665
        $this->dm = $dm;
131 665
        $this->evm = $evm;
132 665
        $this->cm = $cm ?: new CriteriaMerger();
133 665
        $this->uow = $uow;
134 665
        $this->hydratorFactory = $hydratorFactory;
0 ignored issues
show
Bug introduced by
The property hydratorFactory does not exist. Did you maybe forget to declare it?

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

class MyClass { }

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

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

class MyClass {
    public $foo;
}

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

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

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

Loading history...
137 665
        $this->cp = $this->uow->getCollectionPersister();
138 665
    }
139
140
    /**
141
     * @return array
142
     */
143
    public function getInserts()
144
    {
145
        return $this->queuedInserts;
146
    }
147
148
    /**
149
     * @param object $document
150
     * @return bool
151
     */
152
    public function isQueuedForInsert($document)
153
    {
154
        return isset($this->queuedInserts[spl_object_hash($document)]);
155
    }
156
157
    /**
158
     * Adds a document to the queued insertions.
159
     * The document remains queued until {@link executeInserts} is invoked.
160
     *
161
     * @param object $document The document to queue for insertion.
162
     */
163 471
    public function addInsert($document)
164
    {
165 471
        $this->queuedInserts[spl_object_hash($document)] = $document;
166 471
    }
167
168
    /**
169
     * @return array
170
     */
171
    public function getUpserts()
172
    {
173
        return $this->queuedUpserts;
174
    }
175
176
    /**
177
     * @param object $document
178
     * @return boolean
179
     */
180
    public function isQueuedForUpsert($document)
181
    {
182
        return isset($this->queuedUpserts[spl_object_hash($document)]);
183
    }
184
185
    /**
186
     * Adds a document to the queued upserts.
187
     * The document remains queued until {@link executeUpserts} is invoked.
188
     *
189
     * @param object $document The document to queue for insertion.
190
     */
191 76
    public function addUpsert($document)
192
    {
193 76
        $this->queuedUpserts[spl_object_hash($document)] = $document;
194 76
    }
195
196
    /**
197
     * Gets the ClassMetadata instance of the document class this persister is used for.
198
     *
199
     * @return ClassMetadata
200
     */
201
    public function getClassMetadata()
202
    {
203
        return $this->class;
204
    }
205
206
    /**
207
     * Executes all queued document insertions.
208
     *
209
     * Queued documents without an ID will inserted in a batch and queued
210
     * documents with an ID will be upserted individually.
211
     *
212
     * If no inserts are queued, invoking this method is a NOOP.
213
     *
214
     * @param array $options Options for batchInsert() and update() driver methods
215
     */
216 471
    public function executeInserts(array $options = array())
217
    {
218 471
        if ( ! $this->queuedInserts) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->queuedInserts of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
219
            return;
220
        }
221
222 471
        $inserts = array();
223 471
        foreach ($this->queuedInserts as $oid => $document) {
224 471
            $data = $this->pb->prepareInsertData($document);
225
226
            // Set the initial version for each insert
227 470 View Code Duplication
            if ($this->class->isVersioned) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
228 38
                $versionMapping = $this->class->fieldMappings[$this->class->versionField];
229 38
                if ($versionMapping['type'] === 'int') {
230 36
                    $nextVersion = max(1, (int) $this->class->reflFields[$this->class->versionField]->getValue($document));
231 36
                    $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
232 38
                } elseif ($versionMapping['type'] === 'date') {
233 2
                    $nextVersionDateTime = new \DateTime();
234 2
                    $nextVersion = new \MongoDate($nextVersionDateTime->getTimestamp());
235 2
                    $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersionDateTime);
236 2
                }
237 38
                $data[$versionMapping['name']] = $nextVersion;
0 ignored issues
show
Bug introduced by
The variable $nextVersion does not seem to be defined for all execution paths leading up to this point.

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

Let’s take a look at an example:

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

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

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

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

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
238 38
            }
239
240 470
            $inserts[$oid] = $data;
241 470
        }
242
243 470
        if ($inserts) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $inserts of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
244
            try {
245 470
                $this->collection->batchInsert($inserts, $options);
246 470
            } catch (\MongoException $e) {
247 7
                $this->queuedInserts = array();
248 7
                throw $e;
249
            }
250 470
        }
251
252
        /* All collections except for ones using addToSet have already been
253
         * saved. We have left these to be handled separately to avoid checking
254
         * collection for uniqueness on PHP side.
255
         */
256 470
        foreach ($this->queuedInserts as $document) {
257 470
            $this->handleCollections($document, $options);
258 470
        }
259
260 470
        $this->queuedInserts = array();
261 470
    }
262
263
    /**
264
     * Executes all queued document upserts.
265
     *
266
     * Queued documents with an ID are upserted individually.
267
     *
268
     * If no upserts are queued, invoking this method is a NOOP.
269
     *
270
     * @param array $options Options for batchInsert() and update() driver methods
271
     */
272 76
    public function executeUpserts(array $options = array())
273
    {
274 76
        if ( ! $this->queuedUpserts) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->queuedUpserts of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
275
            return;
276
        }
277
278 76
        foreach ($this->queuedUpserts as $oid => $document) {
279 76
            $data = $this->pb->prepareUpsertData($document);
280
281
            // Set the initial version for each upsert
282 76 View Code Duplication
            if ($this->class->isVersioned) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
283 3
                $versionMapping = $this->class->fieldMappings[$this->class->versionField];
284 3
                if ($versionMapping['type'] === 'int') {
285 2
                    $nextVersion = max(1, (int) $this->class->reflFields[$this->class->versionField]->getValue($document));
286 2
                    $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
287 3
                } elseif ($versionMapping['type'] === 'date') {
288 1
                    $nextVersionDateTime = new \DateTime();
289 1
                    $nextVersion = new \MongoDate($nextVersionDateTime->getTimestamp());
290 1
                    $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersionDateTime);
291 1
                }
292 3
                $data['$set'][$versionMapping['name']] = $nextVersion;
0 ignored issues
show
Bug introduced by
The variable $nextVersion does not seem to be defined for all execution paths leading up to this point.

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

Let’s take a look at an example:

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

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

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

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

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
293 3
            }
294
            
295
            try {
296 76
                $this->executeUpsert($data, $options);
297 76
                $this->handleCollections($document, $options);
298 76
                unset($this->queuedUpserts[$oid]);
299 76
            } catch (\MongoException $e) {
300
                unset($this->queuedUpserts[$oid]);
301
                throw $e;
302
            }
303 76
        }
304 76
    }
305
306
    /**
307
     * Executes a single upsert in {@link executeInserts}
308
     *
309
     * @param array $data
310
     * @param array $options
311
     */
312 76
    private function executeUpsert(array $data, array $options)
313
    {
314 76
        $options['upsert'] = true;
315 76
        $criteria = array('_id' => $data['$set']['_id']);
316 76
        unset($data['$set']['_id']);
317
318
        // Do not send an empty $set modifier
319 76
        if (empty($data['$set'])) {
320 13
            unset($data['$set']);
321 13
        }
322
323
        /* If there are no modifiers remaining, we're upserting a document with 
324
         * an identifier as its only field. Since a document with the identifier
325
         * may already exist, the desired behavior is "insert if not exists" and
326
         * NOOP otherwise. MongoDB 2.6+ does not allow empty modifiers, so $set
327
         * the identifier to the same value in our criteria.
328
         *
329
         * This will fail for versions before MongoDB 2.6, which require an
330
         * empty $set modifier. The best we can do (without attempting to check
331
         * server versions in advance) is attempt the 2.6+ behavior and retry
332
         * after the relevant exception.
333
         *
334
         * See: https://jira.mongodb.org/browse/SERVER-12266
335
         */
336 76
        if (empty($data)) {
337 13
            $retry = true;
338 14
            $data = array('$set' => array('_id' => $criteria['_id']));
339 13
        }
340
341
        try {
342 76
            $this->collection->update($criteria, $data, $options);
343 64
            return;
344 13
        } catch (\MongoCursorException $e) {
345 13
            if (empty($retry) || strpos($e->getMessage(), 'Mod on _id not allowed') === false) {
346
                throw $e;
347
            }
348
        }
349
350 13
        $this->collection->update($criteria, array('$set' => new \stdClass), $options);
351 13
    }
352
353
    /**
354
     * Updates the already persisted document if it has any new changesets.
355
     *
356
     * @param object $document
357
     * @param array $options Array of options to be used with update()
358
     * @throws \Doctrine\ODM\MongoDB\LockException
359
     */
360 205
    public function update($document, array $options = array())
361
    {
362 205
        $id = $this->uow->getDocumentIdentifier($document);
363 205
        $update = $this->pb->prepareUpdateData($document);
364
365 205
        $id = $this->class->getDatabaseIdentifierValue($id);
366 205
        $query = array('_id' => $id);
367
368
        // Include versioning logic to set the new version value in the database
369
        // and to ensure the version has not changed since this document object instance
370
        // was fetched from the database
371 205
        if ($this->class->isVersioned) {
372 30
            $versionMapping = $this->class->fieldMappings[$this->class->versionField];
373 30
            $currentVersion = $this->class->reflFields[$this->class->versionField]->getValue($document);
374 30
            if ($versionMapping['type'] === 'int') {
375 27
                $nextVersion = $currentVersion + 1;
376 27
                $update['$inc'][$versionMapping['name']] = 1;
377 27
                $query[$versionMapping['name']] = $currentVersion;
378 27
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
379 30
            } elseif ($versionMapping['type'] === 'date') {
380 3
                $nextVersion = new \DateTime();
381 3
                $update['$set'][$versionMapping['name']] = new \MongoDate($nextVersion->getTimestamp());
382 3
                $query[$versionMapping['name']] = new \MongoDate($currentVersion->getTimestamp());
383 3
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
384 3
            }
385 30
        }
386
387 205
        if ( ! empty($update)) {
388
            // Include locking logic so that if the document object in memory is currently
389
            // locked then it will remove it, otherwise it ensures the document is not locked.
390 145
            if ($this->class->isLockable) {
391 11
                $isLocked = $this->class->reflFields[$this->class->lockField]->getValue($document);
392 11
                $lockMapping = $this->class->fieldMappings[$this->class->lockField];
393 11
                if ($isLocked) {
394 2
                    $update['$unset'] = array($lockMapping['name'] => true);
395 2
                } else {
396 9
                    $query[$lockMapping['name']] = array('$exists' => false);
397
                }
398 11
            }
399
400 145
            $result = $this->collection->update($query, $update, $options);
401
402 145 View Code Duplication
            if (($this->class->isVersioned || $this->class->isLockable) && ! $result['n']) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
403 5
                throw LockException::lockFailed($document);
404
            }
405 141
        }
406
407 201
        $this->handleCollections($document, $options);
408 201
    }
409
410
    /**
411
     * Removes document from mongo
412
     *
413
     * @param mixed $document
414
     * @param array $options Array of options to be used with remove()
415
     * @throws \Doctrine\ODM\MongoDB\LockException
416
     */
417 28
    public function delete($document, array $options = array())
418
    {
419 28
        $id = $this->uow->getDocumentIdentifier($document);
420 28
        $query = array('_id' => $this->class->getDatabaseIdentifierValue($id));
421
422 28
        if ($this->class->isLockable) {
423 2
            $query[$this->class->lockField] = array('$exists' => false);
424 2
        }
425
426 28
        $result = $this->collection->remove($query, $options);
427
428 28 View Code Duplication
        if (($this->class->isVersioned || $this->class->isLockable) && ! $result['n']) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
429 2
            throw LockException::lockFailed($document);
430
        }
431 26
    }
432
433
    /**
434
     * Refreshes a managed document.
435
     *
436
     * @param array $id The identifier of the document.
437
     * @param object $document The document to refresh.
438
     */
439 19
    public function refresh($id, $document)
440
    {
441 19
        $class = $this->dm->getClassMetadata(get_class($document));
0 ignored issues
show
Unused Code introduced by
$class is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
442 19
        $data = $this->collection->findOne(array('_id' => $id));
443 19
        $data = $this->hydratorFactory->hydrate($document, $data);
444 19
        $this->uow->setOriginalDocumentData($document, $data);
445 19
    }
446
447
    /**
448
     * Finds a document by a set of criteria.
449
     *
450
     * If a scalar or MongoId is provided for $criteria, it will be used to
451
     * match an _id value.
452
     *
453
     * @param mixed   $criteria Query criteria
454
     * @param object  $document Document to load the data into. If not specified, a new document is created.
455
     * @param array   $hints    Hints for document creation
456
     * @param integer $lockMode
457
     * @param array   $sort     Sort array for Cursor::sort()
458
     * @throws \Doctrine\ODM\MongoDB\LockException
459
     * @return object|null The loaded and managed document instance or null if no document was found
460
     * @todo Check identity map? loadById method? Try to guess whether $criteria is the id?
461
     */
462 346
    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...
463
    {
464
        // TODO: remove this
465 346
        if ($criteria === null || is_scalar($criteria) || $criteria instanceof \MongoId) {
466
            $criteria = array('_id' => $criteria);
467
        }
468
469 346
        $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...
470 346
        $criteria = $this->addDiscriminatorToPreparedQuery($criteria);
471 346
        $criteria = $this->addFilterToPreparedQuery($criteria);
472
473 346
        $cursor = $this->collection->find($criteria);
474
475 346
        if (null !== $sort) {
476 100
            $cursor->sort($this->prepareSortOrProjection($sort));
477 100
        }
478
479 346
        $result = $cursor->getSingleResult();
480
481 346
        if ($this->class->isLockable) {
482 1
            $lockMapping = $this->class->fieldMappings[$this->class->lockField];
483 1
            if (isset($result[$lockMapping['name']]) && $result[$lockMapping['name']] === LockMode::PESSIMISTIC_WRITE) {
484 1
                throw LockException::lockFailed($result);
485
            }
486
        }
487
488 345
        return $this->createDocument($result, $document, $hints);
489
    }
490
491
    /**
492
     * Finds documents by a set of criteria.
493
     *
494
     * @param array        $criteria Query criteria
495
     * @param array        $sort     Sort array for Cursor::sort()
496
     * @param integer|null $limit    Limit for Cursor::limit()
497
     * @param integer|null $skip     Skip for Cursor::skip()
498
     * @return Cursor
499
     */
500 20
    public function loadAll(array $criteria = array(), array $sort = null, $limit = null, $skip = null)
501
    {
502 20
        $criteria = $this->prepareQueryOrNewObj($criteria);
503 20
        $criteria = $this->addDiscriminatorToPreparedQuery($criteria);
504 20
        $criteria = $this->addFilterToPreparedQuery($criteria);
505
506 20
        $baseCursor = $this->collection->find($criteria);
507 20
        $cursor = $this->wrapCursor($baseCursor);
0 ignored issues
show
Documentation introduced by
$baseCursor is of type object<MongoCursor>, but the function expects a object<Doctrine\MongoDB\CursorInterface>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
508
509 20
        if (null !== $sort) {
510 3
            $cursor->sort($sort);
511 3
        }
512
513 20
        if (null !== $limit) {
514 2
            $cursor->limit($limit);
515 2
        }
516
517 20
        if (null !== $skip) {
518 2
            $cursor->skip($skip);
519 2
        }
520
521 20
        return $cursor;
522
    }
523
524
    /**
525
     * Wraps the supplied base cursor in the corresponding ODM class.
526
     *
527
     * @param CursorInterface $baseCursor
528
     * @return Cursor
529
     */
530 20
    private function wrapCursor(CursorInterface $baseCursor)
531
    {
532 20
        return new Cursor($baseCursor, $this->dm->getUnitOfWork(), $this->class);
533
    }
534
535
    /**
536
     * Checks whether the given managed document exists in the database.
537
     *
538
     * @param object $document
539
     * @return boolean TRUE if the document exists in the database, FALSE otherwise.
540
     */
541 3
    public function exists($document)
542
    {
543 3
        $id = $this->class->getIdentifierObject($document);
544 3
        return (boolean) $this->collection->findOne(array('_id' => $id), array('_id'));
545
    }
546
547
    /**
548
     * Locks document by storing the lock mode on the mapped lock field.
549
     *
550
     * @param object $document
551
     * @param int $lockMode
552
     */
553 5
    public function lock($document, $lockMode)
554
    {
555 5
        $id = $this->uow->getDocumentIdentifier($document);
556 5
        $criteria = array('_id' => $this->class->getDatabaseIdentifierValue($id));
557 5
        $lockMapping = $this->class->fieldMappings[$this->class->lockField];
558 5
        $this->collection->update($criteria, array('$set' => array($lockMapping['name'] => $lockMode)));
559 5
        $this->class->reflFields[$this->class->lockField]->setValue($document, $lockMode);
560 5
    }
561
562
    /**
563
     * Releases any lock that exists on this document.
564
     *
565
     * @param object $document
566
     */
567 1
    public function unlock($document)
568
    {
569 1
        $id = $this->uow->getDocumentIdentifier($document);
570 1
        $criteria = array('_id' => $this->class->getDatabaseIdentifierValue($id));
571 1
        $lockMapping = $this->class->fieldMappings[$this->class->lockField];
572 1
        $this->collection->update($criteria, array('$unset' => array($lockMapping['name'] => true)));
573 1
        $this->class->reflFields[$this->class->lockField]->setValue($document, null);
574 1
    }
575
576
    /**
577
     * Creates or fills a single document object from an query result.
578
     *
579
     * @param object $result The query result.
580
     * @param object $document The document object to fill, if any.
581
     * @param array $hints Hints for document creation.
582
     * @return object The filled and managed document object or NULL, if the query result is empty.
583
     */
584 345
    private function createDocument($result, $document = null, array $hints = array())
585
    {
586 345
        if ($result === null) {
587 114
            return null;
588
        }
589
590 293
        if ($document !== null) {
591 36
            $hints[Query::HINT_REFRESH] = true;
592 36
            $id = $this->class->getPHPIdentifierValue($result['_id']);
593 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...
594 36
        }
595
596 293
        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...
597
    }
598
599
    /**
600
     * Loads a PersistentCollection data. Used in the initialize() method.
601
     *
602
     * @param PersistentCollection $collection
603
     */
604 157
    public function loadCollection(PersistentCollection $collection)
605
    {
606 157
        $mapping = $collection->getMapping();
607 157
        switch ($mapping['association']) {
608 157
            case ClassMetadata::EMBED_MANY:
609 111
                $this->loadEmbedManyCollection($collection);
610 111
                break;
611
612 62
            case ClassMetadata::REFERENCE_MANY:
613 62
                if (isset($mapping['repositoryMethod']) && $mapping['repositoryMethod']) {
614 3
                    $this->loadReferenceManyWithRepositoryMethod($collection);
615 3
                } else {
616 59
                    if ($mapping['isOwningSide']) {
617 49
                        $this->loadReferenceManyCollectionOwningSide($collection);
618 49
                    } else {
619 14
                        $this->loadReferenceManyCollectionInverseSide($collection);
620
                    }
621
                }
622 62
                break;
623 157
        }
624 157
    }
625
626 111
    private function loadEmbedManyCollection(PersistentCollection $collection)
627
    {
628 111
        $embeddedDocuments = $collection->getMongoData();
629 111
        $mapping = $collection->getMapping();
630 111
        $owner = $collection->getOwner();
631 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...
632 82
            foreach ($embeddedDocuments as $key => $embeddedDocument) {
633 82
                $className = $this->uow->getClassNameForAssociation($mapping, $embeddedDocument);
634 82
                $embeddedMetadata = $this->dm->getClassMetadata($className);
635 82
                $embeddedDocumentObject = $embeddedMetadata->newInstance();
636
637 82
                $this->uow->setParentAssociation($embeddedDocumentObject, $mapping, $owner, $mapping['name'] . '.' . $key);
638
639 82
                $data = $this->hydratorFactory->hydrate($embeddedDocumentObject, $embeddedDocument);
640 82
                $id = $embeddedMetadata->identifier && isset($data[$embeddedMetadata->identifier])
641 82
                    ? $data[$embeddedMetadata->identifier]
642 82
                    : null;
643
644 82
                $this->uow->registerManaged($embeddedDocumentObject, $id, $data);
645 82
                if (CollectionHelper::isHash($mapping['strategy'])) {
646 25
                    $collection->set($key, $embeddedDocumentObject);
647 25
                } else {
648 64
                    $collection->add($embeddedDocumentObject);
649
                }
650 82
            }
651 82
        }
652 111
    }
653
654 49
    private function loadReferenceManyCollectionOwningSide(PersistentCollection $collection)
655
    {
656 49
        $hints = $collection->getHints();
657 49
        $mapping = $collection->getMapping();
658 49
        $groupedIds = array();
659
660 49
        $sorted = isset($mapping['sort']) && $mapping['sort'];
661
662 49
        foreach ($collection->getMongoData() as $key => $reference) {
663 44
            if (isset($mapping['simple']) && $mapping['simple']) {
664 4
                $className = $mapping['targetDocument'];
665 4
                $mongoId = $reference;
666 4
            } else {
667 40
                $className = $this->uow->getClassNameForAssociation($mapping, $reference);
668 40
                $mongoId = $reference['$id'];
669
            }
670 44
            $id = $this->dm->getClassMetadata($className)->getPHPIdentifierValue($mongoId);
671
672
            // create a reference to the class and id
673 44
            $reference = $this->dm->getReference($className, $id);
674
675
            // no custom sort so add the references right now in the order they are embedded
676 44
            if ( ! $sorted) {
677 43
                if (CollectionHelper::isHash($mapping['strategy'])) {
678 2
                    $collection->set($key, $reference);
679 2
                } else {
680 41
                    $collection->add($reference);
681
                }
682 43
            }
683
684
            // only query for the referenced object if it is not already initialized or the collection is sorted
685 44
            if (($reference instanceof Proxy && ! $reference->__isInitialized__) || $sorted) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
686 32
                $groupedIds[$className][] = $mongoId;
687 32
            }
688 49
        }
689 49
        foreach ($groupedIds as $className => $ids) {
690 32
            $class = $this->dm->getClassMetadata($className);
691 32
            $mongoCollection = $this->dm->getDocumentCollection($className);
692 32
            $criteria = $this->cm->merge(
693 32
                array('_id' => array('$in' => array_values($ids))),
694 32
                $this->dm->getFilterCollection()->getFilterCriteria($class),
695 32
                isset($mapping['criteria']) ? $mapping['criteria'] : array()
696 32
            );
697 32
            $criteria = $this->uow->getDocumentPersister($className)->prepareQueryOrNewObj($criteria);
698 32
            $cursor = $mongoCollection->find($criteria);
699 32
            if (isset($mapping['sort'])) {
700 32
                $cursor->sort($mapping['sort']);
701 32
            }
702 32
            if (isset($mapping['limit'])) {
703
                $cursor->limit($mapping['limit']);
704
            }
705 32
            if (isset($mapping['skip'])) {
706
                $cursor->skip($mapping['skip']);
707
            }
708 32
            if ( ! empty($hints[Query::HINT_SLAVE_OKAY])) {
709
                $cursor->slaveOkay(true);
710
            }
711 32 View Code Duplication
            if ( ! empty($hints[Query::HINT_READ_PREFERENCE])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
712
                $cursor->setReadPreference($hints[Query::HINT_READ_PREFERENCE], $hints[Query::HINT_READ_PREFERENCE_TAGS]);
713
            }
714 32
            $documents = $cursor->toArray(false);
715 32
            foreach ($documents as $documentData) {
716 31
                $document = $this->uow->getById($documentData['_id'], $class);
717 31
                $data = $this->hydratorFactory->hydrate($document, $documentData);
718 31
                $this->uow->setOriginalDocumentData($document, $data);
719 31
                $document->__isInitialized__ = true;
720 31
                if ($sorted) {
721 1
                    $collection->add($document);
722 1
                }
723 32
            }
724 49
        }
725 49
    }
726
727 14
    private function loadReferenceManyCollectionInverseSide(PersistentCollection $collection)
728
    {
729 14
        $query = $this->createReferenceManyInverseSideQuery($collection);
730 14
        $documents = $query->execute()->toArray(false);
731 14
        foreach ($documents as $key => $document) {
732 13
            $collection->add($document);
733 14
        }
734 14
    }
735
736
    /**
737
     * @param PersistentCollection $collection
738
     *
739
     * @return Query
740
     */
741 16
    public function createReferenceManyInverseSideQuery(PersistentCollection $collection)
742
    {
743 16
        $hints = $collection->getHints();
744 16
        $mapping = $collection->getMapping();
745 16
        $owner = $collection->getOwner();
746 16
        $ownerClass = $this->dm->getClassMetadata(get_class($owner));
747 16
        $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
748 16
        $mappedByMapping = isset($targetClass->fieldMappings[$mapping['mappedBy']]) ? $targetClass->fieldMappings[$mapping['mappedBy']] : array();
749 16
        $mappedByFieldName = isset($mappedByMapping['simple']) && $mappedByMapping['simple'] ? $mapping['mappedBy'] : $mapping['mappedBy'] . '.$id';
750 16
        $criteria = $this->cm->merge(
751 16
            array($mappedByFieldName => $ownerClass->getIdentifierObject($owner)),
752 16
            $this->dm->getFilterCollection()->getFilterCriteria($targetClass),
753 16
            isset($mapping['criteria']) ? $mapping['criteria'] : array()
754 16
        );
755 16
        $criteria = $this->uow->getDocumentPersister($mapping['targetDocument'])->prepareQueryOrNewObj($criteria);
756 16
        $qb = $this->dm->createQueryBuilder($mapping['targetDocument'])
757 16
            ->setQueryArray($criteria);
758
759 16
        if (isset($mapping['sort'])) {
760 16
            $qb->sort($mapping['sort']);
761 16
        }
762 16
        if (isset($mapping['limit'])) {
763 1
            $qb->limit($mapping['limit']);
764 1
        }
765 16
        if (isset($mapping['skip'])) {
766
            $qb->skip($mapping['skip']);
767
        }
768 16
        if ( ! empty($hints[Query::HINT_SLAVE_OKAY])) {
769
            $qb->slaveOkay(true);
770
        }
771 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...
772
            $qb->setReadPreference($hints[Query::HINT_READ_PREFERENCE], $hints[Query::HINT_READ_PREFERENCE_TAGS]);
773
        }
774
775 16
        return $qb->getQuery();
776
    }
777
778 3
    private function loadReferenceManyWithRepositoryMethod(PersistentCollection $collection)
779
    {
780 3
        $cursor = $this->createReferenceManyWithRepositoryMethodCursor($collection);
781 3
        $mapping = $collection->getMapping();        
782 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...
783 3
        foreach ($documents as $key => $obj) {
784 3
            if (CollectionHelper::isHash($mapping['strategy'])) {
785 1
                $collection->set($key, $obj);
786 1
            } else {
787 2
                $collection->add($obj);
788
            }
789 3
        }
790 3
    }
791
792
    /**
793
     * @param PersistentCollection $collection
794
     *
795
     * @return CursorInterface
796
     */
797 3
    public function createReferenceManyWithRepositoryMethodCursor(PersistentCollection $collection)
798
    {
799 3
        $hints = $collection->getHints();
800 3
        $mapping = $collection->getMapping();
801 3
        $cursor = $this->dm->getRepository($mapping['targetDocument'])
802 3
            ->$mapping['repositoryMethod']($collection->getOwner());
803
804 3
        if ( ! $cursor instanceof CursorInterface) {
805
            throw new \BadMethodCallException("Expected repository method {$mapping['repositoryMethod']} to return a CursorInterface");
806
        }
807
808 3
        if (isset($mapping['sort'])) {
809 3
            $cursor->sort($mapping['sort']);
810 3
        }
811 3
        if (isset($mapping['limit'])) {
812
            $cursor->limit($mapping['limit']);
813
        }
814 3
        if (isset($mapping['skip'])) {
815
            $cursor->skip($mapping['skip']);
816
        }
817 3
        if ( ! empty($hints[Query::HINT_SLAVE_OKAY])) {
818
            $cursor->slaveOkay(true);
819
        }
820 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...
821
            $cursor->setReadPreference($hints[Query::HINT_READ_PREFERENCE], $hints[Query::HINT_READ_PREFERENCE_TAGS]);
822
        }
823
824 3
        return $cursor;
825
    }
826
827
    /**
828
     * Prepare a sort or projection array by converting keys, which are PHP
829
     * property names, to MongoDB field names.
830
     *
831
     * @param array $fields
832
     * @return array
833
     */
834 136
    public function prepareSortOrProjection(array $fields)
835
    {
836 136
        $preparedFields = array();
837
838 136
        foreach ($fields as $key => $value) {
839 32
            $preparedFields[$this->prepareFieldName($key)] = $value;
840 136
        }
841
842 136
        return $preparedFields;
843
    }
844
845
    /**
846
     * Prepare a mongodb field name and convert the PHP property names to MongoDB field names.
847
     *
848
     * @param string $fieldName
849
     * @return string
850
     */
851 84
    public function prepareFieldName($fieldName)
852
    {
853 84
        list($fieldName) = $this->prepareQueryElement($fieldName, null, null, false);
854
855 84
        return $fieldName;
856
    }
857
858
    /**
859
     * Adds discriminator criteria to an already-prepared query.
860
     *
861
     * This method should be used once for query criteria and not be used for
862
     * nested expressions. It should be called before
863
     * {@link DocumentPerister::addFilterToPreparedQuery()}.
864
     *
865
     * @param array $preparedQuery
866
     * @return array
867
     */
868 465
    public function addDiscriminatorToPreparedQuery(array $preparedQuery)
869
    {
870
        /* If the class has a discriminator field, which is not already in the
871
         * criteria, inject it now. The field/values need no preparation.
872
         */
873 465
        if ($this->class->hasDiscriminator() && ! isset($preparedQuery[$this->class->discriminatorField])) {
874 18
            $discriminatorValues = $this->getClassDiscriminatorValues($this->class);
875 18
            $preparedQuery[$this->class->discriminatorField] = array('$in' => $discriminatorValues);
876 18
        }
877
878 465
        return $preparedQuery;
879
    }
880
881
    /**
882
     * Adds filter criteria to an already-prepared query.
883
     *
884
     * This method should be used once for query criteria and not be used for
885
     * nested expressions. It should be called after
886
     * {@link DocumentPerister::addDiscriminatorToPreparedQuery()}.
887
     *
888
     * @param array $preparedQuery
889
     * @return array
890
     */
891 466
    public function addFilterToPreparedQuery(array $preparedQuery)
892
    {
893
        /* If filter criteria exists for this class, prepare it and merge
894
         * over the existing query.
895
         *
896
         * @todo Consider recursive merging in case the filter criteria and
897
         * prepared query both contain top-level $and/$or operators.
898
         */
899 466
        if ($filterCriteria = $this->dm->getFilterCollection()->getFilterCriteria($this->class)) {
900 16
            $preparedQuery = $this->cm->merge($preparedQuery, $this->prepareQueryOrNewObj($filterCriteria));
901 16
        }
902
903 466
        return $preparedQuery;
904
    }
905
906
    /**
907
     * Prepares the query criteria or new document object.
908
     *
909
     * PHP field names and types will be converted to those used by MongoDB.
910
     *
911
     * @param array $query
912
     * @return array
913
     */
914 498
    public function prepareQueryOrNewObj(array $query)
915
    {
916 498
        $preparedQuery = array();
917
918 498
        foreach ($query as $key => $value) {
919
            // Recursively prepare logical query clauses
920 460
            if (in_array($key, array('$and', '$or', '$nor')) && is_array($value)) {
921 20
                foreach ($value as $k2 => $v2) {
922 20
                    $preparedQuery[$key][$k2] = $this->prepareQueryOrNewObj($v2);
923 20
                }
924 20
                continue;
925
            }
926
927 460
            if (isset($key[0]) && $key[0] === '$' && is_array($value)) {
928 17
                $preparedQuery[$key] = $this->prepareQueryOrNewObj($value);
929 17
                continue;
930
            }
931
932 460
            list($key, $value) = $this->prepareQueryElement($key, $value, null, true);
933
934 460
            $preparedQuery[$key] = is_array($value)
935 460
                ? array_map('Doctrine\ODM\MongoDB\Types\Type::convertPHPToDatabaseValue', $value)
936 460
                : Type::convertPHPToDatabaseValue($value);
937 498
        }
938
939 498
        return $preparedQuery;
940
    }
941
942
    /**
943
     * Prepares a query value and converts the PHP value to the database value
944
     * if it is an identifier.
945
     *
946
     * It also handles converting $fieldName to the database name if they are different.
947
     *
948
     * @param string $fieldName
949
     * @param mixed $value
950
     * @param ClassMetadata $class        Defaults to $this->class
951
     * @param boolean $prepareValue Whether or not to prepare the value
952
     * @return array        Prepared field name and value
953
     */
954 491
    private function prepareQueryElement($fieldName, $value = null, $class = null, $prepareValue = true)
955
    {
956 491
        $class = isset($class) ? $class : $this->class;
957
958
        // @todo Consider inlining calls to ClassMetadataInfo methods
959
960
        // Process all non-identifier fields by translating field names
961 491
        if ($class->hasField($fieldName) && ! $class->isIdentifier($fieldName)) {
962 223
            $mapping = $class->fieldMappings[$fieldName];
963 223
            $fieldName = $mapping['name'];
964
965 223
            if ( ! $prepareValue) {
966 62
                return array($fieldName, $value);
967
            }
968
969
            // Prepare mapped, embedded objects
970 181
            if ( ! empty($mapping['embedded']) && is_object($value) &&
971 181
                ! $this->dm->getMetadataFactory()->isTransient(get_class($value))) {
972 1
                return array($fieldName, $this->pb->prepareEmbeddedDocumentValue($mapping, $value));
973
            }
974
975
            // No further preparation unless we're dealing with a simple reference
976 181
            if (empty($mapping['reference']) || empty($mapping['simple'])) {
977 109
                return array($fieldName, $value);
978
            }
979
980
            // Additional preparation for one or more simple reference values
981 99
            $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
982
983 99
            if ( ! is_array($value)) {
984 95
                return array($fieldName, $targetClass->getDatabaseIdentifierValue($value));
985
            }
986
987
            // Objects without operators or with DBRef fields can be converted immediately
988 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...
989 3
                return array($fieldName, $targetClass->getDatabaseIdentifierValue($value));
990
            }
991
992 7
            return array($fieldName, $this->prepareQueryExpression($value, $targetClass));
993
        }
994
995
        // Process identifier fields
996 376
        if (($class->hasField($fieldName) && $class->isIdentifier($fieldName)) || $fieldName === '_id') {
997 312
            $fieldName = '_id';
998
999 312
            if ( ! $prepareValue) {
1000 15
                return array($fieldName, $value);
1001
            }
1002
1003 299
            if ( ! is_array($value)) {
1004 278
                return array($fieldName, $class->getDatabaseIdentifierValue($value));
1005
            }
1006
1007
            // Objects without operators or with DBRef fields can be converted immediately
1008 51 View Code Duplication
            if ( ! $this->hasQueryOperators($value) || $this->hasDBRefFields($value)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1009 6
                return array($fieldName, $class->getDatabaseIdentifierValue($value));
1010
            }
1011
1012 46
            return array($fieldName, $this->prepareQueryExpression($value, $class));
1013
        }
1014
1015
        // No processing for unmapped, non-identifier, non-dotted field names
1016 97
        if (strpos($fieldName, '.') === false) {
1017 41
            return array($fieldName, $value);
1018
        }
1019
1020
        /* Process "fieldName.objectProperty" queries (on arrays or objects).
1021
         *
1022
         * We can limit parsing here, since at most three segments are
1023
         * significant: "fieldName.objectProperty" with an optional index or key
1024
         * for collections stored as either BSON arrays or objects.
1025
         */
1026 61
        $e = explode('.', $fieldName, 4);
1027
1028
        // No further processing for unmapped fields
1029 61
        if ( ! isset($class->fieldMappings[$e[0]])) {
1030 3
            return array($fieldName, $value);
1031
        }
1032
1033 59
        $mapping = $class->fieldMappings[$e[0]];
1034 59
        $e[0] = $mapping['name'];
1035
1036
        // Hash and raw fields will not be prepared beyond the field name
1037 59
        if ($mapping['type'] === Type::HASH || $mapping['type'] === Type::RAW) {
1038 1
            $fieldName = implode('.', $e);
1039
1040 1
            return array($fieldName, $value);
1041
        }
1042
1043 58
        if (isset($mapping['strategy']) && CollectionHelper::isHash($mapping['strategy'])
1044 58
                && isset($e[2])) {
1045 1
            $objectProperty = $e[2];
1046 1
            $objectPropertyPrefix = $e[1] . '.';
1047 1
            $nextObjectProperty = implode('.', array_slice($e, 3));
1048 58
        } elseif ($e[1] != '$') {
1049 56
            $fieldName = $e[0] . '.' . $e[1];
1050 56
            $objectProperty = $e[1];
1051 56
            $objectPropertyPrefix = '';
1052 56
            $nextObjectProperty = implode('.', array_slice($e, 2));
1053 57
        } elseif (isset($e[2])) {
1054 1
            $fieldName = $e[0] . '.' . $e[1] . '.' . $e[2];
1055 1
            $objectProperty = $e[2];
1056 1
            $objectPropertyPrefix = $e[1] . '.';
1057 1
            $nextObjectProperty = implode('.', array_slice($e, 3));
1058 1
        } else {
1059 1
            $fieldName = $e[0] . '.' . $e[1];
1060
1061 1
            return array($fieldName, $value);
1062
        }
1063
1064
        // No further processing for fields without a targetDocument mapping
1065 58
        if ( ! isset($mapping['targetDocument'])) {
1066 2
            if ($nextObjectProperty) {
1067
                $fieldName .= '.'.$nextObjectProperty;
1068
            }
1069
1070 2
            return array($fieldName, $value);
1071
        }
1072
1073 56
        $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
1074
1075
        // No further processing for unmapped targetDocument fields
1076 56
        if ( ! $targetClass->hasField($objectProperty)) {
1077 24
            if ($nextObjectProperty) {
1078
                $fieldName .= '.'.$nextObjectProperty;
1079
            }
1080
1081 24
            return array($fieldName, $value);
1082
        }
1083
1084 35
        $targetMapping = $targetClass->getFieldMapping($objectProperty);
1085 35
        $objectPropertyIsId = $targetClass->isIdentifier($objectProperty);
1086
1087
        // Prepare DBRef identifiers or the mapped field's property path
1088 35
        $fieldName = ($objectPropertyIsId && ! empty($mapping['reference']) && empty($mapping['simple']))
1089 35
            ? $e[0] . '.$id'
1090 35
            : $e[0] . '.' . $objectPropertyPrefix . $targetMapping['name'];
1091
1092
        // Process targetDocument identifier fields
1093 35
        if ($objectPropertyIsId) {
1094 14
            if ( ! $prepareValue) {
1095 1
                return array($fieldName, $value);
1096
            }
1097
1098 13
            if ( ! is_array($value)) {
1099 2
                return array($fieldName, $targetClass->getDatabaseIdentifierValue($value));
1100
            }
1101
1102
            // Objects without operators or with DBRef fields can be converted immediately
1103 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...
1104 3
                return array($fieldName, $targetClass->getDatabaseIdentifierValue($value));
1105
            }
1106
1107 12
            return array($fieldName, $this->prepareQueryExpression($value, $targetClass));
1108
        }
1109
1110
        /* The property path may include a third field segment, excluding the
1111
         * collection item pointer. If present, this next object property must
1112
         * be processed recursively.
1113
         */
1114 21
        if ($nextObjectProperty) {
1115
            // Respect the targetDocument's class metadata when recursing
1116 14
            $nextTargetClass = isset($targetMapping['targetDocument'])
1117 14
                ? $this->dm->getClassMetadata($targetMapping['targetDocument'])
1118 14
                : null;
1119
1120 14
            list($key, $value) = $this->prepareQueryElement($nextObjectProperty, $value, $nextTargetClass, $prepareValue);
1121
1122 14
            $fieldName .= '.' . $key;
1123 14
        }
1124
1125 21
        return array($fieldName, $value);
1126
    }
1127
1128
    /**
1129
     * Prepares a query expression.
1130
     *
1131
     * @param array|object  $expression
1132
     * @param ClassMetadata $class
1133
     * @return array
1134
     */
1135 65
    private function prepareQueryExpression($expression, $class)
1136
    {
1137 65
        foreach ($expression as $k => $v) {
1138
            // Ignore query operators whose arguments need no type conversion
1139 65
            if (in_array($k, array('$exists', '$type', '$mod', '$size'))) {
1140 12
                continue;
1141
            }
1142
1143
            // Process query operators whose argument arrays need type conversion
1144 65
            if (in_array($k, array('$in', '$nin', '$all')) && is_array($v)) {
1145 62
                foreach ($v as $k2 => $v2) {
1146 62
                    $expression[$k][$k2] = $class->getDatabaseIdentifierValue($v2);
1147 62
                }
1148 62
                continue;
1149
            }
1150
1151
            // Recursively process expressions within a $not operator
1152 15
            if ($k === '$not' && is_array($v)) {
1153 11
                $expression[$k] = $this->prepareQueryExpression($v, $class);
1154 11
                continue;
1155
            }
1156
1157 15
            $expression[$k] = $class->getDatabaseIdentifierValue($v);
1158 65
        }
1159
1160 65
        return $expression;
1161
    }
1162
1163
    /**
1164
     * Checks whether the value has DBRef fields.
1165
     *
1166
     * This method doesn't check if the the value is a complete DBRef object,
1167
     * although it should return true for a DBRef. Rather, we're checking that
1168
     * the value has one or more fields for a DBref. In practice, this could be
1169
     * $elemMatch criteria for matching a DBRef.
1170
     *
1171
     * @param mixed $value
1172
     * @return boolean
1173
     */
1174 66
    private function hasDBRefFields($value)
1175
    {
1176 66
        if ( ! is_array($value) && ! is_object($value)) {
1177
            return false;
1178
        }
1179
1180 66
        if (is_object($value)) {
1181
            $value = get_object_vars($value);
1182
        }
1183
1184 66
        foreach ($value as $key => $_) {
1185 66
            if ($key === '$ref' || $key === '$id' || $key === '$db') {
1186 3
                return true;
1187
            }
1188 65
        }
1189
1190 65
        return false;
1191
    }
1192
1193
    /**
1194
     * Checks whether the value has query operators.
1195
     *
1196
     * @param mixed $value
1197
     * @return boolean
1198
     */
1199 70
    private function hasQueryOperators($value)
1200
    {
1201 70
        if ( ! is_array($value) && ! is_object($value)) {
1202
            return false;
1203
        }
1204
1205 70
        if (is_object($value)) {
1206
            $value = get_object_vars($value);
1207
        }
1208
1209 70
        foreach ($value as $key => $_) {
1210 70
            if (isset($key[0]) && $key[0] === '$') {
1211 66
                return true;
1212
            }
1213 9
        }
1214
1215 9
        return false;
1216
    }
1217
1218
    /**
1219
     * Gets the array of discriminator values for the given ClassMetadata
1220
     *
1221
     * @param ClassMetadata $metadata
1222
     * @return array
1223
     */
1224 18
    private function getClassDiscriminatorValues(ClassMetadata $metadata)
1225
    {
1226 18
        $discriminatorValues = array($metadata->discriminatorValue);
1227 18
        foreach ($metadata->subClasses as $className) {
1228 7
            if ($key = array_search($className, $metadata->discriminatorMap)) {
1229 7
                $discriminatorValues[] = $key;
1230 7
            }
1231 18
        }
1232
1233
        // If a defaultDiscriminatorValue is set and it is among the discriminators being queries, add NULL to the list
1234 18 View Code Duplication
        if ($metadata->defaultDiscriminatorValue && (array_search($metadata->defaultDiscriminatorValue, $discriminatorValues)) !== false) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1235 2
            $discriminatorValues[] = null;
1236 2
        }
1237
1238 18
        return $discriminatorValues;
1239
    }
1240
1241 534
    private function handleCollections($document, $options)
1242
    {
1243
        // Collection deletions (deletions of complete collections)
1244 534
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
1245 97
            if ($this->uow->isCollectionScheduledForDeletion($coll)) {
1246 30
                $this->cp->delete($coll, $options);
1247 30
            }
1248 534
        }
1249
        // Collection updates (deleteRows, updateRows, insertRows)
1250 534
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
1251 97
            if ($this->uow->isCollectionScheduledForUpdate($coll)) {
1252 90
                $this->cp->update($coll, $options);
1253 90
            }
1254 534
        }
1255
        // Take new snapshots from visited collections
1256 534
        foreach ($this->uow->getVisitedCollections($document) as $coll) {
1257 224
            $coll->takeSnapshot();
1258 534
        }
1259 534
    }
1260
}
1261