Completed
Push — master ( a8fe50...bce26f )
by Maciej
13s
created

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

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 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 */
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 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)
121
    {
122 163
        $primers = array_filter($primers);
123
124 163
        if (! empty($primers)) {
125 22
            $query['eagerCursor'] = true;
126
        }
127
128 163
        if (! empty($query['eagerCursor'])) {
129 23
            $query['useIdentifierKeys'] = false;
130
        }
131
132 163
        switch ($query['type']) {
133 163
            case self::TYPE_FIND:
134 38
            case self::TYPE_FIND_AND_UPDATE:
135 26
            case self::TYPE_FIND_AND_REMOVE:
136 23
            case self::TYPE_INSERT:
137 22
            case self::TYPE_UPDATE:
138 8
            case self::TYPE_REMOVE:
139 6
            case self::TYPE_GROUP:
140 6
            case self::TYPE_MAP_REDUCE:
141 6
            case self::TYPE_DISTINCT:
142 4
            case self::TYPE_COUNT:
143 162
                break;
144
145
            default:
146 1
                throw new \InvalidArgumentException('Invalid query type: ' . $query['type']);
147
        }
148
149 162
        $this->collection = $collection;
150 162
        $this->query      = $query;
151 162
        $this->options    = $options;
152 162
        $this->dm = $dm;
153 162
        $this->class = $class;
154 162
        $this->hydrate = $hydrate;
155 162
        $this->primers = $primers;
156
157 162
        $this->setReadOnly($readOnly);
158 162
        $this->setRefresh($refresh);
159
160 162
        if (! isset($query['readPreference'])) {
161 156
            return;
162
        }
163
164 6
        $this->unitOfWorkHints[self::HINT_READ_PREFERENCE] = $query['readPreference'];
165 6
    }
166
167 64
    public function __clone()
168
    {
169 64
        $this->iterator = null;
170 64
    }
171
172
    /**
173
     * Return an array of information about the query structure for debugging.
174
     *
175
     * The $name parameter may be used to return a specific key from the
176
     * internal $query array property. If omitted, the entire array will be
177
     * returned.
178
     */
179 27
    public function debug(?string $name = null)
180
    {
181 27
        return $name !== null ? $this->query[$name] : $this->query;
182
    }
183
184
    /**
185
     * Execute the query and returns the results.
186
     *
187
     * @throws MongoDBException
188
     * @return Iterator|int|string|array
189
     */
190 122
    public function execute()
191
    {
192 122
        $results = $this->runQuery();
193
194 122
        if (! $this->hydrate) {
195 9
            return $results;
196
        }
197
198 116
        if ($results instanceof Cursor) {
199
            $results = $this->makeIterator($results);
200
        }
201
202 116
        $uow = $this->dm->getUnitOfWork();
203
204
        /* If a single document is returned from a findAndModify command and it
205
         * includes the identifier field, attempt hydration.
206
         */
207 116
        if (($this->query['type'] === self::TYPE_FIND_AND_UPDATE ||
208 116
                $this->query['type'] === self::TYPE_FIND_AND_REMOVE) &&
209 116
            is_array($results) && isset($results['_id'])) {
210 5
            $results = $uow->getOrCreateDocument($this->class->name, $results, $this->unitOfWorkHints);
211
212 5
            if (! empty($this->primers)) {
213 1
                $referencePrimer = new ReferencePrimer($this->dm, $uow);
214
215 1
                foreach ($this->primers as $fieldName => $primer) {
216 1
                    $primer = is_callable($primer) ? $primer : null;
217 1
                    $referencePrimer->primeReferences($this->class, [$results], $fieldName, $this->unitOfWorkHints, $primer);
218
                }
219
            }
220
        }
221
222 116
        return $results;
223
    }
224
225
    /**
226
     * Gets the ClassMetadata instance.
227
     */
228
    public function getClass(): ClassMetadata
229
    {
230
        return $this->class;
231
    }
232
233
    public function getDocumentManager(): DocumentManager
234
    {
235
        return $this->dm;
236
    }
237
238
    /**
239
     * Execute the query and return its result, which must be an Iterator.
240
     *
241
     * If the query type is not expected to return an Iterator,
242
     * BadMethodCallException will be thrown before executing the query.
243
     * Otherwise, the query will be executed and UnexpectedValueException will
244
     * be thrown if {@link Query::execute()} does not return an Iterator.
245
     *
246
     * @see http://php.net/manual/en/iteratoraggregate.getiterator.php
247
     * @throws \BadMethodCallException If the query type would not return an Iterator.
248
     * @throws \UnexpectedValueException If the query did not return an Iterator.
249
     * @throws MongoDBException
250
     */
251 84
    public function getIterator(): Iterator
252
    {
253 84
        switch ($this->query['type']) {
254 84
            case self::TYPE_FIND:
255 6
            case self::TYPE_GROUP:
256 6
            case self::TYPE_MAP_REDUCE:
257 6
            case self::TYPE_DISTINCT:
258 78
                break;
259
260
            default:
261 6
                throw new \BadMethodCallException('Iterator would not be returned for query type: ' . $this->query['type']);
262
        }
263
264 78
        if ($this->iterator === null) {
265 78
            $this->iterator = $this->execute();
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->execute() can also be of type array or integer. However, the property $iterator is declared as type object<Doctrine\ODM\MongoDB\Iterator\Iterator>. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
266
        }
267
268 78
        return $this->iterator;
269
    }
270
271
    /**
272
     * Return the query structure.
273
     */
274 14
    public function getQuery(): array
275
    {
276 14
        return $this->query;
277
    }
278
279
    /**
280
     * Execute the query and return the first result.
281
     *
282
     * @return array|object|null
283
     */
284 64
    public function getSingleResult()
285
    {
286 64
        $clonedQuery = clone $this;
287 64
        $clonedQuery->query['limit'] = 1;
288 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\Common\Collections\AbstractLazyCollection, Doctrine\Common\Collections\ArrayCollection, Doctrine\ODM\MongoDB\Iterator\CachingIterator, Doctrine\ODM\MongoDB\Iterator\HydratingIterator, Doctrine\ODM\MongoDB\Iterator\PrimingIterator, Doctrine\ODM\MongoDB\PersistentCollection, Doctrine\ODM\MongoDB\Tools\Console\MetadataFilter, EmptyIterator, FilesystemIterator, FilterIterator, Generator, GlobIterator, HttpMessage, HttpRequestPool, Imagick, ImagickPixelIterator, InfiniteIterator, Issue523, IteratorIterator, LimitIterator, MongoCommandCursor, MongoCursor, MongoDB\ChangeStream, MongoDB\Model\BSONIterator, MongoDB\Model\CachingIterator, MongoDB\Model\CollectionInfoCommandIterator, MongoDB\Model\DatabaseInfoLegacyIterator, MongoDB\Model\IndexInfoIteratorIterator, MongoDB\Model\TypeMapArrayIterator, MongoGridFSCursor, MultipleIterator, Nette\Iterators\CachingIterator, Nette\Iterators\Mapper, NoRewindIterator, PHPUnit\Framework\TestSuiteIterator, PHPUnit\Runner\Filter\ExcludeGroupFilterIterator, PHPUnit\Runner\Filter\GroupFilterIterator, PHPUnit\Runner\Filter\IncludeGroupFilterIterator, PHPUnit\Runner\Filter\NameFilterIterator, PHP_CodeSniffer\Files\FileList, PHP_CodeSniffer\Filters\ExactMatch, PHP_CodeSniffer\Filters\Filter, PHP_CodeSniffer\Filters\GitModified, PHP_Token_Stream, ParentIterator, Phar, PharData, PharIo\Manifest\AuthorCollectionIterator, PharIo\Manifest\AuthorElementCollection, PharIo\Manifest\BundledComponentCollectionIterator, PharIo\Manifest\ComponentElementCollection, PharIo\Manifest\ElementCollection, PharIo\Manifest\ExtElementCollection, PharIo\Manifest\RequirementCollectionIterator, RecursiveArrayIterator, RecursiveCachingIterator, RecursiveCallbackFilterIterator, RecursiveDirectoryIterator, RecursiveFilterIterator, RecursiveIteratorIterator, RecursiveRegexIterator, RecursiveTreeIterator, RegexIterator, SQLiteResult, SebastianBergmann\CodeCoverage\Node\Iterator, SebastianBergmann\FileIterator\Iterator, SimpleXMLIterator, SplDoublyLinkedList, SplFileObject, SplFixedArray, SplHeap, SplMaxHeap, SplMinHeap, SplObjectStorage, SplPriorityQueue, SplQueue, SplStack, SplTempFileObject, Symfony\Component\Finder...or\CustomFilterIterator, Symfony\Component\Finder...DateRangeFilterIterator, Symfony\Component\Finder...epthRangeFilterIterator, Symfony\Component\Finder...DirectoryFilterIterator, Symfony\Component\Finder...\FileTypeFilterIterator, Symfony\Component\Finder...lecontentFilterIterator, Symfony\Component\Finder...\FilenameFilterIterator, Symfony\Component\Finder...tiplePcreFilterIterator, Symfony\Component\Finder...ator\PathFilterIterator, Symfony\Component\Finder...ursiveDirectoryIterator, Symfony\Component\Finder...SizeRangeFilterIterator, Symfony\Component\Finder...rator\InnerNameIterator, Symfony\Component\Finder...rator\InnerSizeIterator, Symfony\Component\Finder...rator\InnerTypeIterator, Symfony\Component\Finder\Tests\Iterator\Iterator, Symfony\Component\Finder...or\MockFileListIterator, Symfony\Component\Finder...tiplePcreFilterIterator, TestIterator, TestIterator2, TheSeer\Tokenizer\TokenCollection.

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...
289
    }
290
291
    /**
292
     * Return the query type.
293
     */
294
    public function getType(): int
295
    {
296
        return $this->query['type'];
297
    }
298
299
    /**
300
     * Sets whether or not to hydrate the documents to objects.
301
     */
302
    public function setHydrate(bool $hydrate): void
303
    {
304
        $this->hydrate = $hydrate;
305
    }
306
307
    /**
308
     * Set whether documents should be registered in UnitOfWork. If document would
309
     * already be managed it will be left intact and new instance returned.
310
     *
311
     * This option has no effect if hydration is disabled.
312
     */
313 162
    public function setReadOnly(bool $readOnly): void
314
    {
315 162
        $this->unitOfWorkHints[self::HINT_READ_ONLY] = $readOnly;
316 162
    }
317
318
    /**
319
     * Set whether to refresh hydrated documents that are already in the
320
     * identity map.
321
     *
322
     * This option has no effect if hydration is disabled.
323
     */
324 162
    public function setRefresh(bool $refresh): void
325
    {
326 162
        $this->unitOfWorkHints[self::HINT_REFRESH] = (bool) $refresh;
327 162
    }
328
329
    /**
330
     * Execute the query and return its results as an array.
331
     *
332
     * @see IteratorAggregate::toArray()
333
     */
334 11
    public function toArray(): array
335
    {
336 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\Common\Collections\AbstractLazyCollection, Doctrine\Common\Collections\ArrayCollection, Doctrine\ODM\MongoDB\Iterator\CachingIterator, Doctrine\ODM\MongoDB\Iterator\PrimingIterator, Doctrine\ODM\MongoDB\PersistentCollection, 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...
337
    }
338
339
    /**
340
     * Returns an array containing the specified keys and their values from the
341
     * query array, provided they exist and are not null.
342
     */
343 121
    private function getQueryOptions(string ...$keys): array
344
    {
345 121
        return array_filter(
346 121
            array_intersect_key($this->query, array_flip($keys)),
347
            function ($value) {
348 88
                return $value !== null;
349 121
            }
350
        );
351
    }
352
353 106
    private function makeIterator(Cursor $cursor): Iterator
354
    {
355 106
        if ($this->hydrate && $this->class) {
356 98
            $cursor = new HydratingIterator($cursor, $this->dm->getUnitOfWork(), $this->class, $this->unitOfWorkHints);
357
        }
358
359 106
        $cursor = new CachingIterator($cursor);
360
361 106
        if (! empty($this->primers)) {
362 20
            $referencePrimer = new ReferencePrimer($this->dm, $this->dm->getUnitOfWork());
363 20
            $cursor = new PrimingIterator($cursor, $this->class, $referencePrimer, $this->primers, $this->unitOfWorkHints);
364
        }
365
366 106
        return $cursor;
367
    }
368
369
    /**
370
     * Returns an array with its keys renamed based on the translation map.
371
     *
372
     * @return array $rename Translation map (from => to) for renaming keys
373
     */
374 111
    private function renameQueryOptions(array $options, array $rename): array
375
    {
376 111
        if (empty($options)) {
377 42
            return $options;
378
        }
379
380 86
        return array_combine(
381 86
            array_map(
382
                function ($key) use ($rename) {
383 86
                    return $rename[$key] ?? $key;
384 86
                },
385 86
                array_keys($options)
386
            ),
387 86
            array_values($options)
388
        );
389
    }
390
391
    /**
392
     * Execute the query and return its result.
393
     *
394
     * The return value will vary based on the query type. Commands with results
395
     * (e.g. aggregate, inline mapReduce) may return an ArrayIterator. Other
396
     * commands and operations may return a status array or a boolean, depending
397
     * on the driver's write concern. Queries and some mapReduce commands will
398
     * return an Iterator.
399
     *
400
     * @return Iterator|string|int|array
401
     */
402 122
    public function runQuery()
403
    {
404 122
        $options = $this->options;
405
406 122
        switch ($this->query['type']) {
407 122
            case self::TYPE_FIND:
408 106
                $queryOptions = $this->getQueryOptions('select', 'sort', 'skip', 'limit', 'readPreference');
409 106
                $queryOptions = $this->renameQueryOptions($queryOptions, ['select' => 'projection']);
410
411 106
                $cursor = $this->collection->find(
412 106
                    $this->query['query'],
413 106
                    $queryOptions
414
                );
415
416 106
                return $this->makeIterator($cursor);
417
418 24
            case self::TYPE_FIND_AND_UPDATE:
419 6
                $queryOptions = $this->getQueryOptions('select', 'sort', 'upsert');
420 6
                $queryOptions = $this->renameQueryOptions($queryOptions, ['select' => 'projection']);
421 6
                $queryOptions['returnDocument'] = ($this->query['new'] ?? false) ? FindOneAndUpdate::RETURN_DOCUMENT_AFTER : FindOneAndUpdate::RETURN_DOCUMENT_BEFORE;
422
423 6
                return $this->collection->findOneAndUpdate(
424 6
                    $this->query['query'],
425 6
                    $this->query['newObj'],
426 6
                    array_merge($options, $queryOptions)
427
                );
428
429 19
            case self::TYPE_FIND_AND_REMOVE:
430 2
                $queryOptions = $this->getQueryOptions('select', 'sort');
431 2
                $queryOptions = $this->renameQueryOptions($queryOptions, ['select' => 'projection']);
432
433 2
                return $this->collection->findOneAndDelete(
434 2
                    $this->query['query'],
435 2
                    array_merge($options, $queryOptions)
436
                );
437
438 17
            case self::TYPE_INSERT:
439
                return $this->collection->insertOne($this->query['newObj'], $options);
440
441 17
            case self::TYPE_UPDATE:
442 12
                if ($this->query['multiple'] ?? false) {
443 2
                    return $this->collection->updateMany(
444 2
                        $this->query['query'],
445 2
                        $this->query['newObj'],
446 2
                        array_merge($options, $this->getQueryOptions('upsert'))
447
                    );
448
                }
449
450 10
                return $this->collection->updateOne(
451 10
                    $this->query['query'],
452 10
                    $this->query['newObj'],
453 10
                    array_merge($options, $this->getQueryOptions('upsert'))
454
                );
455
456 5
            case self::TYPE_REMOVE:
457 1
                return $this->collection->deleteMany($this->query['query'], $options);
458
459 4
            case self::TYPE_DISTINCT:
460 2
                $collection = $this->collection;
461 2
                $query = $this->query;
462
463 2
                return $collection->distinct(
464 2
                    $query['distinct'],
465 2
                    $query['query'],
466 2
                    array_merge($options, $this->getQueryOptions('readPreference'))
467
                );
468
469 2
            case self::TYPE_COUNT:
470 2
                $collection = $this->collection;
471 2
                $query = $this->query;
472
473 2
                return $collection->count(
0 ignored issues
show
Deprecated Code introduced by
The method MongoDB\Collection::count() has been deprecated with message: 1.4

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
474 2
                    $query['query'],
475 2
                    array_merge($options, $this->getQueryOptions('hint', 'limit', 'skip', 'readPreference'))
476
                );
477
        }
478
    }
479
}
480