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