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