Completed
Pull Request — master (#1803)
by Andreas
15:44
created

Query::getDocumentManager()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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