Completed
Pull Request — master (#1331)
by Maciej
10:31
created

loadReferenceManyWithRepositoryMethod()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 3

Importance

Changes 1
Bugs 1 Features 0
Metric Value
dl 0
loc 13
ccs 11
cts 11
cp 1
rs 9.4286
c 1
b 1
f 0
cc 3
eloc 9
nc 3
nop 1
crap 3
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 673
    public function __construct(PersistenceBuilder $pb, DocumentManager $dm, EventManager $evm, UnitOfWork $uow, HydratorFactory $hydratorFactory, ClassMetadata $class, CriteriaMerger $cm = null)
128
    {
129 673
        $this->pb = $pb;
130 673
        $this->dm = $dm;
131 673
        $this->evm = $evm;
132 673
        $this->cm = $cm ?: new CriteriaMerger();
133 673
        $this->uow = $uow;
134 673
        $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 673
        $this->class = $class;
136 673
        $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 673
        $this->cp = $this->uow->getCollectionPersister();
138 673
    }
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 476
    public function addInsert($document)
164
    {
165 476
        $this->queuedInserts[spl_object_hash($document)] = $document;
166 476
    }
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 476
    public function executeInserts(array $options = array())
217
    {
218 476
        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 476
        $inserts = array();
223 476
        foreach ($this->queuedInserts as $oid => $document) {
224 476
            $data = $this->pb->prepareInsertData($document);
225
226
            // Set the initial version for each insert
227 475 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 475
            $inserts[$oid] = $data;
241 475
        }
242
243 475
        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 475
                $this->collection->batchInsert($inserts, $options);
246 475
            } catch (\MongoException $e) {
247 7
                $this->queuedInserts = array();
248 7
                throw $e;
249
            }
250 475
        }
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 475
        foreach ($this->queuedInserts as $document) {
257 475
            $this->handleCollections($document, $options);
258 475
        }
259
260 475
        $this->queuedInserts = array();
261 475
    }
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 13
            $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 14
            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
    public function update($document, array $options = array())
361
    {
362
        $id = $this->uow->getDocumentIdentifier($document);
363
        $update = $this->pb->prepareUpdateData($document);
364
365
        $id = $this->class->getDatabaseIdentifierValue($id);
366
        $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
        if ($this->class->isVersioned) {
372
            $versionMapping = $this->class->fieldMappings[$this->class->versionField];
373
            $currentVersion = $this->class->reflFields[$this->class->versionField]->getValue($document);
374
            if ($versionMapping['type'] === 'int') {
375
                $nextVersion = $currentVersion + 1;
376
                $update['$inc'][$versionMapping['name']] = 1;
377
                $query[$versionMapping['name']] = $currentVersion;
378
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
379
            } elseif ($versionMapping['type'] === 'date') {
380
                $nextVersion = new \DateTime();
381
                $update['$set'][$versionMapping['name']] = new \MongoDate($nextVersion->getTimestamp());
382
                $query[$versionMapping['name']] = new \MongoDate($currentVersion->getTimestamp());
383
                $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion);
384
            }
385
        }
386
387
        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
            if ($this->class->isLockable) {
391
                $isLocked = $this->class->reflFields[$this->class->lockField]->getValue($document);
392
                $lockMapping = $this->class->fieldMappings[$this->class->lockField];
393
                if ($isLocked) {
394
                    $update['$unset'] = array($lockMapping['name'] => true);
395
                } else {
396
                    $query[$lockMapping['name']] = array('$exists' => false);
397
                }
398
            }
399
400
            $result = $this->collection->update($query, $update, $options);
401
402 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
                throw LockException::lockFailed($document);
404
            }
405
        }
406
407
        $this->handleCollections($document, $options);
408
    }
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 11
    public function delete($document, array $options = array())
418
    {
419 11
        $id = $this->uow->getDocumentIdentifier($document);
420 11
        $query = array('_id' => $this->class->getDatabaseIdentifierValue($id));
421
422 11
        if ($this->class->isLockable) {
423 2
            $query[$this->class->lockField] = array('$exists' => false);
424 2
        }
425
426 11
        $result = $this->collection->remove($query, $options);
427
428 11 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 9
    }
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 15
    public function refresh($id, $document)
440
    {
441 15
        $data = $this->collection->findOne(array('_id' => $id));
442 15
        $data = $this->hydratorFactory->hydrate($document, $data);
443 15
        $this->uow->setOriginalDocumentData($document, $data);
444 15
    }
445
446
    /**
447
     * Finds a document by a set of criteria.
448
     *
449
     * If a scalar or MongoId is provided for $criteria, it will be used to
450
     * match an _id value.
451
     *
452
     * @param mixed   $criteria Query criteria
453
     * @param object  $document Document to load the data into. If not specified, a new document is created.
454
     * @param array   $hints    Hints for document creation
455
     * @param integer $lockMode
456
     * @param array   $sort     Sort array for Cursor::sort()
457
     * @throws \Doctrine\ODM\MongoDB\LockException
458
     * @return object|null The loaded and managed document instance or null if no document was found
459
     * @todo Check identity map? loadById method? Try to guess whether $criteria is the id?
460
     */
461 310
    public function load($criteria, $document = null, array $hints = array(), $lockMode = 0, array $sort = null)
0 ignored issues
show
Unused Code introduced by
The parameter $lockMode is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
462
    {
463
        // TODO: remove this
464 310
        if ($criteria === null || is_scalar($criteria) || $criteria instanceof \MongoId) {
465
            $criteria = array('_id' => $criteria);
466
        }
467
468 310
        $criteria = $this->prepareQueryOrNewObj($criteria);
0 ignored issues
show
Bug introduced by
It seems like $criteria can also be of type object; however, Doctrine\ODM\MongoDB\Per...:prepareQueryOrNewObj() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
469 310
        $criteria = $this->addDiscriminatorToPreparedQuery($criteria);
470 310
        $criteria = $this->addFilterToPreparedQuery($criteria);
471
472 310
        $cursor = $this->collection->find($criteria);
473
474 310
        if (null !== $sort) {
475 90
            $cursor->sort($this->prepareSortOrProjection($sort));
476 90
        }
477
478 310
        $result = $cursor->getSingleResult();
479
480 310
        if ($this->class->isLockable) {
481 1
            $lockMapping = $this->class->fieldMappings[$this->class->lockField];
482 1
            if (isset($result[$lockMapping['name']]) && $result[$lockMapping['name']] === LockMode::PESSIMISTIC_WRITE) {
483 1
                throw LockException::lockFailed($result);
484
            }
485
        }
486
487 309
        return $this->createDocument($result, $document, $hints);
488
    }
489
490
    /**
491
     * Finds documents by a set of criteria.
492
     *
493
     * @param array        $criteria Query criteria
494
     * @param array        $sort     Sort array for Cursor::sort()
495
     * @param integer|null $limit    Limit for Cursor::limit()
496
     * @param integer|null $skip     Skip for Cursor::skip()
497
     * @return Cursor
498
     */
499 22
    public function loadAll(array $criteria = array(), array $sort = null, $limit = null, $skip = null)
500
    {
501 22
        $criteria = $this->prepareQueryOrNewObj($criteria);
502 22
        $criteria = $this->addDiscriminatorToPreparedQuery($criteria);
503 22
        $criteria = $this->addFilterToPreparedQuery($criteria);
504
505 22
        $baseCursor = $this->collection->find($criteria);
506 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...
507
508 22
        if (null !== $sort) {
509 3
            $cursor->sort($sort);
510 3
        }
511
512 22
        if (null !== $limit) {
513 2
            $cursor->limit($limit);
514 2
        }
515
516 22
        if (null !== $skip) {
517 2
            $cursor->skip($skip);
518 2
        }
519
520 22
        return $cursor;
521
    }
522
523
    /**
524
     * Wraps the supplied base cursor in the corresponding ODM class.
525
     *
526
     * @param CursorInterface $baseCursor
527
     * @return Cursor
528
     */
529 22
    private function wrapCursor(CursorInterface $baseCursor)
530
    {
531 22
        return new Cursor($baseCursor, $this->dm->getUnitOfWork(), $this->class);
532
    }
533
534
    /**
535
     * Checks whether the given managed document exists in the database.
536
     *
537
     * @param object $document
538
     * @return boolean TRUE if the document exists in the database, FALSE otherwise.
539
     */
540 3
    public function exists($document)
541
    {
542 3
        $id = $this->class->getIdentifierObject($document);
543 3
        return (boolean) $this->collection->findOne(array('_id' => $id), array('_id'));
544
    }
545
546
    /**
547
     * Locks document by storing the lock mode on the mapped lock field.
548
     *
549
     * @param object $document
550
     * @param int $lockMode
551
     */
552 5
    public function lock($document, $lockMode)
553
    {
554 5
        $id = $this->uow->getDocumentIdentifier($document);
555 5
        $criteria = array('_id' => $this->class->getDatabaseIdentifierValue($id));
556 5
        $lockMapping = $this->class->fieldMappings[$this->class->lockField];
557 5
        $this->collection->update($criteria, array('$set' => array($lockMapping['name'] => $lockMode)));
558 5
        $this->class->reflFields[$this->class->lockField]->setValue($document, $lockMode);
559 5
    }
560
561
    /**
562
     * Releases any lock that exists on this document.
563
     *
564
     * @param object $document
565
     */
566 1
    public function unlock($document)
567
    {
568 1
        $id = $this->uow->getDocumentIdentifier($document);
569 1
        $criteria = array('_id' => $this->class->getDatabaseIdentifierValue($id));
570 1
        $lockMapping = $this->class->fieldMappings[$this->class->lockField];
571 1
        $this->collection->update($criteria, array('$unset' => array($lockMapping['name'] => true)));
572 1
        $this->class->reflFields[$this->class->lockField]->setValue($document, null);
573 1
    }
574
575
    /**
576
     * Creates or fills a single document object from an query result.
577
     *
578
     * @param object $result The query result.
579
     * @param object $document The document object to fill, if any.
580
     * @param array $hints Hints for document creation.
581
     * @return object The filled and managed document object or NULL, if the query result is empty.
582
     */
583 309
    private function createDocument($result, $document = null, array $hints = array())
584
    {
585 309
        if ($result === null) {
586 92
            return null;
587
        }
588
589 266
        if ($document !== null) {
590 34
            $hints[Query::HINT_REFRESH] = true;
591 34
            $id = $this->class->getPHPIdentifierValue($result['_id']);
592 34
            $this->uow->registerManaged($document, $id, $result);
0 ignored issues
show
Documentation introduced by
$result is of type object, but the function expects a array.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
593 34
        }
594
595 266
        return $this->uow->getOrCreateDocument($this->class->name, $result, $hints, $document);
0 ignored issues
show
Documentation introduced by
$result is of type object, but the function expects a array.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
596
    }
597
598
    /**
599
     * Loads a PersistentCollection data. Used in the initialize() method.
600
     *
601
     * @param PersistentCollection $collection
602
     */
603 123
    public function loadCollection(PersistentCollection $collection)
604
    {
605 123
        $mapping = $collection->getMapping();
606 123
        switch ($mapping['association']) {
607 123
            case ClassMetadata::EMBED_MANY:
608 77
                $this->loadEmbedManyCollection($collection);
609 77
                break;
610
611 51
            case ClassMetadata::REFERENCE_MANY:
612 51
                if (isset($mapping['repositoryMethod']) && $mapping['repositoryMethod']) {
613 3
                    $this->loadReferenceManyWithRepositoryMethod($collection);
614 3
                } else {
615 48
                    if ($mapping['isOwningSide']) {
616 38
                        $this->loadReferenceManyCollectionOwningSide($collection);
617 38
                    } else {
618 14
                        $this->loadReferenceManyCollectionInverseSide($collection);
619
                    }
620
                }
621 51
                break;
622 123
        }
623 123
    }
624
625 77
    private function loadEmbedManyCollection(PersistentCollection $collection)
626
    {
627 77
        $embeddedDocuments = $collection->getMongoData();
628 77
        $mapping = $collection->getMapping();
629 77
        $owner = $collection->getOwner();
630 77
        if ($embeddedDocuments) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $embeddedDocuments of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
631 69
            foreach ($embeddedDocuments as $key => $embeddedDocument) {
632 69
                $className = $this->uow->getClassNameForAssociation($mapping, $embeddedDocument);
633 69
                $embeddedMetadata = $this->dm->getClassMetadata($className);
634 69
                $embeddedDocumentObject = $embeddedMetadata->newInstance();
635
636 69
                $this->uow->setParentAssociation($embeddedDocumentObject, $mapping, $owner, $mapping['name'] . '.' . $key);
637
638 69
                $data = $this->hydratorFactory->hydrate($embeddedDocumentObject, $embeddedDocument);
639 69
                $id = $embeddedMetadata->identifier && isset($data[$embeddedMetadata->identifier])
640 69
                    ? $data[$embeddedMetadata->identifier]
641 69
                    : null;
642
643 69
                $this->uow->registerManaged($embeddedDocumentObject, $id, $data);
644 69
                if (CollectionHelper::isHash($mapping['strategy'])) {
645 22
                    $collection->set($key, $embeddedDocumentObject);
646 22
                } else {
647 54
                    $collection->add($embeddedDocumentObject);
648
                }
649 69
            }
650 69
        }
651 77
    }
652
653 38
    private function loadReferenceManyCollectionOwningSide(PersistentCollection $collection)
654
    {
655 38
        $hints = $collection->getHints();
656 38
        $mapping = $collection->getMapping();
657 38
        $groupedIds = array();
658
659 38
        $sorted = isset($mapping['sort']) && $mapping['sort'];
660
661 38
        foreach ($collection->getMongoData() as $key => $reference) {
662 35
            if (isset($mapping['simple']) && $mapping['simple']) {
663 4
                $className = $mapping['targetDocument'];
664 4
                $mongoId = $reference;
665 4
            } else {
666 31
                $className = $this->uow->getClassNameForAssociation($mapping, $reference);
667 31
                $mongoId = $reference['$id'];
668
            }
669 35
            $id = $this->dm->getClassMetadata($className)->getPHPIdentifierValue($mongoId);
670
671
            // create a reference to the class and id
672 35
            $reference = $this->dm->getReference($className, $id);
673
674
            // no custom sort so add the references right now in the order they are embedded
675 35
            if ( ! $sorted) {
676 34
                if (CollectionHelper::isHash($mapping['strategy'])) {
677
                    $collection->set($key, $reference);
678
                } else {
679 34
                    $collection->add($reference);
680
                }
681 34
            }
682
683
            // only query for the referenced object if it is not already initialized or the collection is sorted
684 35
            if (($reference instanceof Proxy && ! $reference->__isInitialized__) || $sorted) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

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

Available Fixes

  1. Adding an additional type check:

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

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

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

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

Loading history...
711
                $cursor->setReadPreference($hints[Query::HINT_READ_PREFERENCE], $hints[Query::HINT_READ_PREFERENCE_TAGS]);
712
            }
713 28
            $documents = $cursor->toArray(false);
714 28
            foreach ($documents as $documentData) {
715 27
                $document = $this->uow->getById($documentData['_id'], $class);
716 27
                $data = $this->hydratorFactory->hydrate($document, $documentData);
717 27
                $this->uow->setOriginalDocumentData($document, $data);
718 27
                $document->__isInitialized__ = true;
719 27
                if ($sorted) {
720 1
                    $collection->add($document);
721 1
                }
722 28
            }
723 38
        }
724 38
    }
725
726 14
    private function loadReferenceManyCollectionInverseSide(PersistentCollection $collection)
727
    {
728 14
        $query = $this->createReferenceManyInverseSideQuery($collection);
729 14
        $documents = $query->execute()->toArray(false);
730 14
        foreach ($documents as $key => $document) {
731 13
            $collection->add($document);
732 14
        }
733 14
    }
734
735
    /**
736
     * @param PersistentCollection $collection
737
     *
738
     * @return Query
739
     */
740 16
    public function createReferenceManyInverseSideQuery(PersistentCollection $collection)
741
    {
742 16
        $hints = $collection->getHints();
743 16
        $mapping = $collection->getMapping();
744 16
        $owner = $collection->getOwner();
745 16
        $ownerClass = $this->dm->getClassMetadata(get_class($owner));
746 16
        $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
747 16
        $mappedByMapping = isset($targetClass->fieldMappings[$mapping['mappedBy']]) ? $targetClass->fieldMappings[$mapping['mappedBy']] : array();
748 16
        $mappedByFieldName = isset($mappedByMapping['simple']) && $mappedByMapping['simple'] ? $mapping['mappedBy'] : $mapping['mappedBy'] . '.$id';
749 16
        $criteria = $this->cm->merge(
750 16
            array($mappedByFieldName => $ownerClass->getIdentifierObject($owner)),
751 16
            $this->dm->getFilterCollection()->getFilterCriteria($targetClass),
752 16
            isset($mapping['criteria']) ? $mapping['criteria'] : array()
753 16
        );
754 16
        $criteria = $this->uow->getDocumentPersister($mapping['targetDocument'])->prepareQueryOrNewObj($criteria);
755 16
        $qb = $this->dm->createQueryBuilder($mapping['targetDocument'])
756 16
            ->setQueryArray($criteria);
757
758 16
        if (isset($mapping['sort'])) {
759 16
            $qb->sort($mapping['sort']);
760 16
        }
761 16
        if (isset($mapping['limit'])) {
762 1
            $qb->limit($mapping['limit']);
763 1
        }
764 16
        if (isset($mapping['skip'])) {
765
            $qb->skip($mapping['skip']);
766
        }
767 16
        if ( ! empty($hints[Query::HINT_SLAVE_OKAY])) {
768
            $qb->slaveOkay(true);
769
        }
770 16 View Code Duplication
        if ( ! empty($hints[Query::HINT_READ_PREFERENCE])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
771
            $qb->setReadPreference($hints[Query::HINT_READ_PREFERENCE], $hints[Query::HINT_READ_PREFERENCE_TAGS]);
772
        }
773
774 16
        return $qb->getQuery();
775
    }
776
777 3
    private function loadReferenceManyWithRepositoryMethod(PersistentCollection $collection)
778
    {
779 3
        $cursor = $this->createReferenceManyWithRepositoryMethodCursor($collection);
780 3
        $mapping = $collection->getMapping();        
781 3
        $documents = $cursor->toArray(false);
0 ignored issues
show
Unused Code introduced by
The call to CursorInterface::toArray() has too many arguments starting with false.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
782 3
        foreach ($documents as $key => $obj) {
783 3
            if (CollectionHelper::isHash($mapping['strategy'])) {
784 1
                $collection->set($key, $obj);
785 1
            } else {
786 2
                $collection->add($obj);
787
            }
788 3
        }
789 3
    }
790
791
    /**
792
     * @param PersistentCollection $collection
793
     *
794
     * @return CursorInterface
795
     */
796 3
    public function createReferenceManyWithRepositoryMethodCursor(PersistentCollection $collection)
797
    {
798 3
        $hints = $collection->getHints();
799 3
        $mapping = $collection->getMapping();
800 3
        $cursor = $this->dm->getRepository($mapping['targetDocument'])
801 3
            ->$mapping['repositoryMethod']($collection->getOwner());
802
803 3
        if ( ! $cursor instanceof CursorInterface) {
804
            throw new \BadMethodCallException("Expected repository method {$mapping['repositoryMethod']} to return a CursorInterface");
805
        }
806
807 3
        if (isset($mapping['sort'])) {
808 3
            $cursor->sort($mapping['sort']);
809 3
        }
810 3
        if (isset($mapping['limit'])) {
811
            $cursor->limit($mapping['limit']);
812
        }
813 3
        if (isset($mapping['skip'])) {
814
            $cursor->skip($mapping['skip']);
815
        }
816 3
        if ( ! empty($hints[Query::HINT_SLAVE_OKAY])) {
817
            $cursor->slaveOkay(true);
818
        }
819 3 View Code Duplication
        if ( ! empty($hints[Query::HINT_READ_PREFERENCE])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
820
            $cursor->setReadPreference($hints[Query::HINT_READ_PREFERENCE], $hints[Query::HINT_READ_PREFERENCE_TAGS]);
821
        }
822
823 3
        return $cursor;
824
    }
825
826
    /**
827
     * Prepare a sort or projection array by converting keys, which are PHP
828
     * property names, to MongoDB field names.
829
     *
830
     * @param array $fields
831
     * @return array
832
     */
833 127
    public function prepareSortOrProjection(array $fields)
834
    {
835 127
        $preparedFields = array();
836
837 127
        foreach ($fields as $key => $value) {
838 33
            $preparedFields[$this->prepareFieldName($key)] = $value;
839 127
        }
840
841 127
        return $preparedFields;
842
    }
843
844
    /**
845
     * Prepare a mongodb field name and convert the PHP property names to MongoDB field names.
846
     *
847
     * @param string $fieldName
848
     * @return string
849
     */
850 85
    public function prepareFieldName($fieldName)
851
    {
852 85
        list($fieldName) = $this->prepareQueryElement($fieldName, null, null, false);
853
854 85
        return $fieldName;
855
    }
856
857
    /**
858
     * Adds discriminator criteria to an already-prepared query.
859
     *
860
     * This method should be used once for query criteria and not be used for
861
     * nested expressions. It should be called before
862
     * {@link DocumentPerister::addFilterToPreparedQuery()}.
863
     *
864
     * @param array $preparedQuery
865
     * @return array
866
     */
867 430
    public function addDiscriminatorToPreparedQuery(array $preparedQuery)
868
    {
869
        /* If the class has a discriminator field, which is not already in the
870
         * criteria, inject it now. The field/values need no preparation.
871
         */
872 430
        if ($this->class->hasDiscriminator() && ! isset($preparedQuery[$this->class->discriminatorField])) {
873 21
            $discriminatorValues = $this->getClassDiscriminatorValues($this->class);
874 21
            if ((count($discriminatorValues) === 1)) {
875 13
                $preparedQuery[$this->class->discriminatorField] = $discriminatorValues[0];
876 13
            } else {
877 10
                $preparedQuery[$this->class->discriminatorField] = array('$in' => $discriminatorValues);
878
            }
879 21
        }
880
881 430
        return $preparedQuery;
882
    }
883
884
    /**
885
     * Adds filter criteria to an already-prepared query.
886
     *
887
     * This method should be used once for query criteria and not be used for
888
     * nested expressions. It should be called after
889
     * {@link DocumentPerister::addDiscriminatorToPreparedQuery()}.
890
     *
891
     * @param array $preparedQuery
892
     * @return array
893
     */
894 431
    public function addFilterToPreparedQuery(array $preparedQuery)
895
    {
896
        /* If filter criteria exists for this class, prepare it and merge
897
         * over the existing query.
898
         *
899
         * @todo Consider recursive merging in case the filter criteria and
900
         * prepared query both contain top-level $and/$or operators.
901
         */
902 431
        if ($filterCriteria = $this->dm->getFilterCollection()->getFilterCriteria($this->class)) {
903 16
            $preparedQuery = $this->cm->merge($preparedQuery, $this->prepareQueryOrNewObj($filterCriteria));
904 16
        }
905
906 431
        return $preparedQuery;
907
    }
908
909
    /**
910
     * Prepares the query criteria or new document object.
911
     *
912
     * PHP field names and types will be converted to those used by MongoDB.
913
     *
914
     * @param array $query
915
     * @return array
916
     */
917 464
    public function prepareQueryOrNewObj(array $query)
918
    {
919 464
        $preparedQuery = array();
920
921 464
        foreach ($query as $key => $value) {
922
            // Recursively prepare logical query clauses
923 425
            if (in_array($key, array('$and', '$or', '$nor')) && is_array($value)) {
924 20
                foreach ($value as $k2 => $v2) {
925 20
                    $preparedQuery[$key][$k2] = $this->prepareQueryOrNewObj($v2);
926 20
                }
927 20
                continue;
928
            }
929
930 425
            if (isset($key[0]) && $key[0] === '$' && is_array($value)) {
931 19
                $preparedQuery[$key] = $this->prepareQueryOrNewObj($value);
932 19
                continue;
933
            }
934
935 425
            list($key, $value) = $this->prepareQueryElement($key, $value, null, true);
936
937 425
            $preparedQuery[$key] = is_array($value)
938 425
                ? array_map('\Doctrine\ODM\MongoDB\Types\Type::convertPHPToDatabaseValue', $value)
939 425
                : Type::convertPHPToDatabaseValue($value);
940 464
        }
941
942 464
        return $preparedQuery;
943
    }
944
945
    /**
946
     * Prepares a query value and converts the PHP value to the database value
947
     * if it is an identifier.
948
     *
949
     * It also handles converting $fieldName to the database name if they are different.
950
     *
951
     * @param string $fieldName
952
     * @param mixed $value
953
     * @param ClassMetadata $class        Defaults to $this->class
954
     * @param boolean $prepareValue Whether or not to prepare the value
955
     * @return array        Prepared field name and value
956
     */
957 456
    private function prepareQueryElement($fieldName, $value = null, $class = null, $prepareValue = true)
958
    {
959 456
        $class = isset($class) ? $class : $this->class;
960
961
        // @todo Consider inlining calls to ClassMetadataInfo methods
962
963
        // Process all non-identifier fields by translating field names
964 456
        if ($class->hasField($fieldName) && ! $class->isIdentifier($fieldName)) {
965 214
            $mapping = $class->fieldMappings[$fieldName];
966 214
            $fieldName = $mapping['name'];
967
968 214
            if ( ! $prepareValue) {
969 62
                return array($fieldName, $value);
970
            }
971
972
            // Prepare mapped, embedded objects
973 172
            if ( ! empty($mapping['embedded']) && is_object($value) &&
974 172
                ! $this->dm->getMetadataFactory()->isTransient(get_class($value))) {
975
                return array($fieldName, $this->pb->prepareEmbeddedDocumentValue($mapping, $value));
976
            }
977
978
            // No further preparation unless we're dealing with a simple reference
979
            // We can't have expressions in empty() with PHP < 5.5, so store it in a variable
980 172
            $arrayValue = (array) $value;
981 172
            if (empty($mapping['reference']) || empty($mapping['simple']) || empty($arrayValue)) {
982 108
                return array($fieldName, $value);
983
            }
984
985
            // Additional preparation for one or more simple reference values
986 89
            $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
987
988 89
            if ( ! is_array($value)) {
989 84
                return array($fieldName, $targetClass->getDatabaseIdentifierValue($value));
990
            }
991
992
            // Objects without operators or with DBRef fields can be converted immediately
993 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...
994 3
                return array($fieldName, $targetClass->getDatabaseIdentifierValue($value));
995
            }
996
997 7
            return array($fieldName, $this->prepareQueryExpression($value, $targetClass));
998
        }
999
1000
        // Process identifier fields
1001 341
        if (($class->hasField($fieldName) && $class->isIdentifier($fieldName)) || $fieldName === '_id') {
1002 277
            $fieldName = '_id';
1003
1004 277
            if ( ! $prepareValue) {
1005 16
                return array($fieldName, $value);
1006
            }
1007
1008 263
            if ( ! is_array($value)) {
1009 241
                return array($fieldName, $class->getDatabaseIdentifierValue($value));
1010
            }
1011
1012
            // Objects without operators or with DBRef fields can be converted immediately
1013 47 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...
1014 6
                return array($fieldName, $class->getDatabaseIdentifierValue($value));
1015
            }
1016
1017 42
            return array($fieldName, $this->prepareQueryExpression($value, $class));
1018
        }
1019
1020
        // No processing for unmapped, non-identifier, non-dotted field names
1021 96
        if (strpos($fieldName, '.') === false) {
1022 42
            return array($fieldName, $value);
1023
        }
1024
1025
        /* Process "fieldName.objectProperty" queries (on arrays or objects).
1026
         *
1027
         * We can limit parsing here, since at most three segments are
1028
         * significant: "fieldName.objectProperty" with an optional index or key
1029
         * for collections stored as either BSON arrays or objects.
1030
         */
1031 59
        $e = explode('.', $fieldName, 4);
1032
1033
        // No further processing for unmapped fields
1034 59
        if ( ! isset($class->fieldMappings[$e[0]])) {
1035 3
            return array($fieldName, $value);
1036
        }
1037
1038 57
        $mapping = $class->fieldMappings[$e[0]];
1039 57
        $e[0] = $mapping['name'];
1040
1041
        // Hash and raw fields will not be prepared beyond the field name
1042 57
        if ($mapping['type'] === Type::HASH || $mapping['type'] === Type::RAW) {
1043 1
            $fieldName = implode('.', $e);
1044
1045 1
            return array($fieldName, $value);
1046
        }
1047
1048 56
        if (isset($mapping['strategy']) && CollectionHelper::isHash($mapping['strategy'])
1049 56
                && isset($e[2])) {
1050 1
            $objectProperty = $e[2];
1051 1
            $objectPropertyPrefix = $e[1] . '.';
1052 1
            $nextObjectProperty = implode('.', array_slice($e, 3));
1053 56
        } elseif ($e[1] != '$') {
1054 54
            $fieldName = $e[0] . '.' . $e[1];
1055 54
            $objectProperty = $e[1];
1056 54
            $objectPropertyPrefix = '';
1057 54
            $nextObjectProperty = implode('.', array_slice($e, 2));
1058 55
        } elseif (isset($e[2])) {
1059 1
            $fieldName = $e[0] . '.' . $e[1] . '.' . $e[2];
1060 1
            $objectProperty = $e[2];
1061 1
            $objectPropertyPrefix = $e[1] . '.';
1062 1
            $nextObjectProperty = implode('.', array_slice($e, 3));
1063 1
        } else {
1064 1
            $fieldName = $e[0] . '.' . $e[1];
1065
1066 1
            return array($fieldName, $value);
1067
        }
1068
1069
        // No further processing for fields without a targetDocument mapping
1070 56
        if ( ! isset($mapping['targetDocument'])) {
1071 1
            if ($nextObjectProperty) {
1072
                $fieldName .= '.'.$nextObjectProperty;
1073
            }
1074
1075 1
            return array($fieldName, $value);
1076
        }
1077
1078 55
        $targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
1079
1080
        // No further processing for unmapped targetDocument fields
1081 55
        if ( ! $targetClass->hasField($objectProperty)) {
1082 23
            if ($nextObjectProperty) {
1083
                $fieldName .= '.'.$nextObjectProperty;
1084
            }
1085
1086 23
            return array($fieldName, $value);
1087
        }
1088
1089 34
        $targetMapping = $targetClass->getFieldMapping($objectProperty);
1090 34
        $objectPropertyIsId = $targetClass->isIdentifier($objectProperty);
1091
1092
        // Prepare DBRef identifiers or the mapped field's property path
1093 34
        $fieldName = ($objectPropertyIsId && ! empty($mapping['reference']) && empty($mapping['simple']))
1094 34
            ? $e[0] . '.$id'
1095 34
            : $e[0] . '.' . $objectPropertyPrefix . $targetMapping['name'];
1096
1097
        // Process targetDocument identifier fields
1098 34
        if ($objectPropertyIsId) {
1099 14
            if ( ! $prepareValue) {
1100 1
                return array($fieldName, $value);
1101
            }
1102
1103 13
            if ( ! is_array($value)) {
1104 2
                return array($fieldName, $targetClass->getDatabaseIdentifierValue($value));
1105
            }
1106
1107
            // Objects without operators or with DBRef fields can be converted immediately
1108 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...
1109 3
                return array($fieldName, $targetClass->getDatabaseIdentifierValue($value));
1110
            }
1111
1112 12
            return array($fieldName, $this->prepareQueryExpression($value, $targetClass));
1113
        }
1114
1115
        /* The property path may include a third field segment, excluding the
1116
         * collection item pointer. If present, this next object property must
1117
         * be processed recursively.
1118
         */
1119 20
        if ($nextObjectProperty) {
1120
            // Respect the targetDocument's class metadata when recursing
1121 14
            $nextTargetClass = isset($targetMapping['targetDocument'])
1122 14
                ? $this->dm->getClassMetadata($targetMapping['targetDocument'])
1123 14
                : null;
1124
1125 14
            list($key, $value) = $this->prepareQueryElement($nextObjectProperty, $value, $nextTargetClass, $prepareValue);
1126
1127 14
            $fieldName .= '.' . $key;
1128 14
        }
1129
1130 20
        return array($fieldName, $value);
1131
    }
1132
1133
    /**
1134
     * Prepares a query expression.
1135
     *
1136
     * @param array|object  $expression
1137
     * @param ClassMetadata $class
1138
     * @return array
1139
     */
1140 61
    private function prepareQueryExpression($expression, $class)
1141
    {
1142 61
        foreach ($expression as $k => $v) {
1143
            // Ignore query operators whose arguments need no type conversion
1144 61
            if (in_array($k, array('$exists', '$type', '$mod', '$size'))) {
1145 12
                continue;
1146
            }
1147
1148
            // Process query operators whose argument arrays need type conversion
1149 61
            if (in_array($k, array('$in', '$nin', '$all')) && is_array($v)) {
1150 58
                foreach ($v as $k2 => $v2) {
1151 58
                    $expression[$k][$k2] = $class->getDatabaseIdentifierValue($v2);
1152 58
                }
1153 58
                continue;
1154
            }
1155
1156
            // Recursively process expressions within a $not operator
1157 15
            if ($k === '$not' && is_array($v)) {
1158 11
                $expression[$k] = $this->prepareQueryExpression($v, $class);
1159 11
                continue;
1160
            }
1161
1162 15
            $expression[$k] = $class->getDatabaseIdentifierValue($v);
1163 61
        }
1164
1165 61
        return $expression;
1166
    }
1167
1168
    /**
1169
     * Checks whether the value has DBRef fields.
1170
     *
1171
     * This method doesn't check if the the value is a complete DBRef object,
1172
     * although it should return true for a DBRef. Rather, we're checking that
1173
     * the value has one or more fields for a DBref. In practice, this could be
1174
     * $elemMatch criteria for matching a DBRef.
1175
     *
1176
     * @param mixed $value
1177
     * @return boolean
1178
     */
1179 62
    private function hasDBRefFields($value)
1180
    {
1181 62
        if ( ! is_array($value) && ! is_object($value)) {
1182
            return false;
1183
        }
1184
1185 62
        if (is_object($value)) {
1186
            $value = get_object_vars($value);
1187
        }
1188
1189 62
        foreach ($value as $key => $_) {
1190 62
            if ($key === '$ref' || $key === '$id' || $key === '$db') {
1191 3
                return true;
1192
            }
1193 61
        }
1194
1195 61
        return false;
1196
    }
1197
1198
    /**
1199
     * Checks whether the value has query operators.
1200
     *
1201
     * @param mixed $value
1202
     * @return boolean
1203
     */
1204 66
    private function hasQueryOperators($value)
1205
    {
1206 66
        if ( ! is_array($value) && ! is_object($value)) {
1207
            return false;
1208
        }
1209
1210 66
        if (is_object($value)) {
1211
            $value = get_object_vars($value);
1212
        }
1213
1214 66
        foreach ($value as $key => $_) {
1215 66
            if (isset($key[0]) && $key[0] === '$') {
1216 62
                return true;
1217
            }
1218 9
        }
1219
1220 9
        return false;
1221
    }
1222
1223
    /**
1224
     * Gets the array of discriminator values for the given ClassMetadata
1225
     *
1226
     * @param ClassMetadata $metadata
1227
     * @return array
1228
     */
1229 21
    private function getClassDiscriminatorValues(ClassMetadata $metadata)
1230
    {
1231 21
        $discriminatorValues = array($metadata->discriminatorValue);
1232 21
        foreach ($metadata->subClasses as $className) {
1233 8
            if ($key = array_search($className, $metadata->discriminatorMap)) {
1234 8
                $discriminatorValues[] = $key;
1235 8
            }
1236 21
        }
1237
1238
        // If a defaultDiscriminatorValue is set and it is among the discriminators being queries, add NULL to the list
1239 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...
1240 2
            $discriminatorValues[] = null;
1241 2
        }
1242
1243 21
        return $discriminatorValues;
1244
    }
1245
1246 539
    private function handleCollections($document, $options)
1247
    {
1248
        // Collection deletions (deletions of complete collections)
1249 539
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
1250 15
            if ($this->uow->isCollectionScheduledForDeletion($coll)) {
1251
                $this->cp->delete($coll, $options);
1252
            }
1253 539
        }
1254
        // Collection updates (deleteRows, updateRows, insertRows)
1255 539
        foreach ($this->uow->getScheduledCollections($document) as $coll) {
1256 15
            if ($this->uow->isCollectionScheduledForUpdate($coll)) {
1257 15
                $this->cp->update($coll, $options);
1258 15
            }
1259 539
        }
1260
        // Take new snapshots from visited collections
1261 539
        foreach ($this->uow->getVisitedCollections($document) as $coll) {
1262 205
            $coll->takeSnapshot();
1263 539
        }
1264 539
    }
1265
}
1266