Completed
Pull Request — master (#1803)
by Maciej
15:26 queued 06:04
created

Query::execute()   D

Complexity

Conditions 10
Paths 7

Size

Total Lines 34
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 10.0203

Importance

Changes 0
Metric Value
dl 0
loc 34
ccs 16
cts 17
cp 0.9412
rs 4.8196
c 0
b 0
f 0
cc 10
eloc 17
nc 7
nop 0
crap 10.0203

How to fix   Complexity   

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:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ODM\MongoDB\Query;
6
7
use Doctrine\ODM\MongoDB\DocumentManager;
8
use Doctrine\ODM\MongoDB\Iterator\CachingIterator;
9
use Doctrine\ODM\MongoDB\Iterator\HydratingIterator;
10
use Doctrine\ODM\MongoDB\Iterator\Iterator;
11
use Doctrine\ODM\MongoDB\Iterator\PrimingIterator;
12
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
13
use Doctrine\ODM\MongoDB\MongoDBException;
14
use MongoDB\Collection;
15
use MongoDB\Driver\Cursor;
16
use MongoDB\Operation\FindOneAndUpdate;
17
use function array_combine;
18
use function array_filter;
19
use function array_flip;
20
use function array_intersect_key;
21
use function array_keys;
22
use function array_map;
23
use function array_merge;
24
use function array_values;
25
use function is_array;
26
use function is_callable;
27
28
/**
29
 * ODM Query wraps the raw Doctrine MongoDB queries to add additional functionality
30
 * and to hydrate the raw arrays of data to Doctrine document objects.
31
 *
32
 */
33
class Query implements \IteratorAggregate
34
{
35
    public const TYPE_FIND            = 1;
36
    public const TYPE_FIND_AND_UPDATE = 2;
37
    public const TYPE_FIND_AND_REMOVE = 3;
38
    public const TYPE_INSERT          = 4;
39
    public const TYPE_UPDATE          = 5;
40
    public const TYPE_REMOVE          = 6;
41
    public const TYPE_GROUP           = 7;
42
    public const TYPE_MAP_REDUCE      = 8;
43
    public const TYPE_DISTINCT        = 9;
44
    public const TYPE_COUNT           = 11;
45
46
    /**
47
     * @deprecated 1.1 Will be removed for 2.0
48
     */
49
    public const TYPE_GEO_LOCATION = 10;
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
    /**
116
     *
117
     *
118
     * Please note that $requireIndexes was deprecated in 1.2 and will be removed in 2.0
119
     *
120
     * @param array $query
121
     * @param array $options
122
     * @param bool  $hydrate
123
     * @param bool  $refresh
124
     * @param array $primers
125
     * @param bool  $readOnly
126
     */
127 161
    public function __construct(DocumentManager $dm, ClassMetadata $class, Collection $collection, array $query = [], array $options = [], $hydrate = true, $refresh = false, array $primers = [], $readOnly = false)
128
    {
129 161
        $primers = array_filter($primers);
130
131 161
        if (! empty($primers)) {
132 22
            $query['eagerCursor'] = true;
133
        }
134
135 161
        if (! empty($query['eagerCursor'])) {
136 23
            $query['useIdentifierKeys'] = false;
137
        }
138
139 161
        switch ($query['type']) {
140 161
            case self::TYPE_FIND:
141 36
            case self::TYPE_FIND_AND_UPDATE:
142 24
            case self::TYPE_FIND_AND_REMOVE:
143 21
            case self::TYPE_INSERT:
144 20
            case self::TYPE_UPDATE:
145 8
            case self::TYPE_REMOVE:
146 6
            case self::TYPE_GROUP:
147 6
            case self::TYPE_MAP_REDUCE:
148 6
            case self::TYPE_DISTINCT:
149 4
            case self::TYPE_COUNT:
150 160
                break;
151
152
            default:
153 1
                throw new \InvalidArgumentException('Invalid query type: ' . $query['type']);
154
        }
155
156 160
        $this->collection = $collection;
157 160
        $this->query      = $query;
158 160
        $this->options    = $options;
159 160
        $this->dm = $dm;
160 160
        $this->class = $class;
161 160
        $this->hydrate = $hydrate;
162 160
        $this->primers = $primers;
163
164 160
        $this->setReadOnly($readOnly);
165 160
        $this->setRefresh($refresh);
166
167 160
        if (! isset($query['readPreference'])) {
168 154
            return;
169
        }
170
171 6
        $this->unitOfWorkHints[self::HINT_READ_PREFERENCE] = $query['readPreference'];
172 6
    }
173
174 64
    public function __clone()
175
    {
176 64
        $this->iterator = null;
177 64
    }
178
179
    /**
180
     * Return an array of information about the query structure for debugging.
181
     *
182
     * The $name parameter may be used to return a specific key from the
183
     * internal $query array property. If omitted, the entire array will be
184
     * returned.
185
     *
186
     * @param string $name
187
     * @return mixed
188
     */
189 27
    public function debug($name = null)
190
    {
191 27
        return $name !== null ? $this->query[$name] : $this->query;
192
    }
193
194
    /**
195
     * Execute the query and returns the results.
196
     *
197
     * @throws MongoDBException
198
     * @return Iterator|int|string|array|object
199
     */
200 120
    public function execute()
201
    {
202 120
        $results = $this->runQuery();
203
204 120
        if (! $this->hydrate) {
205 9
            return $results;
206
        }
207
208 114
        if ($results instanceof Cursor) {
0 ignored issues
show
Bug introduced by
The class MongoDB\Driver\Cursor does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
209
            $results = $this->makeIterator($results);
210
        }
211
212 114
        $uow = $this->dm->getUnitOfWork();
213
214
        /* If a single document is returned from a findAndModify command and it
215
         * includes the identifier field, attempt hydration.
216
         */
217 114
        if (($this->query['type'] === self::TYPE_FIND_AND_UPDATE ||
218 114
                $this->query['type'] === self::TYPE_FIND_AND_REMOVE) &&
219 114
            is_array($results) && isset($results['_id'])) {
220 5
            $results = $uow->getOrCreateDocument($this->class->name, $results, $this->unitOfWorkHints);
221
222 5
            if (! empty($this->primers)) {
223 1
                $referencePrimer = new ReferencePrimer($this->dm, $uow);
224
225 1
                foreach ($this->primers as $fieldName => $primer) {
226 1
                    $primer = is_callable($primer) ? $primer : null;
227 1
                    $referencePrimer->primeReferences($this->class, [$results], $fieldName, $this->unitOfWorkHints, $primer);
228
                }
229
            }
230
        }
231
232 114
        return $results;
233
    }
234
235
    /**
236
     * Gets the ClassMetadata instance.
237
     *
238
     * @return ClassMetadata $class
239
     */
240
    public function getClass()
241
    {
242
        return $this->class;
243
    }
244
245
    /**
246
     * Gets the DocumentManager instance.
247
     *
248
     * @return DocumentManager $dm
249
     */
250
    public function getDocumentManager()
251
    {
252
        return $this->dm;
253
    }
254
255
    /**
256
     * Execute the query and return its result, which must be an Iterator.
257
     *
258
     * If the query type is not expected to return an Iterator,
259
     * BadMethodCallException will be thrown before executing the query.
260
     * Otherwise, the query will be executed and UnexpectedValueException will
261
     * be thrown if {@link Query::execute()} does not return an Iterator.
262
     *
263
     * @see http://php.net/manual/en/iteratoraggregate.getiterator.php
264
     * @return Iterator
265
     * @throws \BadMethodCallException If the query type would not return an Iterator.
266
     * @throws \UnexpectedValueException If the query did not return an Iterator.
267
     */
268 84
    public function getIterator()
269
    {
270 84
        switch ($this->query['type']) {
271 84
            case self::TYPE_FIND:
272 6
            case self::TYPE_GROUP:
273 6
            case self::TYPE_MAP_REDUCE:
274 6
            case self::TYPE_DISTINCT:
275 78
                break;
276
277
            default:
278 6
                throw new \BadMethodCallException('Iterator would not be returned for query type: ' . $this->query['type']);
279
        }
280
281 78
        if ($this->iterator === null) {
282 78
            $result = $this->execute();
283 78
            if (! $result instanceof Iterator) {
284
                throw new \UnexpectedValueException('Iterator was not returned for query type: ' . $this->query['type']);
285
            }
286 78
            $this->iterator = $result;
287
        }
288
289 78
        return $this->iterator;
290
    }
291
292
    /**
293
     * Return the query structure.
294
     *
295
     * @return array
296
     */
297 14
    public function getQuery()
298
    {
299 14
        return $this->query;
300
    }
301
302
    /**
303
     * Execute the query and return the first result.
304
     *
305
     * @return array|object|null
306
     */
307 64
    public function getSingleResult()
308
    {
309 64
        $clonedQuery = clone $this;
310 64
        $clonedQuery->query['limit'] = 1;
311 64
        return $clonedQuery->getIterator()->current() ?: null;
312
    }
313
314
    /**
315
     * Return the query type.
316
     *
317
     * @return int
318
     */
319
    public function getType()
320
    {
321
        return $this->query['type'];
322
    }
323
324
    /**
325
     * Sets whether or not to hydrate the documents to objects.
326
     *
327
     * @param bool $hydrate
328
     */
329
    public function setHydrate($hydrate)
330
    {
331
        $this->hydrate = (bool) $hydrate;
332
    }
333
334
    /**
335
     * Set whether documents should be registered in UnitOfWork. If document would
336
     * already be managed it will be left intact and new instance returned.
337
     *
338
     * This option has no effect if hydration is disabled.
339
     *
340
     * @param bool $readOnly
341
     */
342 160
    public function setReadOnly($readOnly)
343
    {
344 160
        $this->unitOfWorkHints[self::HINT_READ_ONLY] = (bool) $readOnly;
345 160
    }
346
347
    /**
348
     * Set whether to refresh hydrated documents that are already in the
349
     * identity map.
350
     *
351
     * This option has no effect if hydration is disabled.
352
     *
353
     * @param bool $refresh
354
     */
355 160
    public function setRefresh($refresh)
356
    {
357 160
        $this->unitOfWorkHints[self::HINT_REFRESH] = (bool) $refresh;
358 160
    }
359
360
    /**
361
     * Execute the query and return its results as an array.
362
     *
363
     * @see IteratorAggregate::toArray()
364
     * @return array
365
     */
366 11
    public function toArray()
367
    {
368 11
        return $this->getIterator()->toArray();
369
    }
370
371
    /**
372
     * Returns an array containing the specified keys and their values from the
373
     * query array, provided they exist and are not null.
374
     *
375
     * @param string ...$keys One or more option keys to be read
376
     * @return array
377
     */
378 119
    private function getQueryOptions(string ...$keys)
379
    {
380 119
        return array_filter(
381 119
            array_intersect_key($this->query, array_flip($keys)),
382
            function ($value) {
383 88
                return $value !== null;
384 119
            }
385
        );
386
    }
387
388 106
    private function makeIterator(Cursor $cursor): Iterator
389
    {
390 106
        if ($this->hydrate && $this->class) {
391 98
            $cursor = new HydratingIterator($cursor, $this->dm->getUnitOfWork(), $this->class, $this->unitOfWorkHints);
392
        }
393
394 106
        $cursor = new CachingIterator($cursor);
395
396 106
        if (! empty($this->primers)) {
397 20
            $referencePrimer = new ReferencePrimer($this->dm, $this->dm->getUnitOfWork());
398 20
            $cursor = new PrimingIterator($cursor, $this->class, $referencePrimer, $this->primers, $this->unitOfWorkHints);
399
        }
400
401 106
        return $cursor;
402
    }
403
404
    /**
405
     * Returns an array with its keys renamed based on the translation map.
406
     *
407
     * @param array $options Query options
408
     * @return array $rename Translation map (from => to) for renaming keys
409
     */
410 111
    private function renameQueryOptions(array $options, array $rename)
411
    {
412 111
        if (empty($options)) {
413 42
            return $options;
414
        }
415
416 86
        return array_combine(
417 86
            array_map(
418
                function ($key) use ($rename) {
419 86
                    return $rename[$key] ?? $key;
420 86
                },
421 86
                array_keys($options)
422
            ),
423 86
            array_values($options)
424
        );
425
    }
426
427
    /**
428
     * Execute the query and return its result.
429
     *
430
     * The return value will vary based on the query type. Commands with results
431
     * (e.g. aggregate, inline mapReduce) may return an ArrayIterator. Other
432
     * commands and operations may return a status array or a boolean, depending
433
     * on the driver's write concern. Queries and some mapReduce commands will
434
     * return an Iterator.
435
     *
436
     * @return Iterator|string|int|array|object|null
437
     */
438 120
    public function runQuery()
439
    {
440 120
        $options = $this->options;
441
442 120
        switch ($this->query['type']) {
443 120
            case self::TYPE_FIND:
444 106
                $queryOptions = $this->getQueryOptions('select', 'sort', 'skip', 'limit', 'readPreference');
445 106
                $queryOptions = $this->renameQueryOptions($queryOptions, ['select' => 'projection']);
446
447 106
                $cursor = $this->collection->find(
448 106
                    $this->query['query'],
449 106
                    $queryOptions
450
                );
451
452 106
                return $this->makeIterator($cursor);
453
454 22
            case self::TYPE_FIND_AND_UPDATE:
455 6
                $queryOptions = $this->getQueryOptions('select', 'sort', 'upsert');
456 6
                $queryOptions = $this->renameQueryOptions($queryOptions, ['select' => 'projection']);
457 6
                $queryOptions['returnDocument'] = ($this->query['new'] ?? false) ? FindOneAndUpdate::RETURN_DOCUMENT_AFTER : FindOneAndUpdate::RETURN_DOCUMENT_BEFORE;
458
459 6
                return $this->collection->findOneAndUpdate(
460 6
                    $this->query['query'],
461 6
                    $this->query['newObj'],
462 6
                    array_merge($options, $queryOptions)
463
                );
464
465 17
            case self::TYPE_FIND_AND_REMOVE:
466 2
                $queryOptions = $this->getQueryOptions('select', 'sort');
467 2
                $queryOptions = $this->renameQueryOptions($queryOptions, ['select' => 'projection']);
468
469 2
                return $this->collection->findOneAndDelete(
470 2
                    $this->query['query'],
471 2
                    array_merge($options, $queryOptions)
472
                );
473
474 15
            case self::TYPE_INSERT:
475
                return $this->collection->insertOne($this->query['newObj'], $options);
476
477 15
            case self::TYPE_UPDATE:
478 10
                if ($this->query['multiple'] ?? false) {
479 2
                    return $this->collection->updateMany(
480 2
                        $this->query['query'],
481 2
                        $this->query['newObj'],
482 2
                        array_merge($options, $this->getQueryOptions('upsert'))
483
                    );
484
                }
485
486 8
                return $this->collection->updateOne(
487 8
                    $this->query['query'],
488 8
                    $this->query['newObj'],
489 8
                    array_merge($options, $this->getQueryOptions('upsert'))
490
                );
491
492 5
            case self::TYPE_REMOVE:
493 1
                return $this->collection->deleteMany($this->query['query'], $options);
494
495 4
            case self::TYPE_DISTINCT:
496 2
                $collection = $this->collection;
497 2
                $query = $this->query;
498
499 2
                return $collection->distinct(
500 2
                    $query['distinct'],
501 2
                    $query['query'],
502 2
                    array_merge($options, $this->getQueryOptions('readPreference'))
503
                );
504
505 2
            case self::TYPE_COUNT:
506 2
                $collection = $this->collection;
507 2
                $query = $this->query;
508
509 2
                return $collection->count(
510 2
                    $query['query'],
511 2
                    array_merge($options, $this->getQueryOptions('hint', 'limit', 'skip', 'readPreference'))
512
                );
513
        }
514
    }
515
}
516