Completed
Pull Request — master (#2116)
by
unknown
24:13
created

Query::__construct()   B

Complexity

Conditions 10
Paths 17

Size

Total Lines 37

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 25
CRAP Score 10

Importance

Changes 0
Metric Value
dl 0
loc 37
ccs 25
cts 25
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 164
     * Query options
119
     *
120 164
     * @var array
121
     */
122 164
    private $options;
123 164
124 38
    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 26
    {
126 23
        $primers = array_filter($primers);
127 22
128 7
        switch ($query['type']) {
129 5
            case self::TYPE_FIND:
130 3
            case self::TYPE_FIND_AND_UPDATE:
131 163
            case self::TYPE_FIND_AND_REMOVE:
132
            case self::TYPE_INSERT:
133
            case self::TYPE_UPDATE:
134 1
            case self::TYPE_REMOVE:
135
            case self::TYPE_DISTINCT:
136
            case self::TYPE_COUNT:
137 163
                break;
138 163
139 163
            default:
140 163
                throw new InvalidArgumentException('Invalid query type: ' . $query['type']);
141 163
        }
142 163
143 163
        $this->collection = $collection;
144
        $this->query      = $query;
145 163
        $this->options    = $options;
146 163
        $this->dm         = $dm;
147
        $this->class      = $class;
148 163
        $this->hydrate    = $hydrate;
149 155
        $this->primers    = $primers;
150
151
        $this->setReadOnly($readOnly);
152 8
        $this->setRefresh($refresh);
153 8
        $this->setRewindable($rewindable);
154
155 64
        if (! isset($query['readPreference'])) {
156
            return;
157 64
        }
158 64
159
        $this->unitOfWorkHints[self::HINT_READ_PREFERENCE] = $query['readPreference'];
160
    }
161
162
    public function __clone()
163
    {
164
        $this->iterator = null;
165
    }
166
167 27
    /**
168
     * Return an array of information about the query structure for debugging.
169 27
     *
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
    public function debug(?string $name = null)
175
    {
176
        return $name !== null ? $this->query[$name] : $this->query;
177
    }
178
179 123
    /**
180
     * Execute the query and returns the results.
181 123
     *
182
     * @return Iterator|UpdateResult|InsertOneResult|DeleteResult|array|object|int|null
183 122
     *
184 9
     * @throws MongoDBException
185
     */
186
    public function execute()
187 116
    {
188
        $results = $this->runQuery();
189
190
        if (! $this->hydrate) {
191
            return $results;
192 116
        }
193 116
194 116
        $uow = $this->dm->getUnitOfWork();
195 5
196
        /* If a single document is returned from a findAndModify command and it
197 5
         * includes the identifier field, attempt hydration.
198 1
         */
199
        if (($this->query['type'] === self::TYPE_FIND_AND_UPDATE ||
200 1
                $this->query['type'] === self::TYPE_FIND_AND_REMOVE) &&
201 1
            is_array($results) && isset($results['_id'])) {
202 1
            $results = $uow->getOrCreateDocument($this->class->name, $results, $this->unitOfWorkHints);
203
204
            if (! empty($this->primers)) {
205
                $referencePrimer = new ReferencePrimer($this->dm, $uow);
206
207 116
                foreach ($this->primers as $fieldName => $primer) {
208
                    $primer = is_callable($primer) ? $primer : null;
209
                    $referencePrimer->primeReferences($this->class, [$results], $fieldName, $this->unitOfWorkHints, $primer);
210
                }
211
            }
212
        }
213
214
        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 83
     *
238
     * @see http://php.net/manual/en/iteratoraggregate.getiterator.php
239 83
     *
240 83
     * @throws BadMethodCallException If the query type would not return an Iterator.
241 6
     * @throws UnexpectedValueException If the query did not return an Iterator.
242 77
     * @throws MongoDBException
243
     */
244
    public function getIterator() : Iterator
245 6
    {
246
        switch ($this->query['type']) {
247
            case self::TYPE_FIND:
248 77
            case self::TYPE_DISTINCT:
249 77
                break;
250 77
251
            default:
252
                throw new BadMethodCallException('Iterator would not be returned for query type: ' . $this->query['type']);
253 77
        }
254
255
        if ($this->iterator === null) {
256 77
            $result = $this->execute();
257
            if (! $result instanceof Iterator) {
258
                throw new UnexpectedValueException('Iterator was not returned for query type: ' . $this->query['type']);
259
            }
260
            $this->iterator = $result;
261
        }
262 14
263
        return $this->iterator;
264 14
    }
265
266
    /**
267
     * Return the query structure.
268
     */
269
    public function getQuery() : array
270
    {
271
        return $this->query;
272 64
    }
273
274 64
    /**
275 64
     * Execute the query and return the first result.
276
     *
277 64
     * @return array|object|null
278
     */
279
    public function getSingleResult()
280
    {
281
        $clonedQuery                 = clone $this;
282
        $clonedQuery->query['limit'] = 1;
283
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\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 163
303
    /**
304 163
     * Set whether documents should be registered in UnitOfWork. If document would
305 163
     * 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
    public function setReadOnly(bool $readOnly) : void
310
    {
311
        $this->unitOfWorkHints[self::HINT_READ_ONLY] = $readOnly;
312
    }
313 163
314
    /**
315 163
     * Set whether to refresh hydrated documents that are already in the
316 163
     * identity map.
317
     *
318
     * This option has no effect if hydration is disabled.
319
     */
320
    public function setRefresh(bool $refresh) : void
321
    {
322
        $this->unitOfWorkHints[self::HINT_REFRESH] = $refresh;
323 11
    }
324
325 11
    /**
326
     * Set to enable wrapping of resulting Iterator with CachingIterator
327
     */
328
    public function setRewindable(bool $rewindable = true) : void
329
    {
330
        $this->rewindable = $rewindable;
331
    }
332 121
333
    /**
334 121
     * Execute the query and return its results as an array.
335 121
     *
336
     * @see IteratorAggregate::toArray()
337 85
     */
338 121
    public function toArray() : array
339
    {
340
        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
    private function getQueryOptions(string ...$keys) : array
348
    {
349
        return array_filter(
350 107
            array_intersect_key($this->query, array_flip($keys)),
351
            static function ($value) {
352 107
                return $value !== null;
353 99
            }
354
        );
355
    }
356 107
357
    /**
358 107
     * Decorate the cursor with caching, hydration, and priming behavior.
359 20
     *
360 20
     * 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 107
     * so this should not have any adverse effects.
364
     */
365
    private function makeIterator(Traversable $cursor) : Iterator
366
    {
367
        if ($this->hydrate) {
368
            $cursor = new HydratingIterator($cursor, $this->dm->getUnitOfWork(), $this->class, $this->unitOfWorkHints);
369
        }
370
371 112
        $cursor = $this->rewindable ? new CachingIterator($cursor) : new UnrewindableIterator($cursor);
372
373 112
        if (! empty($this->primers)) {
374 44
            $referencePrimer = new ReferencePrimer($this->dm, $this->dm->getUnitOfWork());
375
            $cursor          = new PrimingIterator($cursor, $this->class, $referencePrimer, $this->primers, $this->unitOfWorkHints);
376
        }
377 83
378 83
        return $cursor;
379
    }
380 83
381 83
    /**
382 83
     * Returns an array with its keys renamed based on the translation map.
383
     *
384 83
     * @return array $rename Translation map (from => to) for renaming keys
385
     */
386
    private function renameQueryOptions(array $options, array $rename) : array
387
    {
388 83
        if (empty($options)) {
389
            return $options;
390 83
        }
391
392
        $options = array_combine(
393
            array_map(
394
                static function ($key) use ($rename) {
395
                    return $rename[$key] ?? $key;
396
                },
397
                array_keys($options)
398
            ),
399
            array_values($options)
400
        );
401
402
        // Necessary because of https://github.com/phpstan/phpstan/issues/1580
403
        assert($options !== false);
404 123
405
        return $options;
406 123
    }
407
408 123
    /**
409 123
     * Execute the query and return its result.
410 107
     *
411 107
     * 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 107
     * commands and operations may return a status array or a boolean, depending
414 107
     * on the driver's write concern. Queries and some mapReduce commands will
415 107
     * return an Iterator.
416
     *
417
     * @return Iterator|UpdateResult|InsertOneResult|DeleteResult|array|object|int|null
418 107
     */
419 24
    private function runQuery()
420 6
    {
421 6
        $options = $this->options;
422 6
423
        switch ($this->query['type']) {
424 6
            case self::TYPE_FIND:
425
                $queryOptions = $this->getQueryOptions('select', 'sort', 'skip', 'limit', 'readPreference');
426 6
                $queryOptions = $this->renameQueryOptions($queryOptions, ['select' => 'projection']);
427 6
428 6
                $cursor = $this->collection->find(
429 6
                    $this->query['query'],
430
                    array_merge($options, $queryOptions)
431 19
                );
432 2
433 2
                return $this->makeIterator($cursor);
434
            case self::TYPE_FIND_AND_UPDATE:
435 2
                $queryOptions                   = $this->getQueryOptions('select', 'sort', 'upsert');
436 2
                $queryOptions                   = $this->renameQueryOptions($queryOptions, ['select' => 'projection']);
437 2
                $queryOptions['returnDocument'] = $this->query['new'] ?? false ? FindOneAndUpdate::RETURN_DOCUMENT_AFTER : FindOneAndUpdate::RETURN_DOCUMENT_BEFORE;
438
439 17
                $operation = $this->isFirstKeyUpdateOperator() ? 'findOneAndUpdate' : 'findOneAndReplace';
440
441 17
                return $this->collection->{$operation}(
442 13
                    $this->query['query'],
443
                    $this->query['newObj'],
444 13
                    array_merge($options, $queryOptions)
445 12
                );
446
            case self::TYPE_FIND_AND_REMOVE:
447 1
                $queryOptions = $this->getQueryOptions('select', 'sort');
448 1
                $queryOptions = $this->renameQueryOptions($queryOptions, ['select' => 'projection']);
449
450
                return $this->collection->findOneAndDelete(
451
                    $this->query['query'],
452
                    array_merge($options, $queryOptions)
453
                );
454 12
            case self::TYPE_INSERT:
455 2
                return $this->collection->insertOne($this->query['newObj'], $options);
456 2
            case self::TYPE_UPDATE:
457 2
                $multiple = $this->query['multiple'] ?? false;
458 2
459
                if ($this->isFirstKeyUpdateOperator()) {
460
                    $operation = 'updateOne';
461
                } else {
462 10
                    if ($multiple) {
463 10
                        throw new InvalidArgumentException('Combining the "multiple" option without using an update operator as first operation in a query is not supported.');
464 10
                    }
465 10
466
                    $operation = 'replaceOne';
467 4
                }
468 1
469 3
                if ($multiple) {
470 2
                    return $this->collection->updateMany(
471 2
                        $this->query['query'],
472
                        $this->query['newObj'],
473 2
                        array_merge($options, $this->getQueryOptions('upsert'))
474 2
                    );
475 2
                }
476 2
477
                return $this->collection->{$operation}(
478 1
                    $this->query['query'],
479 1
                    $this->query['newObj'],
480 1
                    array_merge($options, $this->getQueryOptions('upsert'))
481
                );
482 1
            case self::TYPE_REMOVE:
483 1
                return $this->collection->deleteMany($this->query['query'], $options);
484 1
            case self::TYPE_DISTINCT:
485
                $collection = $this->collection;
486
                $query      = $this->query;
487
488
                return $collection->distinct(
489
                    $query['distinct'],
490
                    $query['query'],
491 19
                    array_merge($options, $this->getQueryOptions('readPreference'))
492
                );
493 19
            case self::TYPE_COUNT:
494 19
                $collection = $this->collection;
495
                $query      = $this->query;
496 19
497
                return $collection->count(
498
                    $query['query'],
499
                    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
    private function isFirstKeyUpdateOperator() : bool
507
    {
508
        reset($this->query['newObj']);
509
        $firstKey = key($this->query['newObj']);
510
511
        return is_string($firstKey) && $firstKey[0] === '$';
512
    }
513
}
514