Completed
Pull Request — master (#1956)
by Jeremy
22:51
created

Query   C

Complexity

Total Complexity 54

Size/Duplication

Total Lines 433
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Test Coverage

Coverage 92.99%

Importance

Changes 0
Metric Value
wmc 54
lcom 1
cbo 7
dl 0
loc 433
ccs 146
cts 157
cp 0.9299
rs 6.4799
c 0
b 0
f 0

18 Methods

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