Completed
Pull Request — master (#2116)
by
unknown
14:25 queued 44s
created

Query::__construct()   B

Complexity

Conditions 10
Paths 17

Size

Total Lines 37

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 27
CRAP Score 10

Importance

Changes 0
Metric Value
dl 0
loc 37
ccs 27
cts 27
cp 1
rs 7.6666
c 0
b 0
f 0
cc 10
nc 17
nop 10
crap 10

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