Completed
Pull Request — master (#1803)
by Andreas
21:59
created

Query   D

Complexity

Total Complexity 59

Size/Duplication

Total Lines 440
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Test Coverage

Coverage 92.41%

Importance

Changes 0
Metric Value
wmc 59
lcom 1
cbo 7
dl 0
loc 440
ccs 146
cts 158
cp 0.9241
rs 4.08
c 0
b 0
f 0

18 Methods

Rating   Name   Duplication   Size   Complexity  
C __construct() 0 38 12
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
B getIterator() 0 23 7
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 3
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 BadMethodCallException;
8
use Doctrine\ODM\MongoDB\DocumentManager;
9
use Doctrine\ODM\MongoDB\Iterator\CachingIterator;
10
use Doctrine\ODM\MongoDB\Iterator\HydratingIterator;
11
use Doctrine\ODM\MongoDB\Iterator\Iterator;
12
use Doctrine\ODM\MongoDB\Iterator\PrimingIterator;
13
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
14
use Doctrine\ODM\MongoDB\MongoDBException;
15
use InvalidArgumentException;
16
use IteratorAggregate;
17
use MongoDB\Collection;
18
use MongoDB\Driver\Cursor;
19
use MongoDB\Operation\FindOneAndUpdate;
20
use UnexpectedValueException;
21
use function array_combine;
22
use function array_filter;
23
use function array_flip;
24
use function array_intersect_key;
25
use function array_keys;
26
use function array_map;
27
use function array_merge;
28
use function array_values;
29
use function is_array;
30
use function is_callable;
31
32
/**
33
 * ODM Query wraps the raw Doctrine MongoDB queries to add additional functionality
34
 * and to hydrate the raw arrays of data to Doctrine document objects.
35
 */
36
class Query implements IteratorAggregate
37
{
38
    public const TYPE_FIND            = 1;
39
    public const TYPE_FIND_AND_UPDATE = 2;
40
    public const TYPE_FIND_AND_REMOVE = 3;
41
    public const TYPE_INSERT          = 4;
42
    public const TYPE_UPDATE          = 5;
43
    public const TYPE_REMOVE          = 6;
44
    public const TYPE_GROUP           = 7;
45
    public const TYPE_MAP_REDUCE      = 8;
46
    public const TYPE_DISTINCT        = 9;
47
    public const TYPE_COUNT           = 11;
48
49
    /**
50
     * @deprecated 1.1 Will be removed for 2.0
51
     */
52
    public const TYPE_GEO_LOCATION = 10;
53
54
    public const HINT_REFRESH = 1;
55
    // 2 was used for HINT_SLAVE_OKAY, which was removed in 2.0
56
    public const HINT_READ_PREFERENCE = 3;
57
    public const HINT_READ_ONLY       = 5;
58
59
    /**
60
     * The DocumentManager instance.
61
     *
62
     * @var DocumentManager
63
     */
64
    private $dm;
65
66
    /**
67
     * The ClassMetadata instance.
68
     *
69
     * @var ClassMetadata
70
     */
71
    private $class;
72
73
    /**
74
     * Whether to hydrate results as document class instances.
75
     *
76
     * @var bool
77
     */
78
    private $hydrate = true;
79
80
    /**
81
     * Array of primer Closure instances.
82
     *
83
     * @var array
84
     */
85
    private $primers = [];
86
87
    /**
88
     * Hints for UnitOfWork behavior.
89
     *
90
     * @var array
91
     */
92
    private $unitOfWorkHints = [];
93
94
    /**
95
     * The Collection instance.
96
     *
97
     * @var Collection
98
     */
99
    protected $collection;
100
101
    /**
102
     * Query structure generated by the Builder class.
103
     *
104
     * @var array
105
     */
106
    private $query;
107
108
    /** @var Iterator|null */
109
    private $iterator;
110
111
    /**
112
     * Query options
113
     *
114
     * @var array
115
     */
116
    private $options;
117
118 162
    public function __construct(DocumentManager $dm, ClassMetadata $class, Collection $collection, array $query = [], array $options = [], bool $hydrate = true, bool $refresh = false, array $primers = [], bool $readOnly = false)
119
    {
120 162
        $primers = array_filter($primers);
121
122 162
        switch ($query['type']) {
123 162
            case self::TYPE_FIND:
124 38
            case self::TYPE_FIND_AND_UPDATE:
125 26
            case self::TYPE_FIND_AND_REMOVE:
126 23
            case self::TYPE_INSERT:
127 22
            case self::TYPE_UPDATE:
128 8
            case self::TYPE_REMOVE:
129 6
            case self::TYPE_GROUP:
130 6
            case self::TYPE_MAP_REDUCE:
131 6
            case self::TYPE_DISTINCT:
132 4
            case self::TYPE_COUNT:
133 161
                break;
134
135
            default:
136 1
                throw new InvalidArgumentException('Invalid query type: ' . $query['type']);
137
        }
138
139 161
        $this->collection = $collection;
140 161
        $this->query      = $query;
141 161
        $this->options    = $options;
142 161
        $this->dm         = $dm;
143 161
        $this->class      = $class;
144 161
        $this->hydrate    = $hydrate;
145 161
        $this->primers    = $primers;
146
147 161
        $this->setReadOnly($readOnly);
148 161
        $this->setRefresh($refresh);
149
150 161
        if (! isset($query['readPreference'])) {
151 155
            return;
152
        }
153
154 6
        $this->unitOfWorkHints[self::HINT_READ_PREFERENCE] = $query['readPreference'];
155 6
    }
156
157 64
    public function __clone()
158
    {
159 64
        $this->iterator = null;
160 64
    }
161
162
    /**
163
     * Return an array of information about the query structure for debugging.
164
     *
165
     * The $name parameter may be used to return a specific key from the
166
     * internal $query array property. If omitted, the entire array will be
167
     * returned.
168
     */
169 27
    public function debug(?string $name = null)
170
    {
171 27
        return $name !== null ? $this->query[$name] : $this->query;
172
    }
173
174
    /**
175
     * Execute the query and returns the results.
176
     *
177
     * @return Iterator|int|string|array|object
178
     *
179
     * @throws MongoDBException
180
     */
181 122
    public function execute()
182
    {
183 122
        $results = $this->runQuery();
184
185 122
        if (! $this->hydrate) {
186 9
            return $results;
187
        }
188
189 116
        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...
190
            $results = $this->makeIterator($results);
191
        }
192
193 116
        $uow = $this->dm->getUnitOfWork();
194
195
        /* If a single document is returned from a findAndModify command and it
196
         * includes the identifier field, attempt hydration.
197
         */
198 116
        if (($this->query['type'] === self::TYPE_FIND_AND_UPDATE ||
199 116
                $this->query['type'] === self::TYPE_FIND_AND_REMOVE) &&
200 116
            is_array($results) && isset($results['_id'])) {
201 5
            $results = $uow->getOrCreateDocument($this->class->name, $results, $this->unitOfWorkHints);
202
203 5
            if (! empty($this->primers)) {
204 1
                $referencePrimer = new ReferencePrimer($this->dm, $uow);
205
206 1
                foreach ($this->primers as $fieldName => $primer) {
207 1
                    $primer = is_callable($primer) ? $primer : null;
208 1
                    $referencePrimer->primeReferences($this->class, [$results], $fieldName, $this->unitOfWorkHints, $primer);
209
                }
210
            }
211
        }
212
213 116
        return $results;
214
    }
215
216
    /**
217
     * Gets the ClassMetadata instance.
218
     */
219
    public function getClass() : ClassMetadata
220
    {
221
        return $this->class;
222
    }
223
224
    public function getDocumentManager() : DocumentManager
225
    {
226
        return $this->dm;
227
    }
228
229
    /**
230
     * Execute the query and return its result, which must be an Iterator.
231
     *
232
     * If the query type is not expected to return an Iterator,
233
     * BadMethodCallException will be thrown before executing the query.
234
     * Otherwise, the query will be executed and UnexpectedValueException will
235
     * be thrown if {@link Query::execute()} does not return an Iterator.
236
     *
237
     * @see http://php.net/manual/en/iteratoraggregate.getiterator.php
238
     *
239
     * @throws BadMethodCallException If the query type would not return an Iterator.
240
     * @throws UnexpectedValueException If the query did not return an Iterator.
241
     * @throws MongoDBException
242
     */
243 83
    public function getIterator() : Iterator
244
    {
245 83
        switch ($this->query['type']) {
246 83
            case self::TYPE_FIND:
247 6
            case self::TYPE_GROUP:
248 6
            case self::TYPE_MAP_REDUCE:
249 6
            case self::TYPE_DISTINCT:
250 77
                break;
251
252
            default:
253 6
                throw new BadMethodCallException('Iterator would not be returned for query type: ' . $this->query['type']);
254
        }
255
256 77
        if ($this->iterator === null) {
257 77
            $result = $this->execute();
258
            if (! $result instanceof Iterator) {
259
                throw new \UnexpectedValueException('Iterator was not returned for query type: ' . $this->query['type']);
260 77
            }
261
            $this->iterator = $result;
262
        }
263
264
        return $this->iterator;
265
    }
266 13
267
    /**
268 13
     * Return the query structure.
269
     */
270
    public function getQuery() : array
271
    {
272
        return $this->query;
273
    }
274
275
    /**
276 64
     * Execute the query and return the first result.
277
     *
278 64
     * @return array|object|null
279 64
     */
280 64
    public function getSingleResult()
281
    {
282
        $clonedQuery                 = clone $this;
283
        $clonedQuery->query['limit'] = 1;
284
        return $clonedQuery->getIterator()->current() ?: null;
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Traversable as the method current() does only exist in the following implementations of said interface: APCUIterator, AppendIterator, ArrayIterator, CachingIterator, CallbackFilterIterator, DirectoryIterator, Doctrine\ODM\MongoDB\Iterator\CachingIterator, Doctrine\ODM\MongoDB\Iterator\HydratingIterator, Doctrine\ODM\MongoDB\Iterator\PrimingIterator, Doctrine\ODM\MongoDB\Tools\Console\MetadataFilter, EmptyIterator, FilesystemIterator, FilterIterator, Generator, GlobIterator, HttpMessage, HttpRequestPool, Imagick, ImagickPixelIterator, InfiniteIterator, IteratorIterator, LimitIterator, MongoCommandCursor, MongoCursor, MongoGridFSCursor, MultipleIterator, NoRewindIterator, ParentIterator, Phar, PharData, RecursiveArrayIterator, RecursiveCachingIterator, RecursiveCallbackFilterIterator, RecursiveDirectoryIterator, RecursiveFilterIterator, RecursiveIteratorIterator, RecursiveRegexIterator, RecursiveTreeIterator, RegexIterator, SQLiteResult, SimpleXMLIterator, SplDoublyLinkedList, SplFileObject, SplFixedArray, SplHeap, SplMaxHeap, SplMinHeap, SplObjectStorage, SplPriorityQueue, SplQueue, SplStack, SplTempFileObject.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
285
    }
286
287
    /**
288
     * Return the query type.
289
     */
290
    public function getType() : int
291
    {
292
        return $this->query['type'];
293
    }
294
295
    /**
296
     * Sets whether or not to hydrate the documents to objects.
297
     */
298
    public function setHydrate(bool $hydrate) : void
299
    {
300
        $this->hydrate = $hydrate;
301
    }
302
303
    /**
304
     * Set whether documents should be registered in UnitOfWork. If document would
305 161
     * already be managed it will be left intact and new instance returned.
306
     *
307 161
     * This option has no effect if hydration is disabled.
308 161
     */
309
    public function setReadOnly(bool $readOnly) : void
310
    {
311
        $this->unitOfWorkHints[self::HINT_READ_ONLY] = $readOnly;
312
    }
313
314
    /**
315
     * Set whether to refresh hydrated documents that are already in the
316 161
     * identity map.
317
     *
318 161
     * This option has no effect if hydration is disabled.
319 161
     */
320
    public function setRefresh(bool $refresh) : void
321
    {
322
        $this->unitOfWorkHints[self::HINT_REFRESH] = $refresh;
323
    }
324
325
    /**
326 11
     * Execute the query and return its results as an array.
327
     *
328 11
     * @see IteratorAggregate::toArray()
329
     */
330
    public function toArray() : array
331
    {
332
        return $this->getIterator()->toArray();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Traversable as the method toArray() does only exist in the following implementations of said interface: Doctrine\ODM\MongoDB\Iterator\CachingIterator, Doctrine\ODM\MongoDB\Iterator\PrimingIterator, Doctrine\ODM\MongoDB\Query\Query, SplFixedArray.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
333
    }
334
335 121
    /**
336
     * Returns an array containing the specified keys and their values from the
337 121
     * query array, provided they exist and are not null.
338 121
     */
339
    private function getQueryOptions(string ...$keys) : array
340 88
    {
341 121
        return array_filter(
342
            array_intersect_key($this->query, array_flip($keys)),
343
            static function ($value) {
344
                return $value !== null;
345 106
            }
346
        );
347 106
    }
348 98
349
    private function makeIterator(Cursor $cursor) : Iterator
350
    {
351 106
        if ($this->hydrate) {
352
            $cursor = new HydratingIterator($cursor, $this->dm->getUnitOfWork(), $this->class, $this->unitOfWorkHints);
353 106
        }
354 20
355 20
        $cursor = new CachingIterator($cursor);
356
357
        if (! empty($this->primers)) {
358 106
            $referencePrimer = new ReferencePrimer($this->dm, $this->dm->getUnitOfWork());
359
            $cursor          = new PrimingIterator($cursor, $this->class, $referencePrimer, $this->primers, $this->unitOfWorkHints);
360
        }
361
362
        return $cursor;
363
    }
364
365
    /**
366 111
     * Returns an array with its keys renamed based on the translation map.
367
     *
368 111
     * @return array $rename Translation map (from => to) for renaming keys
369 42
     */
370
    private function renameQueryOptions(array $options, array $rename) : array
371
    {
372 86
        if (empty($options)) {
373 86
            return $options;
374
        }
375 86
376 86
        return array_combine(
377 86
            array_map(
378
                static function ($key) use ($rename) {
379 86
                    return $rename[$key] ?? $key;
380
                },
381
                array_keys($options)
382
            ),
383
            array_values($options)
384
        );
385
    }
386
387
    /**
388
     * Execute the query and return its result.
389
     *
390
     * The return value will vary based on the query type. Commands with results
391
     * (e.g. aggregate, inline mapReduce) may return an ArrayIterator. Other
392
     * commands and operations may return a status array or a boolean, depending
393
     * on the driver's write concern. Queries and some mapReduce commands will
394 122
     * return an Iterator.
395
     *
396 122
     * @return Iterator|string|int|array|object|null
397
     */
398 122
    public function runQuery()
399 122
    {
400 106
        $options = $this->options;
401 106
402
        switch ($this->query['type']) {
403 106
            case self::TYPE_FIND:
404 106
                $queryOptions = $this->getQueryOptions('select', 'sort', 'skip', 'limit', 'readPreference');
405 106
                $queryOptions = $this->renameQueryOptions($queryOptions, ['select' => 'projection']);
406
407
                $cursor = $this->collection->find(
408 106
                    $this->query['query'],
409
                    $queryOptions
410 24
                );
411 6
412 6
                return $this->makeIterator($cursor);
413 6
414
            case self::TYPE_FIND_AND_UPDATE:
415 6
                $queryOptions                   = $this->getQueryOptions('select', 'sort', 'upsert');
416 6
                $queryOptions                   = $this->renameQueryOptions($queryOptions, ['select' => 'projection']);
417 6
                $queryOptions['returnDocument'] = $this->query['new'] ?? false ? FindOneAndUpdate::RETURN_DOCUMENT_AFTER : FindOneAndUpdate::RETURN_DOCUMENT_BEFORE;
418 6
419
                return $this->collection->findOneAndUpdate(
420
                    $this->query['query'],
421 19
                    $this->query['newObj'],
422 2
                    array_merge($options, $queryOptions)
423 2
                );
424
425 2
            case self::TYPE_FIND_AND_REMOVE:
426 2
                $queryOptions = $this->getQueryOptions('select', 'sort');
427 2
                $queryOptions = $this->renameQueryOptions($queryOptions, ['select' => 'projection']);
428
429
                return $this->collection->findOneAndDelete(
430 17
                    $this->query['query'],
431
                    array_merge($options, $queryOptions)
432
                );
433 17
434 12
            case self::TYPE_INSERT:
435 2
                return $this->collection->insertOne($this->query['newObj'], $options);
436 2
437 2
            case self::TYPE_UPDATE:
438 2
                if ($this->query['multiple'] ?? false) {
439
                    return $this->collection->updateMany(
440
                        $this->query['query'],
441
                        $this->query['newObj'],
442 10
                        array_merge($options, $this->getQueryOptions('upsert'))
443 10
                    );
444 10
                }
445 10
446
                return $this->collection->updateOne(
447
                    $this->query['query'],
448 5
                    $this->query['newObj'],
449 1
                    array_merge($options, $this->getQueryOptions('upsert'))
450
                );
451 4
452 2
            case self::TYPE_REMOVE:
453 2
                return $this->collection->deleteMany($this->query['query'], $options);
454
455 2
            case self::TYPE_DISTINCT:
456 2
                $collection = $this->collection;
457 2
                $query      = $this->query;
458 2
459
                return $collection->distinct(
460
                    $query['distinct'],
461 2
                    $query['query'],
462 2
                    array_merge($options, $this->getQueryOptions('readPreference'))
463 2
                );
464
465 2
            case self::TYPE_COUNT:
466 2
                $collection = $this->collection;
467 2
                $query      = $this->query;
468
469
                return $collection->count(
470
                    $query['query'],
471
                    array_merge($options, $this->getQueryOptions('hint', 'limit', 'skip', 'readPreference'))
472
                );
473
        }
474
    }
475
}
476