Completed
Push — master ( 9574ed...280e05 )
by Andreas
20s
created

Query::__construct()   B

Complexity

Conditions 10
Paths 17

Size

Total Lines 36

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 26
CRAP Score 10

Importance

Changes 0
Metric Value
dl 0
loc 36
ccs 26
cts 26
cp 1
rs 7.6666
c 0
b 0
f 0
cc 10
nc 17
nop 9
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\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 163
    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 163
        $primers = array_filter($primers);
118
119 163
        switch ($query['type']) {
120 163
            case self::TYPE_FIND:
121 38
            case self::TYPE_FIND_AND_UPDATE:
122 26
            case self::TYPE_FIND_AND_REMOVE:
123 23
            case self::TYPE_INSERT:
124 22
            case self::TYPE_UPDATE:
125 8
            case self::TYPE_REMOVE:
126 6
            case self::TYPE_DISTINCT:
127 4
            case self::TYPE_COUNT:
128 162
                break;
129
130
            default:
131 1
                throw new InvalidArgumentException('Invalid query type: ' . $query['type']);
132
        }
133
134 162
        $this->collection = $collection;
135 162
        $this->query      = $query;
136 162
        $this->options    = $options;
137 162
        $this->dm         = $dm;
138 162
        $this->class      = $class;
139 162
        $this->hydrate    = $hydrate;
140 162
        $this->primers    = $primers;
141
142 162
        $this->setReadOnly($readOnly);
143 162
        $this->setRefresh($refresh);
144
145 162
        if (! isset($query['readPreference'])) {
146 156
            return;
147
        }
148
149 6
        $this->unitOfWorkHints[self::HINT_READ_PREFERENCE] = $query['readPreference'];
150 6
    }
151
152 64
    public function __clone()
153
    {
154 64
        $this->iterator = null;
155 64
    }
156
157
    /**
158
     * Return an array of information about the query structure for debugging.
159
     *
160
     * The $name parameter may be used to return a specific key from the
161
     * internal $query array property. If omitted, the entire array will be
162
     * returned.
163
     */
164 27
    public function debug(?string $name = null)
165
    {
166 27
        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
     *
174
     * @throws MongoDBException
175
     */
176 122
    public function execute()
177
    {
178 122
        $results = $this->runQuery();
179
180 122
        if (! $this->hydrate) {
181 9
            return $results;
182
        }
183
184 116
        $uow = $this->dm->getUnitOfWork();
185
186
        /* If a single document is returned from a findAndModify command and it
187
         * includes the identifier field, attempt hydration.
188
         */
189 116
        if (($this->query['type'] === self::TYPE_FIND_AND_UPDATE ||
190 116
                $this->query['type'] === self::TYPE_FIND_AND_REMOVE) &&
191 116
            is_array($results) && isset($results['_id'])) {
192 5
            $results = $uow->getOrCreateDocument($this->class->name, $results, $this->unitOfWorkHints);
193
194 5
            if (! empty($this->primers)) {
195 1
                $referencePrimer = new ReferencePrimer($this->dm, $uow);
196
197 1
                foreach ($this->primers as $fieldName => $primer) {
198 1
                    $primer = is_callable($primer) ? $primer : null;
199 1
                    $referencePrimer->primeReferences($this->class, [$results], $fieldName, $this->unitOfWorkHints, $primer);
200
                }
201
            }
202
        }
203
204 116
        return $results;
205
    }
206
207
    /**
208
     * Gets the ClassMetadata instance.
209
     */
210
    public function getClass() : ClassMetadata
211
    {
212
        return $this->class;
213
    }
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 83
    public function getIterator() : Iterator
235
    {
236 83
        switch ($this->query['type']) {
237 83
            case self::TYPE_FIND:
238 6
            case self::TYPE_DISTINCT:
239 77
                break;
240
241
            default:
242 6
                throw new BadMethodCallException('Iterator would not be returned for query type: ' . $this->query['type']);
243
        }
244
245 77
        if ($this->iterator === null) {
246 77
            $result = $this->execute();
247 77
            if (! $result instanceof Iterator) {
248
                throw new UnexpectedValueException('Iterator was not returned for query type: ' . $this->query['type']);
249
            }
250 77
            $this->iterator = $result;
251
        }
252
253 77
        return $this->iterator;
254
    }
255
256
    /**
257
     * Return the query structure.
258
     */
259 14
    public function getQuery() : array
260
    {
261 14
        return $this->query;
262
    }
263
264
    /**
265
     * Execute the query and return the first result.
266
     *
267
     * @return array|object|null
268
     */
269 64
    public function getSingleResult()
270
    {
271 64
        $clonedQuery                 = clone $this;
272 64
        $clonedQuery->query['limit'] = 1;
273 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\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
    {
281
        return $this->query['type'];
282
    }
283
284
    /**
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 162
    public function setReadOnly(bool $readOnly) : void
299
    {
300 162
        $this->unitOfWorkHints[self::HINT_READ_ONLY] = $readOnly;
301 162
    }
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 11
    public function toArray() : array
320
    {
321 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\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
    }
323
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 121
    private function getQueryOptions(string ...$keys) : array
329
    {
330 121
        return array_filter(
331 121
            array_intersect_key($this->query, array_flip($keys)),
332
            static function ($value) {
333 84
                return $value !== null;
334 121
            }
335
        );
336
    }
337
338 106
    private function makeIterator(Cursor $cursor) : Iterator
339
    {
340 106
        if ($this->hydrate) {
341 98
            $cursor = new HydratingIterator($cursor, $this->dm->getUnitOfWork(), $this->class, $this->unitOfWorkHints);
342
        }
343
344 106
        $cursor = new CachingIterator($cursor);
345
346 106
        if (! empty($this->primers)) {
347 20
            $referencePrimer = new ReferencePrimer($this->dm, $this->dm->getUnitOfWork());
348 20
            $cursor          = new PrimingIterator($cursor, $this->class, $referencePrimer, $this->primers, $this->unitOfWorkHints);
349
        }
350
351 106
        return $cursor;
352
    }
353
354
    /**
355
     * Returns an array with its keys renamed based on the translation map.
356
     *
357
     * @return array $rename Translation map (from => to) for renaming keys
358
     */
359 111
    private function renameQueryOptions(array $options, array $rename) : array
360
    {
361 111
        if (empty($options)) {
362 44
            return $options;
363
        }
364
365 82
        $options = array_combine(
366 82
            array_map(
367
                static function ($key) use ($rename) {
368 82
                    return $rename[$key] ?? $key;
369 82
                },
370 82
                array_keys($options)
371
            ),
372 82
            array_values($options)
373
        );
374
375
        // Necessary because of https://github.com/phpstan/phpstan/issues/1580
376 82
        assert($options !== false);
377
378 82
        return $options;
379
    }
380
381
    /**
382
     * Execute the query and return its result.
383
     *
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
     * on the driver's write concern. Queries and some mapReduce commands will
388
     * return an Iterator.
389
     *
390
     * @return Iterator|UpdateResult|InsertOneResult|DeleteResult|array|object|int|null
391
     */
392 122
    public function runQuery()
393
    {
394 122
        $options = $this->options;
395
396 122
        switch ($this->query['type']) {
397 122
            case self::TYPE_FIND:
398 106
                $queryOptions = $this->getQueryOptions('select', 'sort', 'skip', 'limit', 'readPreference');
399 106
                $queryOptions = $this->renameQueryOptions($queryOptions, ['select' => 'projection']);
400
401 106
                $cursor = $this->collection->find(
402 106
                    $this->query['query'],
403 106
                    $queryOptions
404
                );
405
406 106
                return $this->makeIterator($cursor);
407
408 24
            case self::TYPE_FIND_AND_UPDATE:
409 6
                $queryOptions                   = $this->getQueryOptions('select', 'sort', 'upsert');
410 6
                $queryOptions                   = $this->renameQueryOptions($queryOptions, ['select' => 'projection']);
411 6
                $queryOptions['returnDocument'] = $this->query['new'] ?? false ? FindOneAndUpdate::RETURN_DOCUMENT_AFTER : FindOneAndUpdate::RETURN_DOCUMENT_BEFORE;
412
413 6
                return $this->collection->findOneAndUpdate(
414 6
                    $this->query['query'],
415 6
                    $this->query['newObj'],
416 6
                    array_merge($options, $queryOptions)
417
                );
418
419 19
            case self::TYPE_FIND_AND_REMOVE:
420 2
                $queryOptions = $this->getQueryOptions('select', 'sort');
421 2
                $queryOptions = $this->renameQueryOptions($queryOptions, ['select' => 'projection']);
422
423 2
                return $this->collection->findOneAndDelete(
424 2
                    $this->query['query'],
425 2
                    array_merge($options, $queryOptions)
426
                );
427
428 17
            case self::TYPE_INSERT:
429
                return $this->collection->insertOne($this->query['newObj'], $options);
430
431 17
            case self::TYPE_UPDATE:
432 12
                if ($this->query['multiple'] ?? false) {
433 2
                    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
440 10
                return $this->collection->updateOne(
441 10
                    $this->query['query'],
442 10
                    $this->query['newObj'],
443 10
                    array_merge($options, $this->getQueryOptions('upsert'))
444
                );
445
446 5
            case self::TYPE_REMOVE:
447 1
                return $this->collection->deleteMany($this->query['query'], $options);
448
449 4
            case self::TYPE_DISTINCT:
450 2
                $collection = $this->collection;
451 2
                $query      = $this->query;
452
453 2
                return $collection->distinct(
454 2
                    $query['distinct'],
455 2
                    $query['query'],
456 2
                    array_merge($options, $this->getQueryOptions('readPreference'))
457
                );
458
459 2
            case self::TYPE_COUNT:
460 2
                $collection = $this->collection;
461 2
                $query      = $this->query;
462
463 2
                return $collection->count(
464 2
                    $query['query'],
465 2
                    array_merge($options, $this->getQueryOptions('hint', 'limit', 'skip', 'readPreference'))
466
                );
467
468
            default:
469
                throw new InvalidArgumentException('Invalid query type: ' . $this->query['type']);
470
        }
471
    }
472
}
473