Completed
Push — master ( b8f7dd...9625a8 )
by Maciej
11s
created

lib/Doctrine/ODM/MongoDB/Query/Query.php (2 issues)

Labels
Severity

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
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\Driver\Cursor;
19
use MongoDB\Operation\FindOneAndUpdate;
20
use UnexpectedValueException;
21
use function array_combine;
22
use function array_filter;
23
use function array_flip;
24
use function array_intersect_key;
25
use function array_keys;
26
use function array_map;
27
use function array_merge;
28
use function array_values;
29
use function is_array;
30
use function is_callable;
31
32
/**
33
 * ODM Query wraps the raw Doctrine MongoDB queries to add additional functionality
34
 * and to hydrate the raw arrays of data to Doctrine document objects.
35
 */
36
class Query implements IteratorAggregate
37
{
38
    public const TYPE_FIND            = 1;
39
    public const TYPE_FIND_AND_UPDATE = 2;
40
    public const TYPE_FIND_AND_REMOVE = 3;
41
    public const TYPE_INSERT          = 4;
42
    public const TYPE_UPDATE          = 5;
43
    public const TYPE_REMOVE          = 6;
44
    public const TYPE_GROUP           = 7;
45
    public const TYPE_MAP_REDUCE      = 8;
46
    public const TYPE_DISTINCT        = 9;
47
    public const TYPE_COUNT           = 11;
48
49
    /**
50
     * @deprecated 1.1 Will be removed for 2.0
51
     */
52
    public const TYPE_GEO_LOCATION = 10;
53
54
    public const HINT_REFRESH = 1;
55
    // 2 was used for HINT_SLAVE_OKAY, which was removed in 2.0
56
    public const HINT_READ_PREFERENCE = 3;
57
    public const HINT_READ_ONLY       = 5;
58
59
    /**
60
     * The DocumentManager instance.
61
     *
62
     * @var DocumentManager
63
     */
64
    private $dm;
65
66
    /**
67
     * The ClassMetadata instance.
68
     *
69
     * @var ClassMetadata
70
     */
71
    private $class;
72
73
    /**
74
     * Whether to hydrate results as document class instances.
75
     *
76
     * @var bool
77
     */
78
    private $hydrate = true;
79
80
    /**
81
     * Array of primer Closure instances.
82
     *
83
     * @var array
84
     */
85
    private $primers = [];
86
87
    /**
88
     * Hints for UnitOfWork behavior.
89
     *
90
     * @var array
91
     */
92
    private $unitOfWorkHints = [];
93
94
    /**
95
     * The Collection instance.
96
     *
97
     * @var Collection
98
     */
99
    protected $collection;
100
101
    /**
102
     * Query structure generated by the Builder class.
103
     *
104
     * @var array
105
     */
106
    private $query;
107
108
    /** @var Iterator */
109
    private $iterator;
110
111
    /**
112
     * Query options
113
     *
114
     * @var array
115
     */
116
    private $options;
117
118
    /**
119
     * Please note that $requireIndexes was deprecated in 1.2 and will be removed in 2.0
120
     */
121 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)
122
    {
123 163
        $primers = array_filter($primers);
124
125 163
        if (! empty($primers)) {
126 22
            $query['eagerCursor'] = true;
127
        }
128
129 163
        if (! empty($query['eagerCursor'])) {
130 23
            $query['useIdentifierKeys'] = false;
131
        }
132
133 163
        switch ($query['type']) {
134 163
            case self::TYPE_FIND:
135 38
            case self::TYPE_FIND_AND_UPDATE:
136 26
            case self::TYPE_FIND_AND_REMOVE:
137 23
            case self::TYPE_INSERT:
138 22
            case self::TYPE_UPDATE:
139 8
            case self::TYPE_REMOVE:
140 6
            case self::TYPE_GROUP:
141 6
            case self::TYPE_MAP_REDUCE:
142 6
            case self::TYPE_DISTINCT:
143 4
            case self::TYPE_COUNT:
144 162
                break;
145
146
            default:
147 1
                throw new InvalidArgumentException('Invalid query type: ' . $query['type']);
148
        }
149
150 162
        $this->collection = $collection;
151 162
        $this->query      = $query;
152 162
        $this->options    = $options;
153 162
        $this->dm         = $dm;
154 162
        $this->class      = $class;
155 162
        $this->hydrate    = $hydrate;
156 162
        $this->primers    = $primers;
157
158 162
        $this->setReadOnly($readOnly);
159 162
        $this->setRefresh($refresh);
160
161 162
        if (! isset($query['readPreference'])) {
162 156
            return;
163
        }
164
165 6
        $this->unitOfWorkHints[self::HINT_READ_PREFERENCE] = $query['readPreference'];
166 6
    }
167
168 64
    public function __clone()
169
    {
170 64
        $this->iterator = null;
171 64
    }
172
173
    /**
174
     * Return an array of information about the query structure for debugging.
175
     *
176
     * The $name parameter may be used to return a specific key from the
177
     * internal $query array property. If omitted, the entire array will be
178
     * returned.
179
     */
180 27
    public function debug(?string $name = null)
181
    {
182 27
        return $name !== null ? $this->query[$name] : $this->query;
183
    }
184
185
    /**
186
     * Execute the query and returns the results.
187
     *
188
     * @return Iterator|int|string|array
189
     *
190
     * @throws MongoDBException
191
     */
192 122
    public function execute()
193
    {
194 122
        $results = $this->runQuery();
195
196 122
        if (! $this->hydrate) {
197 9
            return $results;
198
        }
199
200 116
        if ($results instanceof Cursor) {
201
            $results = $this->makeIterator($results);
202
        }
203
204 116
        $uow = $this->dm->getUnitOfWork();
205
206
        /* If a single document is returned from a findAndModify command and it
207
         * includes the identifier field, attempt hydration.
208
         */
209 116
        if (($this->query['type'] === self::TYPE_FIND_AND_UPDATE ||
210 116
                $this->query['type'] === self::TYPE_FIND_AND_REMOVE) &&
211 116
            is_array($results) && isset($results['_id'])) {
212 5
            $results = $uow->getOrCreateDocument($this->class->name, $results, $this->unitOfWorkHints);
213
214 5
            if (! empty($this->primers)) {
215 1
                $referencePrimer = new ReferencePrimer($this->dm, $uow);
216
217 1
                foreach ($this->primers as $fieldName => $primer) {
218 1
                    $primer = is_callable($primer) ? $primer : null;
219 1
                    $referencePrimer->primeReferences($this->class, [$results], $fieldName, $this->unitOfWorkHints, $primer);
220
                }
221
            }
222
        }
223
224 116
        return $results;
225
    }
226
227
    /**
228
     * Gets the ClassMetadata instance.
229
     */
230
    public function getClass() : ClassMetadata
231
    {
232
        return $this->class;
233
    }
234
235
    public function getDocumentManager() : DocumentManager
236
    {
237
        return $this->dm;
238
    }
239
240
    /**
241
     * Execute the query and return its result, which must be an Iterator.
242
     *
243
     * If the query type is not expected to return an Iterator,
244
     * BadMethodCallException will be thrown before executing the query.
245
     * Otherwise, the query will be executed and UnexpectedValueException will
246
     * be thrown if {@link Query::execute()} does not return an Iterator.
247
     *
248
     * @see http://php.net/manual/en/iteratoraggregate.getiterator.php
249
     *
250
     * @throws BadMethodCallException If the query type would not return an Iterator.
251
     * @throws UnexpectedValueException If the query did not return an Iterator.
252
     * @throws MongoDBException
253
     */
254 84
    public function getIterator() : Iterator
255
    {
256 84
        switch ($this->query['type']) {
257 84
            case self::TYPE_FIND:
258 6
            case self::TYPE_GROUP:
259 6
            case self::TYPE_MAP_REDUCE:
260 6
            case self::TYPE_DISTINCT:
261 78
                break;
262
263
            default:
264 6
                throw new BadMethodCallException('Iterator would not be returned for query type: ' . $this->query['type']);
265
        }
266
267 78
        if ($this->iterator === null) {
268 78
            $this->iterator = $this->execute();
269
        }
270
271 78
        return $this->iterator;
272
    }
273
274
    /**
275
     * Return the query structure.
276
     */
277 14
    public function getQuery() : array
278
    {
279 14
        return $this->query;
280
    }
281
282
    /**
283
     * Execute the query and return the first result.
284
     *
285
     * @return array|object|null
286
     */
287 64
    public function getSingleResult()
288
    {
289 64
        $clonedQuery                 = clone $this;
290 64
        $clonedQuery->query['limit'] = 1;
291 64
        return $clonedQuery->getIterator()->current() ?: null;
0 ignored issues
show
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...
292
    }
293
294
    /**
295
     * Return the query type.
296
     */
297
    public function getType() : int
298
    {
299
        return $this->query['type'];
300
    }
301
302
    /**
303
     * Sets whether or not to hydrate the documents to objects.
304
     */
305
    public function setHydrate(bool $hydrate) : void
306
    {
307
        $this->hydrate = $hydrate;
308
    }
309
310
    /**
311
     * Set whether documents should be registered in UnitOfWork. If document would
312
     * already be managed it will be left intact and new instance returned.
313
     *
314
     * This option has no effect if hydration is disabled.
315
     */
316 162
    public function setReadOnly(bool $readOnly) : void
317
    {
318 162
        $this->unitOfWorkHints[self::HINT_READ_ONLY] = $readOnly;
319 162
    }
320
321
    /**
322
     * Set whether to refresh hydrated documents that are already in the
323
     * identity map.
324
     *
325
     * This option has no effect if hydration is disabled.
326
     */
327 162
    public function setRefresh(bool $refresh) : void
328
    {
329 162
        $this->unitOfWorkHints[self::HINT_REFRESH] = (bool) $refresh;
330 162
    }
331
332
    /**
333
     * Execute the query and return its results as an array.
334
     *
335
     * @see IteratorAggregate::toArray()
336
     */
337 11
    public function toArray() : array
338
    {
339 11
        return $this->getIterator()->toArray();
0 ignored issues
show
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...
340
    }
341
342
    /**
343
     * Returns an array containing the specified keys and their values from the
344
     * query array, provided they exist and are not null.
345
     */
346 121
    private function getQueryOptions(string ...$keys) : array
347
    {
348 121
        return array_filter(
349 121
            array_intersect_key($this->query, array_flip($keys)),
350
            static function ($value) {
351 88
                return $value !== null;
352 121
            }
353
        );
354
    }
355
356 106
    private function makeIterator(Cursor $cursor) : Iterator
357
    {
358 106
        if ($this->hydrate && $this->class) {
359 98
            $cursor = new HydratingIterator($cursor, $this->dm->getUnitOfWork(), $this->class, $this->unitOfWorkHints);
360
        }
361
362 106
        $cursor = new CachingIterator($cursor);
363
364 106
        if (! empty($this->primers)) {
365 20
            $referencePrimer = new ReferencePrimer($this->dm, $this->dm->getUnitOfWork());
366 20
            $cursor          = new PrimingIterator($cursor, $this->class, $referencePrimer, $this->primers, $this->unitOfWorkHints);
367
        }
368
369 106
        return $cursor;
370
    }
371
372
    /**
373
     * Returns an array with its keys renamed based on the translation map.
374
     *
375
     * @return array $rename Translation map (from => to) for renaming keys
376
     */
377 111
    private function renameQueryOptions(array $options, array $rename) : array
378
    {
379 111
        if (empty($options)) {
380 42
            return $options;
381
        }
382
383 86
        return array_combine(
384 86
            array_map(
385
                static function ($key) use ($rename) {
386 86
                    return $rename[$key] ?? $key;
387 86
                },
388 86
                array_keys($options)
389
            ),
390 86
            array_values($options)
391
        );
392
    }
393
394
    /**
395
     * Execute the query and return its result.
396
     *
397
     * The return value will vary based on the query type. Commands with results
398
     * (e.g. aggregate, inline mapReduce) may return an ArrayIterator. Other
399
     * commands and operations may return a status array or a boolean, depending
400
     * on the driver's write concern. Queries and some mapReduce commands will
401
     * return an Iterator.
402
     *
403
     * @return Iterator|string|int|array
404
     */
405 122
    public function runQuery()
406
    {
407 122
        $options = $this->options;
408
409 122
        switch ($this->query['type']) {
410 122
            case self::TYPE_FIND:
411 106
                $queryOptions = $this->getQueryOptions('select', 'sort', 'skip', 'limit', 'readPreference');
412 106
                $queryOptions = $this->renameQueryOptions($queryOptions, ['select' => 'projection']);
413
414 106
                $cursor = $this->collection->find(
415 106
                    $this->query['query'],
416 106
                    $queryOptions
417
                );
418
419 106
                return $this->makeIterator($cursor);
420
421 24
            case self::TYPE_FIND_AND_UPDATE:
422 6
                $queryOptions                   = $this->getQueryOptions('select', 'sort', 'upsert');
423 6
                $queryOptions                   = $this->renameQueryOptions($queryOptions, ['select' => 'projection']);
424 6
                $queryOptions['returnDocument'] = $this->query['new'] ?? false ? FindOneAndUpdate::RETURN_DOCUMENT_AFTER : FindOneAndUpdate::RETURN_DOCUMENT_BEFORE;
425
426 6
                return $this->collection->findOneAndUpdate(
427 6
                    $this->query['query'],
428 6
                    $this->query['newObj'],
429 6
                    array_merge($options, $queryOptions)
430
                );
431
432 19
            case self::TYPE_FIND_AND_REMOVE:
433 2
                $queryOptions = $this->getQueryOptions('select', 'sort');
434 2
                $queryOptions = $this->renameQueryOptions($queryOptions, ['select' => 'projection']);
435
436 2
                return $this->collection->findOneAndDelete(
437 2
                    $this->query['query'],
438 2
                    array_merge($options, $queryOptions)
439
                );
440
441 17
            case self::TYPE_INSERT:
442
                return $this->collection->insertOne($this->query['newObj'], $options);
443
444 17
            case self::TYPE_UPDATE:
445 12
                if ($this->query['multiple'] ?? false) {
446 2
                    return $this->collection->updateMany(
447 2
                        $this->query['query'],
448 2
                        $this->query['newObj'],
449 2
                        array_merge($options, $this->getQueryOptions('upsert'))
450
                    );
451
                }
452
453 10
                return $this->collection->updateOne(
454 10
                    $this->query['query'],
455 10
                    $this->query['newObj'],
456 10
                    array_merge($options, $this->getQueryOptions('upsert'))
457
                );
458
459 5
            case self::TYPE_REMOVE:
460 1
                return $this->collection->deleteMany($this->query['query'], $options);
461
462 4
            case self::TYPE_DISTINCT:
463 2
                $collection = $this->collection;
464 2
                $query      = $this->query;
465
466 2
                return $collection->distinct(
467 2
                    $query['distinct'],
468 2
                    $query['query'],
469 2
                    array_merge($options, $this->getQueryOptions('readPreference'))
470
                );
471
472 2
            case self::TYPE_COUNT:
473 2
                $collection = $this->collection;
474 2
                $query      = $this->query;
475
476 2
                return $collection->count(
477 2
                    $query['query'],
478 2
                    array_merge($options, $this->getQueryOptions('hint', 'limit', 'skip', 'readPreference'))
479
                );
480
        }
481
    }
482
}
483