Completed
Push — master ( 11f27c...4713a5 )
by Andreas
08:33
created

Query::setReadOnly()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 3
cts 3
cp 1
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 1
crap 1
1
<?php
2
/*
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license. For more information, see
17
 * <http://www.doctrine-project.org>.
18
 */
19
20
namespace Doctrine\ODM\MongoDB\Query;
21
22
use Doctrine\ODM\MongoDB\DocumentManager;
23
use Doctrine\ODM\MongoDB\Iterator\CachingIterator;
24
use Doctrine\ODM\MongoDB\Iterator\HydratingIterator;
25
use Doctrine\ODM\MongoDB\Iterator\Iterator;
26
use Doctrine\ODM\MongoDB\Iterator\PrimingIterator;
27
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
28
use MongoDB\Collection;
29
use MongoDB\Driver\Cursor;
30
use MongoDB\Operation\FindOneAndUpdate;
31
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
 * @since       1.0
38
 */
39
class Query implements \IteratorAggregate
40
{
41
    const TYPE_FIND            = 1;
42
    const TYPE_FIND_AND_UPDATE = 2;
43
    const TYPE_FIND_AND_REMOVE = 3;
44
    const TYPE_INSERT          = 4;
45
    const TYPE_UPDATE          = 5;
46
    const TYPE_REMOVE          = 6;
47
    const TYPE_GROUP           = 7;
48
    const TYPE_MAP_REDUCE      = 8;
49
    const TYPE_DISTINCT        = 9;
50
    const TYPE_COUNT           = 11;
51
52
    /**
53
     * @deprecated 1.1 Will be removed for 2.0
54
     */
55
    const TYPE_GEO_LOCATION = 10;
56
57
    const HINT_REFRESH = 1;
58
    /** @deprecated */
59
    const HINT_SLAVE_OKAY = 2;
60
    const HINT_READ_PREFERENCE = 3;
61
    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 boolean
81
     */
82
    private $hydrate = true;
83
84
    /**
85
     * Array of primer Closure instances.
86
     *
87
     * @var array
88
     */
89
    private $primers = array();
90
91
    /**
92
     * Hints for UnitOfWork behavior.
93
     *
94
     * @var array
95
     */
96
    private $unitOfWorkHints = array();
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
    /**
113
     * @var Iterator
114
     */
115
    private $iterator;
116
117
    /**
118
     * Query options
119
     *
120
     * @var array
121
     */
122
    private $options;
123
124
    /**
125
     * Constructor.
126
     *
127
     * Please note that $requireIndexes was deprecated in 1.2 and will be removed in 2.0
128
     *
129
     * @param DocumentManager $dm
130
     * @param ClassMetadata $class
131
     * @param Collection $collection
132
     * @param array $query
133
     * @param array $options
134
     * @param boolean $hydrate
135
     * @param boolean $refresh
136
     * @param array $primers
137
     * @param boolean $readOnly
138
     */
139 159
    public function __construct(DocumentManager $dm, ClassMetadata $class, Collection $collection, array $query = array(), array $options = array(), $hydrate = true, $refresh = false, array $primers = array(), $readOnly = false)
140
    {
141 159
        $primers = array_filter($primers);
142
143 159
        if ( ! empty($primers)) {
144 22
            $query['eagerCursor'] = true;
145
        }
146
147 159
        if ( ! empty($query['eagerCursor'])) {
148 23
            $query['useIdentifierKeys'] = false;
149
        }
150
151 159
        switch ($query['type']) {
152 159
            case self::TYPE_FIND:
153 36
            case self::TYPE_FIND_AND_UPDATE:
154 24
            case self::TYPE_FIND_AND_REMOVE:
155 21
            case self::TYPE_INSERT:
156 20
            case self::TYPE_UPDATE:
157 8
            case self::TYPE_REMOVE:
158 6
            case self::TYPE_GROUP:
159 6
            case self::TYPE_MAP_REDUCE:
160 6
            case self::TYPE_DISTINCT:
161 4
            case self::TYPE_COUNT:
162 158
                break;
163
164
            default:
165 1
                throw new \InvalidArgumentException('Invalid query type: ' . $query['type']);
166
        }
167
168 158
        $this->collection = $collection;
169 158
        $this->query      = $query;
170 158
        $this->options    = $options;
171 158
        $this->dm = $dm;
172 158
        $this->class = $class;
173 158
        $this->hydrate = $hydrate;
174 158
        $this->primers = $primers;
175
176 158
        $this->setReadOnly($readOnly);
177 158
        $this->setRefresh($refresh);
178
179 158
        if (isset($query['slaveOkay'])) {
180
            $this->unitOfWorkHints[self::HINT_SLAVE_OKAY] = $query['slaveOkay'];
0 ignored issues
show
Deprecated Code introduced by
The constant Doctrine\ODM\MongoDB\Query\Query::HINT_SLAVE_OKAY has been deprecated.

This class constant has been deprecated.

Loading history...
181
        }
182
183 158
        if (isset($query['readPreference'])) {
184 6
            $this->unitOfWorkHints[self::HINT_READ_PREFERENCE] = $query['readPreference'];
185
        }
186 158
    }
187
188 64
    public function __clone()
189
    {
190 64
        $this->iterator = null;
191 64
    }
192
193
    /**
194
     * Return an array of information about the query structure for debugging.
195
     *
196
     * The $name parameter may be used to return a specific key from the
197
     * internal $query array property. If omitted, the entire array will be
198
     * returned.
199
     *
200
     * @param string $name
201
     * @return mixed
202
     */
203 26
    public function debug($name = null)
204
    {
205 26
        return $name !== null ? $this->query[$name] : $this->query;
206
    }
207
208
    /**
209
     * Execute the query and returns the results.
210
     *
211
     * @throws \Doctrine\ODM\MongoDB\MongoDBException
212
     * @return Iterator|int|string|array
213
     */
214 119
    public function execute()
215
    {
216 119
        $results = $this->runQuery();
217
218 119
        if ( ! $this->hydrate) {
219 9
            return $results;
220
        }
221
222 113
        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...
223
            $results = $this->makeIterator($results);
224
        }
225
226 113
        $uow = $this->dm->getUnitOfWork();
227
228
        /* If a single document is returned from a findAndModify command and it
229
         * includes the identifier field, attempt hydration.
230
         */
231 113
        if (($this->query['type'] === self::TYPE_FIND_AND_UPDATE ||
232 113
                $this->query['type'] === self::TYPE_FIND_AND_REMOVE) &&
233 113
            is_array($results) && isset($results['_id'])) {
234
235 5
            $results = $uow->getOrCreateDocument($this->class->name, $results, $this->unitOfWorkHints);
236
237 5
            if ( ! empty($this->primers)) {
238 1
                $referencePrimer = new ReferencePrimer($this->dm, $uow);
239
240 1
                foreach ($this->primers as $fieldName => $primer) {
241 1
                    $primer = is_callable($primer) ? $primer : null;
242 1
                    $referencePrimer->primeReferences($this->class, array($results), $fieldName, $this->unitOfWorkHints, $primer);
243
                }
244
            }
245
        }
246
247 113
        return $results;
248
    }
249
250
    /**
251
     * Gets the ClassMetadata instance.
252
     *
253
     * @return ClassMetadata $class
254
     */
255
    public function getClass()
256
    {
257
        return $this->class;
258
    }
259
260
    /**
261
     * Gets the DocumentManager instance.
262
     *
263
     * @return DocumentManager $dm
264
     */
265
    public function getDocumentManager()
266
    {
267
        return $this->dm;
268
    }
269
270
    /**
271
     * Execute the query and return its result, which must be an Iterator.
272
     *
273
     * If the query type is not expected to return an Iterator,
274
     * BadMethodCallException will be thrown before executing the query.
275
     * Otherwise, the query will be executed and UnexpectedValueException will
276
     * be thrown if {@link Query::execute()} does not return an Iterator.
277
     *
278
     * @see http://php.net/manual/en/iteratoraggregate.getiterator.php
279
     * @return Iterator
280
     * @throws \BadMethodCallException if the query type would not return an Iterator
281
     * @throws \UnexpectedValueException if the query did not return an Iterator
282
     */
283 84
    public function getIterator()
284
    {
285 84
        switch ($this->query['type']) {
286 84
            case self::TYPE_FIND:
287 6
            case self::TYPE_GROUP:
288 6
            case self::TYPE_MAP_REDUCE:
289 6
            case self::TYPE_DISTINCT:
290 78
                break;
291
292
            default:
293 6
                throw new \BadMethodCallException('Iterator would not be returned for query type: ' . $this->query['type']);
294
        }
295
296 78
        if ($this->iterator === null) {
297 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...
298
        }
299
300 78
        return $this->iterator;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->iterator; (null|array|object|integer) is incompatible with the return type declared by the interface IteratorAggregate::getIterator of type Traversable.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

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

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
301
    }
302
303
    /**
304
     * Return the query structure.
305
     *
306
     * @return array
307
     */
308 14
    public function getQuery()
309
    {
310 14
        return $this->query;
311
    }
312
313
    /**
314
     * Execute the query and return the first result.
315
     *
316
     * @return array|object|null
317
     */
318 64
    public function getSingleResult()
319
    {
320 64
        $clonedQuery = clone $this;
321 64
        $clonedQuery->query['limit'] = 1;
322 64
        return $clonedQuery->getIterator()->current() ?: null;
323
    }
324
325
    /**
326
     * Return the query type.
327
     *
328
     * @return integer
329
     */
330
    public function getType()
331
    {
332
        return $this->query['type'];
333
    }
334
335
    /**
336
     * Sets whether or not to hydrate the documents to objects.
337
     *
338
     * @param boolean $hydrate
339
     */
340
    public function setHydrate($hydrate)
341
    {
342
        $this->hydrate = (boolean) $hydrate;
343
    }
344
345
    /**
346
     * Set whether documents should be registered in UnitOfWork. If document would
347
     * already be managed it will be left intact and new instance returned.
348
     * 
349
     * This option has no effect if hydration is disabled.
350
     * 
351
     * @param boolean $readOnly
352
     */
353 158
    public function setReadOnly($readOnly)
354
    {
355 158
        $this->unitOfWorkHints[Query::HINT_READ_ONLY] = (boolean) $readOnly;
356 158
    }
357
358
    /**
359
     * Set whether to refresh hydrated documents that are already in the
360
     * identity map.
361
     *
362
     * This option has no effect if hydration is disabled.
363
     *
364
     * @param boolean $refresh
365
     */
366 158
    public function setRefresh($refresh)
367
    {
368 158
        $this->unitOfWorkHints[Query::HINT_REFRESH] = (boolean) $refresh;
369 158
    }
370
371
    /**
372
     * Execute the query and return its results as an array.
373
     *
374
     * @see IteratorAggregate::toArray()
375
     * @return array
376
     */
377 11
    public function toArray()
378
    {
379 11
        return $this->getIterator()->toArray();
380
    }
381
382
    /**
383
     * Returns an array containing the specified keys and their values from the
384
     * query array, provided they exist and are not null.
385
     *
386
     * @param string $key,... One or more option keys to be read
0 ignored issues
show
Bug introduced by
There is no parameter named $key,.... Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
387
     * @return array
388
     */
389 118
    private function getQueryOptions(/* $key, ... */)
390
    {
391 118
        return array_filter(
392 118
            array_intersect_key($this->query, array_flip(func_get_args())),
393
            function($value) { return $value !== null; }
394
        );
395
    }
396
397 105
    private function makeIterator(Cursor $cursor): Iterator
398
    {
399 105
        if ($this->hydrate && $this->class) {
400 97
            $cursor = new HydratingIterator($cursor, $this->dm->getUnitOfWork(), $this->class, $this->unitOfWorkHints);
401
        }
402
403 105
        $cursor = new CachingIterator($cursor);
404
405 105
        if ( ! empty($this->primers)) {
406 20
            $referencePrimer = new ReferencePrimer($this->dm, $this->dm->getUnitOfWork());
407 20
            $cursor = new PrimingIterator($cursor, $this->class, $referencePrimer, $this->primers, $this->unitOfWorkHints);
408
        }
409
410 105
        return $cursor;
411
    }
412
413
    /**
414
     * Returns an array with its keys renamed based on the translation map.
415
     *
416
     * @param array $options Query options
417
     * @return array $rename Translation map (from => to) for renaming keys
418
     */
419 110
    private function renameQueryOptions(array $options, array $rename)
420
    {
421 110
        if (empty($options)) {
422 41
            return $options;
423
        }
424
425 86
        return array_combine(
426 86
            array_map(
427
                function($key) use ($rename) { return isset($rename[$key]) ? $rename[$key] : $key; },
428 86
                array_keys($options)
429
            ),
430 86
            array_values($options)
431
        );
432
    }
433
434
    /**
435
     * Execute the query and return its result.
436
     *
437
     * The return value will vary based on the query type. Commands with results
438
     * (e.g. aggregate, inline mapReduce) may return an ArrayIterator. Other
439
     * commands and operations may return a status array or a boolean, depending
440
     * on the driver's write concern. Queries and some mapReduce commands will
441
     * return an Iterator.
442
     *
443
     * @return Iterator|string|int|array
444
     */
445 119
    public function runQuery()
446
    {
447 119
        $options = $this->options;
448
449 119
        switch ($this->query['type']) {
450 119
            case self::TYPE_FIND:
451 105
                $queryOptions = $this->getQueryOptions('select', 'sort', 'skip', 'limit', 'readPreference');
452 105
                $queryOptions = $this->renameQueryOptions($queryOptions, ['select' => 'projection']);
453
454 105
                $cursor = $this->collection->find(
455 105
                    $this->query['query'],
456 105
                    $queryOptions
457
                );
458
459 105
                return $this->makeIterator($cursor);
460
461 22
            case self::TYPE_FIND_AND_UPDATE:
462 6
                $queryOptions = $this->getQueryOptions('select', 'sort', 'upsert');
463 6
                $queryOptions = $this->renameQueryOptions($queryOptions, ['select' => 'projection']);
464 6
                $queryOptions['returnDocument'] = ($this->query['new'] ?? false) ? FindOneAndUpdate::RETURN_DOCUMENT_AFTER : FindOneAndUpdate::RETURN_DOCUMENT_BEFORE;
465
466 6
                return $this->collection->findOneAndUpdate(
467 6
                    $this->query['query'],
468 6
                    $this->query['newObj'],
469 6
                    array_merge($options, $queryOptions)
470
                );
471
472 17
            case self::TYPE_FIND_AND_REMOVE:
473 2
                $queryOptions = $this->getQueryOptions('select', 'sort');
474 2
                $queryOptions = $this->renameQueryOptions($queryOptions, ['select' => 'projection']);
475
476 2
                return $this->collection->findOneAndDelete(
477 2
                    $this->query['query'],
478 2
                    array_merge($options, $queryOptions)
479
                );
480
481 15
            case self::TYPE_INSERT:
482
                return $this->collection->insertOne($this->query['newObj'], $options);
483
484 15
            case self::TYPE_UPDATE:
485 10
                if ($this->query['multiple'] ?? false) {
486 2
                    return $this->collection->updateMany(
487 2
                        $this->query['query'],
488 2
                        $this->query['newObj'],
489 2
                        array_merge($options, $this->getQueryOptions('upsert'))
490
                    );
491
                } else {
492 8
                    return $this->collection->updateOne(
493 8
                        $this->query['query'],
494 8
                        $this->query['newObj'],
495 8
                        array_merge($options, $this->getQueryOptions('upsert'))
496
                    );
497
                }
498
499 5
            case self::TYPE_REMOVE:
500 1
                return $this->collection->deleteMany($this->query['query'], $options);
501
502 4 View Code Duplication
            case self::TYPE_DISTINCT:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
503 2
                $collection = $this->collection;
504 2
                $query = $this->query;
505
506 2
                return $collection->distinct(
507 2
                    $query['distinct'],
508 2
                    $query['query'],
509 2
                    array_merge($options, $this->getQueryOptions('readPreference'))
510
                );
511
512 2 View Code Duplication
            case self::TYPE_COUNT:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
513 2
                $collection = $this->collection;
514 2
                $query = $this->query;
515
516 2
                return $collection->count(
517 2
                    $query['query'],
518 2
                    array_merge($options, $this->getQueryOptions('hint', 'limit', 'skip', 'readPreference'))
519
                );
520
        }
521
    }
522
}
523