Completed
Pull Request — master (#1803)
by Maciej
18:23 queued 15:20
created

Query::getIterator()   C

Complexity

Conditions 7
Paths 13

Size

Total Lines 23
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 7.0178

Importance

Changes 0
Metric Value
dl 0
loc 23
ccs 13
cts 14
cp 0.9286
rs 6.7272
c 0
b 0
f 0
cc 7
eloc 15
nc 13
nop 0
crap 7.0178
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
    public function setHydrate(bool $hydrate)
328
    {
329
        $this->hydrate = $hydrate;
330
    }
331
332
    /**
333
     * Set whether documents should be registered in UnitOfWork. If document would
334
     * already be managed it will be left intact and new instance returned.
335
     *
336
     * This option has no effect if hydration is disabled.
337
     */
338 160
    public function setReadOnly(bool $readOnly)
339
    {
340 160
        $this->unitOfWorkHints[self::HINT_READ_ONLY] = $readOnly;
341 160
    }
342
343
    /**
344
     * Set whether to refresh hydrated documents that are already in the
345
     * identity map.
346
     *
347
     * This option has no effect if hydration is disabled.
348
     */
349 160
    public function setRefresh(bool $refresh)
350
    {
351 160
        $this->unitOfWorkHints[self::HINT_REFRESH] = $refresh;
352 160
    }
353
354
    /**
355
     * Execute the query and return its results as an array.
356
     *
357
     * @see IteratorAggregate::toArray()
358
     * @return array
359
     */
360 11
    public function toArray()
361
    {
362 11
        return $this->getIterator()->toArray();
363
    }
364
365
    /**
366
     * Returns an array containing the specified keys and their values from the
367
     * query array, provided they exist and are not null.
368
     *
369
     * @param string ...$keys One or more option keys to be read
370
     * @return array
371
     */
372 119
    private function getQueryOptions(string ...$keys)
373
    {
374 119
        return array_filter(
375 119
            array_intersect_key($this->query, array_flip($keys)),
376
            function ($value) {
377 84
                return $value !== null;
378 119
            }
379
        );
380
    }
381
382 106
    private function makeIterator(Cursor $cursor): Iterator
383
    {
384 106
        if ($this->hydrate && $this->class) {
385 98
            $cursor = new HydratingIterator($cursor, $this->dm->getUnitOfWork(), $this->class, $this->unitOfWorkHints);
386
        }
387
388 106
        $cursor = new CachingIterator($cursor);
389
390 106
        if (! empty($this->primers)) {
391 20
            $referencePrimer = new ReferencePrimer($this->dm, $this->dm->getUnitOfWork());
392 20
            $cursor = new PrimingIterator($cursor, $this->class, $referencePrimer, $this->primers, $this->unitOfWorkHints);
393
        }
394
395 106
        return $cursor;
396
    }
397
398
    /**
399
     * Returns an array with its keys renamed based on the translation map.
400
     *
401
     * @param array $options Query options
402
     * @return array $rename Translation map (from => to) for renaming keys
403
     */
404 111
    private function renameQueryOptions(array $options, array $rename)
405
    {
406 111
        if (empty($options)) {
407 44
            return $options;
408
        }
409
410 82
        return array_combine(
411 82
            array_map(
412
                function ($key) use ($rename) {
413 82
                    return $rename[$key] ?? $key;
414 82
                },
415 82
                array_keys($options)
416
            ),
417 82
            array_values($options)
418
        );
419
    }
420
421
    /**
422
     * Execute the query and return its result.
423
     *
424
     * The return value will vary based on the query type. Commands with results
425
     * (e.g. aggregate, inline mapReduce) may return an ArrayIterator. Other
426
     * commands and operations may return a status array or a boolean, depending
427
     * on the driver's write concern. Queries and some mapReduce commands will
428
     * return an Iterator.
429
     *
430
     * @return Iterator|string|int|array|object|null
431
     */
432 120
    public function runQuery()
433
    {
434 120
        $options = $this->options;
435
436 120
        switch ($this->query['type']) {
437 120
            case self::TYPE_FIND:
438 106
                $queryOptions = $this->getQueryOptions('select', 'sort', 'skip', 'limit', 'readPreference');
439 106
                $queryOptions = $this->renameQueryOptions($queryOptions, ['select' => 'projection']);
440
441 106
                $cursor = $this->collection->find(
442 106
                    $this->query['query'],
443 106
                    $queryOptions
444
                );
445
446 106
                return $this->makeIterator($cursor);
447
448 22
            case self::TYPE_FIND_AND_UPDATE:
449 6
                $queryOptions = $this->getQueryOptions('select', 'sort', 'upsert');
450 6
                $queryOptions = $this->renameQueryOptions($queryOptions, ['select' => 'projection']);
451 6
                $queryOptions['returnDocument'] = ($this->query['new'] ?? false) ? FindOneAndUpdate::RETURN_DOCUMENT_AFTER : FindOneAndUpdate::RETURN_DOCUMENT_BEFORE;
452
453 6
                return $this->collection->findOneAndUpdate(
454 6
                    $this->query['query'],
455 6
                    $this->query['newObj'],
456 6
                    array_merge($options, $queryOptions)
457
                );
458
459 17
            case self::TYPE_FIND_AND_REMOVE:
460 2
                $queryOptions = $this->getQueryOptions('select', 'sort');
461 2
                $queryOptions = $this->renameQueryOptions($queryOptions, ['select' => 'projection']);
462
463 2
                return $this->collection->findOneAndDelete(
464 2
                    $this->query['query'],
465 2
                    array_merge($options, $queryOptions)
466
                );
467
468 15
            case self::TYPE_INSERT:
469
                return $this->collection->insertOne($this->query['newObj'], $options);
470
471 15
            case self::TYPE_UPDATE:
472 10
                if ($this->query['multiple'] ?? false) {
473 2
                    return $this->collection->updateMany(
474 2
                        $this->query['query'],
475 2
                        $this->query['newObj'],
476 2
                        array_merge($options, $this->getQueryOptions('upsert'))
477
                    );
478
                }
479
480 8
                return $this->collection->updateOne(
481 8
                    $this->query['query'],
482 8
                    $this->query['newObj'],
483 8
                    array_merge($options, $this->getQueryOptions('upsert'))
484
                );
485
486 5
            case self::TYPE_REMOVE:
487 1
                return $this->collection->deleteMany($this->query['query'], $options);
488
489 4
            case self::TYPE_DISTINCT:
490 2
                $collection = $this->collection;
491 2
                $query = $this->query;
492
493 2
                return $collection->distinct(
494 2
                    $query['distinct'],
495 2
                    $query['query'],
496 2
                    array_merge($options, $this->getQueryOptions('readPreference'))
497
                );
498
499 2
            case self::TYPE_COUNT:
500 2
                $collection = $this->collection;
501 2
                $query = $this->query;
502
503 2
                return $collection->count(
504 2
                    $query['query'],
505 2
                    array_merge($options, $this->getQueryOptions('hint', 'limit', 'skip', 'readPreference'))
506
                );
507
        }
508
    }
509
}
510