Completed
Push — master ( 08b9e1...e0c601 )
by Andreas
13s
created

Query   F

Complexity

Total Complexity 61

Size/Duplication

Total Lines 479
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 8

Test Coverage

Coverage 92.59%

Importance

Changes 0
Metric Value
wmc 61
lcom 1
cbo 8
dl 0
loc 479
ccs 150
cts 162
cp 0.9259
rs 3.52
c 0
b 0
f 0

18 Methods

Rating   Name   Duplication   Size   Complexity  
C __construct() 0 46 14
A __clone() 0 4 1
A debug() 0 4 2
B execute() 0 34 10
A getClass() 0 4 1
A getDocumentManager() 0 4 1
A getIterator() 0 19 6
A getQuery() 0 4 1
A getSingleResult() 0 6 2
A getType() 0 4 1
A setHydrate() 0 4 1
A setReadOnly() 0 4 1
A setRefresh() 0 4 1
A toArray() 0 4 1
A getQueryOptions() 0 9 1
A makeIterator() 0 15 4
A renameQueryOptions() 0 16 2
C runQuery() 0 77 11

How to fix   Complexity   

Complex Class

Complex classes like Query often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Query, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ODM\MongoDB\Query;
6
7
use Doctrine\ODM\MongoDB\DocumentManager;
8
use Doctrine\ODM\MongoDB\Iterator\CachingIterator;
9
use Doctrine\ODM\MongoDB\Iterator\HydratingIterator;
10
use Doctrine\ODM\MongoDB\Iterator\Iterator;
11
use Doctrine\ODM\MongoDB\Iterator\PrimingIterator;
12
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
13
use Doctrine\ODM\MongoDB\MongoDBException;
14
use MongoDB\Collection;
15
use MongoDB\Driver\Cursor;
16
use MongoDB\Operation\FindOneAndUpdate;
17
use function array_combine;
18
use function array_filter;
19
use function array_flip;
20
use function array_intersect_key;
21
use function array_keys;
22
use function array_map;
23
use function array_merge;
24
use function array_values;
25
use function is_array;
26
use function is_callable;
27
28
/**
29
 * ODM Query wraps the raw Doctrine MongoDB queries to add additional functionality
30
 * and to hydrate the raw arrays of data to Doctrine document objects.
31
 *
32
 */
33
class Query implements \IteratorAggregate
34
{
35
    public const TYPE_FIND            = 1;
36
    public const TYPE_FIND_AND_UPDATE = 2;
37
    public const TYPE_FIND_AND_REMOVE = 3;
38
    public const TYPE_INSERT          = 4;
39
    public const TYPE_UPDATE          = 5;
40
    public const TYPE_REMOVE          = 6;
41
    public const TYPE_GROUP           = 7;
42
    public const TYPE_MAP_REDUCE      = 8;
43
    public const TYPE_DISTINCT        = 9;
44
    public const TYPE_COUNT           = 11;
45
46
    /**
47
     * @deprecated 1.1 Will be removed for 2.0
48
     */
49
    public const TYPE_GEO_LOCATION = 10;
50
51
    public const HINT_REFRESH = 1;
52
    // 2 was used for HINT_SLAVE_OKAY, which was removed in 2.0
53
    public const HINT_READ_PREFERENCE = 3;
54
    public const HINT_READ_ONLY = 5;
55
56
    /**
57
     * The DocumentManager instance.
58
     *
59
     * @var DocumentManager
60
     */
61
    private $dm;
62
63
    /**
64
     * The ClassMetadata instance.
65
     *
66
     * @var ClassMetadata
67
     */
68
    private $class;
69
70
    /**
71
     * Whether to hydrate results as document class instances.
72
     *
73
     * @var bool
74
     */
75
    private $hydrate = true;
76
77
    /**
78
     * Array of primer Closure instances.
79
     *
80
     * @var array
81
     */
82
    private $primers = [];
83
84
    /**
85
     * Hints for UnitOfWork behavior.
86
     *
87
     * @var array
88
     */
89
    private $unitOfWorkHints = [];
90
91
    /**
92
     * The Collection instance.
93
     *
94
     * @var Collection
95
     */
96
    protected $collection;
97
98
    /**
99
     * Query structure generated by the Builder class.
100
     *
101
     * @var array
102
     */
103
    private $query;
104
105
    /** @var Iterator */
106
    private $iterator;
107
108
    /**
109
     * Query options
110
     *
111
     * @var array
112
     */
113
    private $options;
114
115
    /**
116
     *
117
     *
118
     * Please note that $requireIndexes was deprecated in 1.2 and will be removed in 2.0
119
     *
120
     * @param array $query
121
     * @param array $options
122
     * @param bool  $hydrate
123
     * @param bool  $refresh
124
     * @param array $primers
125
     * @param bool  $readOnly
126
     */
127 161
    public function __construct(DocumentManager $dm, ClassMetadata $class, Collection $collection, array $query = [], array $options = [], $hydrate = true, $refresh = false, array $primers = [], $readOnly = false)
128
    {
129 161
        $primers = array_filter($primers);
130
131 161
        if (! empty($primers)) {
132 22
            $query['eagerCursor'] = true;
133
        }
134
135 161
        if (! empty($query['eagerCursor'])) {
136 23
            $query['useIdentifierKeys'] = false;
137
        }
138
139 161
        switch ($query['type']) {
140 161
            case self::TYPE_FIND:
141 36
            case self::TYPE_FIND_AND_UPDATE:
142 24
            case self::TYPE_FIND_AND_REMOVE:
143 21
            case self::TYPE_INSERT:
144 20
            case self::TYPE_UPDATE:
145 8
            case self::TYPE_REMOVE:
146 6
            case self::TYPE_GROUP:
147 6
            case self::TYPE_MAP_REDUCE:
148 6
            case self::TYPE_DISTINCT:
149 4
            case self::TYPE_COUNT:
150 160
                break;
151
152
            default:
153 1
                throw new \InvalidArgumentException('Invalid query type: ' . $query['type']);
154
        }
155
156 160
        $this->collection = $collection;
157 160
        $this->query      = $query;
158 160
        $this->options    = $options;
159 160
        $this->dm = $dm;
160 160
        $this->class = $class;
161 160
        $this->hydrate = $hydrate;
162 160
        $this->primers = $primers;
163
164 160
        $this->setReadOnly($readOnly);
165 160
        $this->setRefresh($refresh);
166
167 160
        if (! isset($query['readPreference'])) {
168 154
            return;
169
        }
170
171 6
        $this->unitOfWorkHints[self::HINT_READ_PREFERENCE] = $query['readPreference'];
172 6
    }
173
174 64
    public function __clone()
175
    {
176 64
        $this->iterator = null;
177 64
    }
178
179
    /**
180
     * Return an array of information about the query structure for debugging.
181
     *
182
     * The $name parameter may be used to return a specific key from the
183
     * internal $query array property. If omitted, the entire array will be
184
     * returned.
185
     *
186
     * @param string $name
187
     * @return mixed
188
     */
189 27
    public function debug($name = null)
190
    {
191 27
        return $name !== null ? $this->query[$name] : $this->query;
192
    }
193
194
    /**
195
     * Execute the query and returns the results.
196
     *
197
     * @throws MongoDBException
198
     * @return Iterator|int|string|array
199
     */
200 120
    public function execute()
201
    {
202 120
        $results = $this->runQuery();
203
204 120
        if (! $this->hydrate) {
205 9
            return $results;
206
        }
207
208 114
        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...
209
            $results = $this->makeIterator($results);
210
        }
211
212 114
        $uow = $this->dm->getUnitOfWork();
213
214
        /* If a single document is returned from a findAndModify command and it
215
         * includes the identifier field, attempt hydration.
216
         */
217 114
        if (($this->query['type'] === self::TYPE_FIND_AND_UPDATE ||
218 114
                $this->query['type'] === self::TYPE_FIND_AND_REMOVE) &&
219 114
            is_array($results) && isset($results['_id'])) {
220 5
            $results = $uow->getOrCreateDocument($this->class->name, $results, $this->unitOfWorkHints);
221
222 5
            if (! empty($this->primers)) {
223 1
                $referencePrimer = new ReferencePrimer($this->dm, $uow);
224
225 1
                foreach ($this->primers as $fieldName => $primer) {
226 1
                    $primer = is_callable($primer) ? $primer : null;
227 1
                    $referencePrimer->primeReferences($this->class, [$results], $fieldName, $this->unitOfWorkHints, $primer);
228
                }
229
            }
230
        }
231
232 114
        return $results;
233
    }
234
235
    /**
236
     * Gets the ClassMetadata instance.
237
     *
238
     * @return ClassMetadata $class
239
     */
240
    public function getClass()
241
    {
242
        return $this->class;
243
    }
244
245
    /**
246
     * Gets the DocumentManager instance.
247
     *
248
     * @return DocumentManager $dm
249
     */
250
    public function getDocumentManager()
251
    {
252
        return $this->dm;
253
    }
254
255
    /**
256
     * Execute the query and return its result, which must be an Iterator.
257
     *
258
     * If the query type is not expected to return an Iterator,
259
     * BadMethodCallException will be thrown before executing the query.
260
     * Otherwise, the query will be executed and UnexpectedValueException will
261
     * be thrown if {@link Query::execute()} does not return an Iterator.
262
     *
263
     * @see http://php.net/manual/en/iteratoraggregate.getiterator.php
264
     * @return Iterator
265
     * @throws \BadMethodCallException If the query type would not return an Iterator.
266
     * @throws \UnexpectedValueException If the query did not return an Iterator.
267
     */
268 84
    public function getIterator()
269
    {
270 84
        switch ($this->query['type']) {
271 84
            case self::TYPE_FIND:
272 6
            case self::TYPE_GROUP:
273 6
            case self::TYPE_MAP_REDUCE:
274 6
            case self::TYPE_DISTINCT:
275 78
                break;
276
277
            default:
278 6
                throw new \BadMethodCallException('Iterator would not be returned for query type: ' . $this->query['type']);
279
        }
280
281 78
        if ($this->iterator === null) {
282 78
            $this->iterator = $this->execute();
283
        }
284
285 78
        return $this->iterator;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->iterator; (string|integer|array|object) 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...
286
    }
287
288
    /**
289
     * Return the query structure.
290
     *
291
     * @return array
292
     */
293 14
    public function getQuery()
294
    {
295 14
        return $this->query;
296
    }
297
298
    /**
299
     * Execute the query and return the first result.
300
     *
301
     * @return array|object|null
302
     */
303 64
    public function getSingleResult()
304
    {
305 64
        $clonedQuery = clone $this;
306 64
        $clonedQuery->query['limit'] = 1;
307 64
        return $clonedQuery->getIterator()->current() ?: null;
308
    }
309
310
    /**
311
     * Return the query type.
312
     *
313
     * @return int
314
     */
315
    public function getType()
316
    {
317
        return $this->query['type'];
318
    }
319
320
    /**
321
     * Sets whether or not to hydrate the documents to objects.
322
     *
323
     * @param bool $hydrate
324
     */
325
    public function setHydrate($hydrate)
326
    {
327
        $this->hydrate = (bool) $hydrate;
328
    }
329
330
    /**
331
     * Set whether documents should be registered in UnitOfWork. If document would
332
     * already be managed it will be left intact and new instance returned.
333
     *
334
     * This option has no effect if hydration is disabled.
335
     *
336
     * @param bool $readOnly
337
     */
338 160
    public function setReadOnly($readOnly)
339
    {
340 160
        $this->unitOfWorkHints[self::HINT_READ_ONLY] = (bool) $readOnly;
341 160
    }
342
343
    /**
344
     * Set whether to refresh hydrated documents that are already in the
345
     * identity map.
346
     *
347
     * This option has no effect if hydration is disabled.
348
     *
349
     * @param bool $refresh
350
     */
351 160
    public function setRefresh($refresh)
352
    {
353 160
        $this->unitOfWorkHints[self::HINT_REFRESH] = (bool) $refresh;
354 160
    }
355
356
    /**
357
     * Execute the query and return its results as an array.
358
     *
359
     * @see IteratorAggregate::toArray()
360
     * @return array
361
     */
362 11
    public function toArray()
363
    {
364 11
        return $this->getIterator()->toArray();
365
    }
366
367
    /**
368
     * Returns an array containing the specified keys and their values from the
369
     * query array, provided they exist and are not null.
370
     *
371
     * @param string ...$keys One or more option keys to be read
372
     * @return array
373
     */
374 119
    private function getQueryOptions(string ...$keys)
375
    {
376 119
        return array_filter(
377 119
            array_intersect_key($this->query, array_flip($keys)),
378
            function ($value) {
379 88
                return $value !== null;
380 119
            }
381
        );
382
    }
383
384 106
    private function makeIterator(Cursor $cursor): Iterator
385
    {
386 106
        if ($this->hydrate && $this->class) {
387 98
            $cursor = new HydratingIterator($cursor, $this->dm->getUnitOfWork(), $this->class, $this->unitOfWorkHints);
388
        }
389
390 106
        $cursor = new CachingIterator($cursor);
391
392 106
        if (! empty($this->primers)) {
393 20
            $referencePrimer = new ReferencePrimer($this->dm, $this->dm->getUnitOfWork());
394 20
            $cursor = new PrimingIterator($cursor, $this->class, $referencePrimer, $this->primers, $this->unitOfWorkHints);
395
        }
396
397 106
        return $cursor;
398
    }
399
400
    /**
401
     * Returns an array with its keys renamed based on the translation map.
402
     *
403
     * @param array $options Query options
404
     * @return array $rename Translation map (from => to) for renaming keys
405
     */
406 111
    private function renameQueryOptions(array $options, array $rename)
407
    {
408 111
        if (empty($options)) {
409 42
            return $options;
410
        }
411
412 86
        return array_combine(
413 86
            array_map(
414
                function ($key) use ($rename) {
415 86
                    return $rename[$key] ?? $key;
416 86
                },
417 86
                array_keys($options)
418
            ),
419 86
            array_values($options)
420
        );
421
    }
422
423
    /**
424
     * Execute the query and return its result.
425
     *
426
     * The return value will vary based on the query type. Commands with results
427
     * (e.g. aggregate, inline mapReduce) may return an ArrayIterator. Other
428
     * commands and operations may return a status array or a boolean, depending
429
     * on the driver's write concern. Queries and some mapReduce commands will
430
     * return an Iterator.
431
     *
432
     * @return Iterator|string|int|array
433
     */
434 120
    public function runQuery()
435
    {
436 120
        $options = $this->options;
437
438 120
        switch ($this->query['type']) {
439 120
            case self::TYPE_FIND:
440 106
                $queryOptions = $this->getQueryOptions('select', 'sort', 'skip', 'limit', 'readPreference');
441 106
                $queryOptions = $this->renameQueryOptions($queryOptions, ['select' => 'projection']);
442
443 106
                $cursor = $this->collection->find(
444 106
                    $this->query['query'],
445 106
                    $queryOptions
446
                );
447
448 106
                return $this->makeIterator($cursor);
449
450 22
            case self::TYPE_FIND_AND_UPDATE:
451 6
                $queryOptions = $this->getQueryOptions('select', 'sort', 'upsert');
452 6
                $queryOptions = $this->renameQueryOptions($queryOptions, ['select' => 'projection']);
453 6
                $queryOptions['returnDocument'] = ($this->query['new'] ?? false) ? FindOneAndUpdate::RETURN_DOCUMENT_AFTER : FindOneAndUpdate::RETURN_DOCUMENT_BEFORE;
454
455 6
                return $this->collection->findOneAndUpdate(
456 6
                    $this->query['query'],
457 6
                    $this->query['newObj'],
458 6
                    array_merge($options, $queryOptions)
459
                );
460
461 17
            case self::TYPE_FIND_AND_REMOVE:
462 2
                $queryOptions = $this->getQueryOptions('select', 'sort');
463 2
                $queryOptions = $this->renameQueryOptions($queryOptions, ['select' => 'projection']);
464
465 2
                return $this->collection->findOneAndDelete(
466 2
                    $this->query['query'],
467 2
                    array_merge($options, $queryOptions)
468
                );
469
470 15
            case self::TYPE_INSERT:
471
                return $this->collection->insertOne($this->query['newObj'], $options);
472
473 15
            case self::TYPE_UPDATE:
474 10
                if ($this->query['multiple'] ?? false) {
475 2
                    return $this->collection->updateMany(
476 2
                        $this->query['query'],
477 2
                        $this->query['newObj'],
478 2
                        array_merge($options, $this->getQueryOptions('upsert'))
479
                    );
480
                }
481
482 8
                return $this->collection->updateOne(
483 8
                    $this->query['query'],
484 8
                    $this->query['newObj'],
485 8
                    array_merge($options, $this->getQueryOptions('upsert'))
486
                );
487
488 5
            case self::TYPE_REMOVE:
489 1
                return $this->collection->deleteMany($this->query['query'], $options);
490
491 4
            case self::TYPE_DISTINCT:
492 2
                $collection = $this->collection;
493 2
                $query = $this->query;
494
495 2
                return $collection->distinct(
496 2
                    $query['distinct'],
497 2
                    $query['query'],
498 2
                    array_merge($options, $this->getQueryOptions('readPreference'))
499
                );
500
501 2
            case self::TYPE_COUNT:
502 2
                $collection = $this->collection;
503 2
                $query = $this->query;
504
505 2
                return $collection->count(
506 2
                    $query['query'],
507 2
                    array_merge($options, $this->getQueryOptions('hint', 'limit', 'skip', 'readPreference'))
508
                );
509
        }
510
    }
511
}
512