Completed
Push — master ( f2e43e...df01cf )
by Maciej
14:18
created

lib/Doctrine/ODM/MongoDB/Query/Query.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
namespace Doctrine\ODM\MongoDB\Query;
4
5
use Doctrine\ODM\MongoDB\DocumentManager;
6
use Doctrine\ODM\MongoDB\Iterator\CachingIterator;
7
use Doctrine\ODM\MongoDB\Iterator\HydratingIterator;
8
use Doctrine\ODM\MongoDB\Iterator\Iterator;
9
use Doctrine\ODM\MongoDB\Iterator\PrimingIterator;
10
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
11
use MongoDB\Collection;
12
use MongoDB\Driver\Cursor;
13
use MongoDB\Operation\FindOneAndUpdate;
14
15
16
/**
17
 * ODM Query wraps the raw Doctrine MongoDB queries to add additional functionality
18
 * and to hydrate the raw arrays of data to Doctrine document objects.
19
 *
20
 * @since       1.0
21
 */
22
class Query implements \IteratorAggregate
23
{
24
    const TYPE_FIND            = 1;
25
    const TYPE_FIND_AND_UPDATE = 2;
26
    const TYPE_FIND_AND_REMOVE = 3;
27
    const TYPE_INSERT          = 4;
28
    const TYPE_UPDATE          = 5;
29
    const TYPE_REMOVE          = 6;
30
    const TYPE_GROUP           = 7;
31
    const TYPE_MAP_REDUCE      = 8;
32
    const TYPE_DISTINCT        = 9;
33
    const TYPE_COUNT           = 11;
34
35
    /**
36
     * @deprecated 1.1 Will be removed for 2.0
37
     */
38
    const TYPE_GEO_LOCATION = 10;
39
40
    const HINT_REFRESH = 1;
41
    /** @deprecated */
42
    const HINT_SLAVE_OKAY = 2;
43
    const HINT_READ_PREFERENCE = 3;
44
    const HINT_READ_ONLY = 5;
45
46
    /**
47
     * The DocumentManager instance.
48
     *
49
     * @var DocumentManager
50
     */
51
    private $dm;
52
53
    /**
54
     * The ClassMetadata instance.
55
     *
56
     * @var ClassMetadata
57
     */
58
    private $class;
59
60
    /**
61
     * Whether to hydrate results as document class instances.
62
     *
63
     * @var boolean
64
     */
65
    private $hydrate = true;
66
67
    /**
68
     * Array of primer Closure instances.
69
     *
70
     * @var array
71
     */
72
    private $primers = array();
73
74
    /**
75
     * Hints for UnitOfWork behavior.
76
     *
77
     * @var array
78
     */
79
    private $unitOfWorkHints = array();
80
81
    /**
82
     * The Collection instance.
83
     *
84
     * @var Collection
85
     */
86
    protected $collection;
87
88
    /**
89
     * Query structure generated by the Builder class.
90
     *
91
     * @var array
92
     */
93
    private $query;
94
95
    /**
96
     * @var Iterator
97
     */
98
    private $iterator;
99
100
    /**
101
     * Query options
102
     *
103
     * @var array
104
     */
105
    private $options;
106
107
    /**
108
     * Constructor.
109
     *
110
     * Please note that $requireIndexes was deprecated in 1.2 and will be removed in 2.0
111
     *
112
     * @param DocumentManager $dm
113
     * @param ClassMetadata $class
114
     * @param Collection $collection
115
     * @param array $query
116
     * @param array $options
117
     * @param boolean $hydrate
118
     * @param boolean $refresh
119
     * @param array $primers
120
     * @param boolean $readOnly
121
     */
122 159
    public function __construct(DocumentManager $dm, ClassMetadata $class, Collection $collection, array $query = array(), array $options = array(), $hydrate = true, $refresh = false, array $primers = array(), $readOnly = false)
123
    {
124 159
        $primers = array_filter($primers);
125
126 159
        if ( ! empty($primers)) {
127 22
            $query['eagerCursor'] = true;
128
        }
129
130 159
        if ( ! empty($query['eagerCursor'])) {
131 23
            $query['useIdentifierKeys'] = false;
132
        }
133
134 159
        switch ($query['type']) {
135 159
            case self::TYPE_FIND:
136 36
            case self::TYPE_FIND_AND_UPDATE:
137 24
            case self::TYPE_FIND_AND_REMOVE:
138 21
            case self::TYPE_INSERT:
139 20
            case self::TYPE_UPDATE:
140 8
            case self::TYPE_REMOVE:
141 6
            case self::TYPE_GROUP:
142 6
            case self::TYPE_MAP_REDUCE:
143 6
            case self::TYPE_DISTINCT:
144 4
            case self::TYPE_COUNT:
145 158
                break;
146
147
            default:
148 1
                throw new \InvalidArgumentException('Invalid query type: ' . $query['type']);
149
        }
150
151 158
        $this->collection = $collection;
152 158
        $this->query      = $query;
153 158
        $this->options    = $options;
154 158
        $this->dm = $dm;
155 158
        $this->class = $class;
156 158
        $this->hydrate = $hydrate;
157 158
        $this->primers = $primers;
158
159 158
        $this->setReadOnly($readOnly);
160 158
        $this->setRefresh($refresh);
161
162 158
        if (isset($query['slaveOkay'])) {
163
            $this->unitOfWorkHints[self::HINT_SLAVE_OKAY] = $query['slaveOkay'];
0 ignored issues
show
Deprecated Code introduced by
The constant Doctrine\ODM\MongoDB\Query\Query::HINT_SLAVE_OKAY has been deprecated.

This class constant has been deprecated.

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