Completed
Push — master ( f2e43e...df01cf )
by Maciej
14:18
created

Query::__construct()   C

Complexity

Conditions 14
Paths 84

Size

Total Lines 44
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 31
CRAP Score 14

Importance

Changes 0
Metric Value
dl 0
loc 44
c 0
b 0
f 0
ccs 31
cts 31
cp 1
rs 5.0864
cc 14
eloc 31
nc 84
nop 9
crap 14

How to fix   Complexity    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
namespace Doctrine\ODM\MongoDB\Query;
4
5
use Doctrine\ODM\MongoDB\DocumentManager;
6
use Doctrine\ODM\MongoDB\Iterator\CachingIterator;
7
use Doctrine\ODM\MongoDB\Iterator\HydratingIterator;
8
use Doctrine\ODM\MongoDB\Iterator\Iterator;
9
use Doctrine\ODM\MongoDB\Iterator\PrimingIterator;
10
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
11
use MongoDB\Collection;
12
use MongoDB\Driver\Cursor;
13
use MongoDB\Operation\FindOneAndUpdate;
14
15
16
/**
17
 * ODM Query wraps the raw Doctrine MongoDB queries to add additional functionality
18
 * and to hydrate the raw arrays of data to Doctrine document objects.
19
 *
20
 * @since       1.0
21
 */
22
class Query implements \IteratorAggregate
23
{
24
    const TYPE_FIND            = 1;
25
    const TYPE_FIND_AND_UPDATE = 2;
26
    const TYPE_FIND_AND_REMOVE = 3;
27
    const TYPE_INSERT          = 4;
28
    const TYPE_UPDATE          = 5;
29
    const TYPE_REMOVE          = 6;
30
    const TYPE_GROUP           = 7;
31
    const TYPE_MAP_REDUCE      = 8;
32
    const TYPE_DISTINCT        = 9;
33
    const TYPE_COUNT           = 11;
34
35
    /**
36
     * @deprecated 1.1 Will be removed for 2.0
37
     */
38
    const TYPE_GEO_LOCATION = 10;
39
40
    const HINT_REFRESH = 1;
41
    // 2 was used for HINT_SLAVE_OKAY, which was removed in 2.0
42
    const HINT_READ_PREFERENCE = 3;
43
    const HINT_READ_ONLY = 5;
44
45
    /**
46
     * The DocumentManager instance.
47
     *
48
     * @var DocumentManager
49
     */
50
    private $dm;
51
52
    /**
53
     * The ClassMetadata instance.
54
     *
55
     * @var ClassMetadata
56
     */
57
    private $class;
58
59
    /**
60
     * Whether to hydrate results as document class instances.
61
     *
62
     * @var boolean
63
     */
64
    private $hydrate = true;
65
66
    /**
67
     * Array of primer Closure instances.
68
     *
69
     * @var array
70
     */
71
    private $primers = array();
72
73
    /**
74
     * Hints for UnitOfWork behavior.
75
     *
76
     * @var array
77
     */
78
    private $unitOfWorkHints = array();
79
80
    /**
81
     * The Collection instance.
82
     *
83
     * @var Collection
84
     */
85
    protected $collection;
86
87
    /**
88
     * Query structure generated by the Builder class.
89
     *
90
     * @var array
91
     */
92
    private $query;
93
94
    /**
95
     * @var Iterator
96
     */
97
    private $iterator;
98
99
    /**
100
     * Query options
101
     *
102
     * @var array
103
     */
104
    private $options;
105
106
    /**
107
     * Constructor.
108
     *
109
     * Please note that $requireIndexes was deprecated in 1.2 and will be removed in 2.0
110
     *
111
     * @param DocumentManager $dm
112
     * @param ClassMetadata $class
113
     * @param Collection $collection
114
     * @param array $query
115
     * @param array $options
116
     * @param boolean $hydrate
117
     * @param boolean $refresh
118
     * @param array $primers
119
     * @param boolean $readOnly
120
     */
121 159
    public function __construct(DocumentManager $dm, ClassMetadata $class, Collection $collection, array $query = array(), array $options = array(), $hydrate = true, $refresh = false, array $primers = array(), $readOnly = false)
122
    {
123 159
        $primers = array_filter($primers);
124
125 159
        if ( ! empty($primers)) {
126 22
            $query['eagerCursor'] = true;
127
        }
128
129 159
        if ( ! empty($query['eagerCursor'])) {
130 23
            $query['useIdentifierKeys'] = false;
131
        }
132
133 159
        switch ($query['type']) {
134 159
            case self::TYPE_FIND:
135 36
            case self::TYPE_FIND_AND_UPDATE:
136 24
            case self::TYPE_FIND_AND_REMOVE:
137 21
            case self::TYPE_INSERT:
138 20
            case self::TYPE_UPDATE:
139 8
            case self::TYPE_REMOVE:
140 6
            case self::TYPE_GROUP:
141 6
            case self::TYPE_MAP_REDUCE:
142 6
            case self::TYPE_DISTINCT:
143 4
            case self::TYPE_COUNT:
144 158
                break;
145
146
            default:
147 1
                throw new \InvalidArgumentException('Invalid query type: ' . $query['type']);
148
        }
149
150 158
        $this->collection = $collection;
151 158
        $this->query      = $query;
152 158
        $this->options    = $options;
153 158
        $this->dm = $dm;
154 158
        $this->class = $class;
155 158
        $this->hydrate = $hydrate;
156 158
        $this->primers = $primers;
157
158 158
        $this->setReadOnly($readOnly);
159 158
        $this->setRefresh($refresh);
160
161 158
        if (isset($query['readPreference'])) {
162 6
            $this->unitOfWorkHints[self::HINT_READ_PREFERENCE] = $query['readPreference'];
163
        }
164 158
    }
165
166 64
    public function __clone()
167
    {
168 64
        $this->iterator = null;
169 64
    }
170
171
    /**
172
     * Return an array of information about the query structure for debugging.
173
     *
174
     * The $name parameter may be used to return a specific key from the
175
     * internal $query array property. If omitted, the entire array will be
176
     * returned.
177
     *
178
     * @param string $name
179
     * @return mixed
180
     */
181 26
    public function debug($name = null)
182
    {
183 26
        return $name !== null ? $this->query[$name] : $this->query;
184
    }
185
186
    /**
187
     * Execute the query and returns the results.
188
     *
189
     * @throws \Doctrine\ODM\MongoDB\MongoDBException
190
     * @return Iterator|int|string|array
191
     */
192 119
    public function execute()
193
    {
194 119
        $results = $this->runQuery();
195
196 119
        if ( ! $this->hydrate) {
197 9
            return $results;
198
        }
199
200 113
        if ($results instanceof Cursor) {
0 ignored issues
show
Bug introduced by
The class MongoDB\Driver\Cursor does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
201
            $results = $this->makeIterator($results);
202
        }
203
204 113
        $uow = $this->dm->getUnitOfWork();
205
206
        /* If a single document is returned from a findAndModify command and it
207
         * includes the identifier field, attempt hydration.
208
         */
209 113
        if (($this->query['type'] === self::TYPE_FIND_AND_UPDATE ||
210 113
                $this->query['type'] === self::TYPE_FIND_AND_REMOVE) &&
211 113
            is_array($results) && isset($results['_id'])) {
212
213 5
            $results = $uow->getOrCreateDocument($this->class->name, $results, $this->unitOfWorkHints);
214
215 5
            if ( ! empty($this->primers)) {
216 1
                $referencePrimer = new ReferencePrimer($this->dm, $uow);
217
218 1
                foreach ($this->primers as $fieldName => $primer) {
219 1
                    $primer = is_callable($primer) ? $primer : null;
220 1
                    $referencePrimer->primeReferences($this->class, array($results), $fieldName, $this->unitOfWorkHints, $primer);
221
                }
222
            }
223
        }
224
225 113
        return $results;
226
    }
227
228
    /**
229
     * Gets the ClassMetadata instance.
230
     *
231
     * @return ClassMetadata $class
232
     */
233
    public function getClass()
234
    {
235
        return $this->class;
236
    }
237
238
    /**
239
     * Gets the DocumentManager instance.
240
     *
241
     * @return DocumentManager $dm
242
     */
243
    public function getDocumentManager()
244
    {
245
        return $this->dm;
246
    }
247
248
    /**
249
     * Execute the query and return its result, which must be an Iterator.
250
     *
251
     * If the query type is not expected to return an Iterator,
252
     * BadMethodCallException will be thrown before executing the query.
253
     * Otherwise, the query will be executed and UnexpectedValueException will
254
     * be thrown if {@link Query::execute()} does not return an Iterator.
255
     *
256
     * @see http://php.net/manual/en/iteratoraggregate.getiterator.php
257
     * @return Iterator
258
     * @throws \BadMethodCallException if the query type would not return an Iterator
259
     * @throws \UnexpectedValueException if the query did not return an Iterator
260
     */
261 84
    public function getIterator()
262
    {
263 84
        switch ($this->query['type']) {
264 84
            case self::TYPE_FIND:
265 6
            case self::TYPE_GROUP:
266 6
            case self::TYPE_MAP_REDUCE:
267 6
            case self::TYPE_DISTINCT:
268 78
                break;
269
270
            default:
271 6
                throw new \BadMethodCallException('Iterator would not be returned for query type: ' . $this->query['type']);
272
        }
273
274 78
        if ($this->iterator === null) {
275 78
            $this->iterator = $this->execute();
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->execute() can also be of type array or integer. However, the property $iterator is declared as type object<Doctrine\ODM\MongoDB\Iterator\Iterator>. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
276
        }
277
278 78
        return $this->iterator;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->iterator; (null|array|object|integer) is incompatible with the return type declared by the interface IteratorAggregate::getIterator of type Traversable.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
279
    }
280
281
    /**
282
     * Return the query structure.
283
     *
284
     * @return array
285
     */
286 14
    public function getQuery()
287
    {
288 14
        return $this->query;
289
    }
290
291
    /**
292
     * Execute the query and return the first result.
293
     *
294
     * @return array|object|null
295
     */
296 64
    public function getSingleResult()
297
    {
298 64
        $clonedQuery = clone $this;
299 64
        $clonedQuery->query['limit'] = 1;
300 64
        return $clonedQuery->getIterator()->current() ?: null;
301
    }
302
303
    /**
304
     * Return the query type.
305
     *
306
     * @return integer
307
     */
308
    public function getType()
309
    {
310
        return $this->query['type'];
311
    }
312
313
    /**
314
     * Sets whether or not to hydrate the documents to objects.
315
     *
316
     * @param boolean $hydrate
317
     */
318
    public function setHydrate($hydrate)
319
    {
320
        $this->hydrate = (boolean) $hydrate;
321
    }
322
323
    /**
324
     * Set whether documents should be registered in UnitOfWork. If document would
325
     * already be managed it will be left intact and new instance returned.
326
     *
327
     * This option has no effect if hydration is disabled.
328
     *
329
     * @param boolean $readOnly
330
     */
331 158
    public function setReadOnly($readOnly)
332
    {
333 158
        $this->unitOfWorkHints[Query::HINT_READ_ONLY] = (boolean) $readOnly;
334 158
    }
335
336
    /**
337
     * Set whether to refresh hydrated documents that are already in the
338
     * identity map.
339
     *
340
     * This option has no effect if hydration is disabled.
341
     *
342
     * @param boolean $refresh
343
     */
344 158
    public function setRefresh($refresh)
345
    {
346 158
        $this->unitOfWorkHints[Query::HINT_REFRESH] = (boolean) $refresh;
347 158
    }
348
349
    /**
350
     * Execute the query and return its results as an array.
351
     *
352
     * @see IteratorAggregate::toArray()
353
     * @return array
354
     */
355 11
    public function toArray()
356
    {
357 11
        return $this->getIterator()->toArray();
358
    }
359
360
    /**
361
     * Returns an array containing the specified keys and their values from the
362
     * query array, provided they exist and are not null.
363
     *
364
     * @param string $key,... One or more option keys to be read
0 ignored issues
show
Bug introduced by
There is no parameter named $key,.... Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
365
     * @return array
366
     */
367 118
    private function getQueryOptions(/* $key, ... */)
368
    {
369 118
        return array_filter(
370 118
            array_intersect_key($this->query, array_flip(func_get_args())),
371
            function($value) { return $value !== null; }
372
        );
373
    }
374
375 105
    private function makeIterator(Cursor $cursor): Iterator
376
    {
377 105
        if ($this->hydrate && $this->class) {
378 97
            $cursor = new HydratingIterator($cursor, $this->dm->getUnitOfWork(), $this->class, $this->unitOfWorkHints);
379
        }
380
381 105
        $cursor = new CachingIterator($cursor);
382
383 105
        if ( ! empty($this->primers)) {
384 20
            $referencePrimer = new ReferencePrimer($this->dm, $this->dm->getUnitOfWork());
385 20
            $cursor = new PrimingIterator($cursor, $this->class, $referencePrimer, $this->primers, $this->unitOfWorkHints);
386
        }
387
388 105
        return $cursor;
389
    }
390
391
    /**
392
     * Returns an array with its keys renamed based on the translation map.
393
     *
394
     * @param array $options Query options
395
     * @return array $rename Translation map (from => to) for renaming keys
396
     */
397 110
    private function renameQueryOptions(array $options, array $rename)
398
    {
399 110
        if (empty($options)) {
400 41
            return $options;
401
        }
402
403 86
        return array_combine(
404 86
            array_map(
405
                function($key) use ($rename) { return $rename[$key] ?? $key; },
406 86
                array_keys($options)
407
            ),
408 86
            array_values($options)
409
        );
410
    }
411
412
    /**
413
     * Execute the query and return its result.
414
     *
415
     * The return value will vary based on the query type. Commands with results
416
     * (e.g. aggregate, inline mapReduce) may return an ArrayIterator. Other
417
     * commands and operations may return a status array or a boolean, depending
418
     * on the driver's write concern. Queries and some mapReduce commands will
419
     * return an Iterator.
420
     *
421
     * @return Iterator|string|int|array
422
     */
423 119
    public function runQuery()
424
    {
425 119
        $options = $this->options;
426
427 119
        switch ($this->query['type']) {
428 119
            case self::TYPE_FIND:
429 105
                $queryOptions = $this->getQueryOptions('select', 'sort', 'skip', 'limit', 'readPreference');
430 105
                $queryOptions = $this->renameQueryOptions($queryOptions, ['select' => 'projection']);
431
432 105
                $cursor = $this->collection->find(
433 105
                    $this->query['query'],
434 105
                    $queryOptions
435
                );
436
437 105
                return $this->makeIterator($cursor);
438
439 22
            case self::TYPE_FIND_AND_UPDATE:
440 6
                $queryOptions = $this->getQueryOptions('select', 'sort', 'upsert');
441 6
                $queryOptions = $this->renameQueryOptions($queryOptions, ['select' => 'projection']);
442 6
                $queryOptions['returnDocument'] = ($this->query['new'] ?? false) ? FindOneAndUpdate::RETURN_DOCUMENT_AFTER : FindOneAndUpdate::RETURN_DOCUMENT_BEFORE;
443
444 6
                return $this->collection->findOneAndUpdate(
445 6
                    $this->query['query'],
446 6
                    $this->query['newObj'],
447 6
                    array_merge($options, $queryOptions)
448
                );
449
450 17
            case self::TYPE_FIND_AND_REMOVE:
451 2
                $queryOptions = $this->getQueryOptions('select', 'sort');
452 2
                $queryOptions = $this->renameQueryOptions($queryOptions, ['select' => 'projection']);
453
454 2
                return $this->collection->findOneAndDelete(
455 2
                    $this->query['query'],
456 2
                    array_merge($options, $queryOptions)
457
                );
458
459 15
            case self::TYPE_INSERT:
460
                return $this->collection->insertOne($this->query['newObj'], $options);
461
462 15
            case self::TYPE_UPDATE:
463 10
                if ($this->query['multiple'] ?? false) {
464 2
                    return $this->collection->updateMany(
465 2
                        $this->query['query'],
466 2
                        $this->query['newObj'],
467 2
                        array_merge($options, $this->getQueryOptions('upsert'))
468
                    );
469
                }
470
471 8
                return $this->collection->updateOne(
472 8
                    $this->query['query'],
473 8
                    $this->query['newObj'],
474 8
                    array_merge($options, $this->getQueryOptions('upsert'))
475
                );
476
477 5
            case self::TYPE_REMOVE:
478 1
                return $this->collection->deleteMany($this->query['query'], $options);
479
480 4 View Code Duplication
            case self::TYPE_DISTINCT:
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...
481 2
                $collection = $this->collection;
482 2
                $query = $this->query;
483
484 2
                return $collection->distinct(
485 2
                    $query['distinct'],
486 2
                    $query['query'],
487 2
                    array_merge($options, $this->getQueryOptions('readPreference'))
488
                );
489
490 2 View Code Duplication
            case self::TYPE_COUNT:
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...
491 2
                $collection = $this->collection;
492 2
                $query = $this->query;
493
494 2
                return $collection->count(
495 2
                    $query['query'],
496 2
                    array_merge($options, $this->getQueryOptions('hint', 'limit', 'skip', 'readPreference'))
497
                );
498
        }
499
    }
500
}
501