Completed
Push — master ( d6bb56...1beca3 )
by Maciej
9s
created

Builder::readOnly()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 5
ccs 3
cts 3
cp 1
rs 9.4285
cc 1
eloc 3
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\Hydrator;
24
use Doctrine\ODM\MongoDB\Mapping\ClassMetadataInfo;
25
26
/**
27
 * Query builder for ODM.
28
 *
29
 * @since       1.0
30
 */
31
class Builder extends \Doctrine\MongoDB\Query\Builder
32
{
33
    /**
34
     * The DocumentManager instance for this query
35
     *
36
     * @var DocumentManager
37
     */
38
    private $dm;
39
40
    /**
41
     * The ClassMetadata instance.
42
     *
43
     * @var \Doctrine\ODM\MongoDB\Mapping\ClassMetadata
44
     */
45
    private $class;
46
47
    /**
48
     * The current field we are operating on.
49
     *
50
     * @todo Change this to private once ODM requires doctrine/mongodb 1.1+
51
     * @var string
52
     */
53
    protected $currentField;
54
55
    /**
56
     * Whether or not to hydrate the data to documents.
57
     *
58
     * @var boolean
59
     */
60
    private $hydrate = true;
61
62
    /**
63
     * Whether or not to refresh the data for documents that are already in the identity map.
64
     *
65
     * @var boolean
66
     */
67
    private $refresh = false;
68
69
    /**
70
     * Array of primer Closure instances.
71
     *
72
     * @var array
73
     */
74
    private $primers = array();
75
76
    /**
77
     * Whether or not to require indexes.
78
     *
79
     * @var bool
80
     */
81
    private $requireIndexes;
82
83
    /**
84
     * Whether or not to register documents in UnitOfWork.
85
     *
86
     * @var bool
87
     */
88
    private $readOnly;
89
90
    /**
91
     * Construct a Builder
92
     *
93
     * @param DocumentManager $dm
94
     * @param string[]|string|null $documentName (optional) an array of document names, the document name, or none
95
     */
96 221
    public function __construct(DocumentManager $dm, $documentName = null)
97
    {
98 221
        $this->dm = $dm;
99 221
        $this->expr = new Expr($dm);
100 221
        if ($documentName !== null) {
101 213
            $this->setDocumentName($documentName);
102
        }
103 220
    }
104
105
    /**
106
     * Set whether or not to require indexes.
107
     *
108
     * @param bool $requireIndexes
109
     * @return $this
110
     */
111 2
    public function requireIndexes($requireIndexes = true)
112
    {
113 2
        $this->requireIndexes = $requireIndexes;
114 2
        return $this;
115
    }
116
117
    /**
118
     * Set the current field to operate on.
119
     *
120
     * @param string $field
121
     * @return $this
122
     */
123 141
    public function field($field)
124
    {
125 141
        $this->currentField = $field;
126 141
        parent::field($field);
127
128 141
        return $this;
129
    }
130
131
    /**
132
     * Use a primer to eagerly load all references in the current field.
133
     *
134
     * If $primer is true or a callable is provided, referenced documents for
135
     * this field will loaded into UnitOfWork immediately after the query is
136
     * executed. This will avoid multiple queries due to lazy initialization of
137
     * Proxy objects.
138
     *
139
     * If $primer is false, no priming will take place. That is also the default
140
     * behavior.
141
     *
142
     * If a custom callable is used, its signature should conform to the default
143
     * Closure defined in {@link ReferencePrimer::__construct()}.
144
     *
145
     * @param boolean|callable $primer
146
     * @return $this
147
     * @throws \InvalidArgumentException If $primer is not boolean or callable
148
     */
149 22
    public function prime($primer = true)
150
    {
151 22
        if ( ! is_bool($primer) && ! is_callable($primer)) {
152 1
            throw new \InvalidArgumentException('$primer is not a boolean or callable');
153
        }
154
155 21
        if ($primer === false) {
156 1
            unset($this->primers[$this->currentField]);
157
158 1
            return $this;
159
        }
160
161 21
        if (array_key_exists('eagerCursor', $this->query) && !$this->query['eagerCursor']) {
162 1
            throw new \BadMethodCallException("Can't call prime() when setting eagerCursor to false");
163
        }
164
165 20
        $this->primers[$this->currentField] = $primer;
166 20
        return $this;
167
    }
168
169
    /**
170
     * {@inheritdoc}
171
     */
172 5
    public function eagerCursor($bool = true)
173
    {
174 5
        if ( ! $bool && ! empty($this->primers)) {
175 1
            throw new \BadMethodCallException("Can't set eagerCursor to false when using reference primers");
176
        }
177
178 4
        return parent::eagerCursor($bool);
179
    }
180
181
182
    /**
183
     * @param bool $bool
184
     * @return $this
185
     */
186 16
    public function hydrate($bool = true)
187
    {
188 16
        $this->hydrate = $bool;
189 16
        return $this;
190
    }
191
192
    /**
193
     * @param bool $bool
194
     * @return $this
195
     */
196 1
    public function readOnly($bool = true)
197
    {
198 1
        $this->readOnly = $bool;
199 1
        return $this;
200
    }
201
202
    /**
203
     * @param bool $bool
204
     * @return $this
205
     */
206 5
    public function refresh($bool = true)
207
    {
208 5
        $this->refresh = $bool;
209 5
        return $this;
210
    }
211
212
    /**
213
     * Change the query type to find and optionally set and change the class being queried.
214
     *
215
     * @param string $documentName
216
     * @return $this
217
     */
218 12
    public function find($documentName = null)
219
    {
220 12
        $this->setDocumentName($documentName);
221 12
        parent::find();
222
223 12
        return $this;
224
    }
225
226
    /**
227
     * @param string $documentName
228
     * @return $this
229
     */
230 7
    public function findAndUpdate($documentName = null)
231
    {
232 7
        $this->setDocumentName($documentName);
233 7
        parent::findAndUpdate();
234
235 7
        return $this;
236
    }
237
238
    /**
239
     * @param bool $bool
240
     * @return $this
241
     */
242 4
    public function returnNew($bool = true)
243
    {
244 4
        $this->refresh(true);
245 4
        parent::returnNew($bool);
246
247 4
        return $this;
248
    }
249
250
    /**
251
     * @param string $documentName
252
     * @return $this
253
     */
254 1
    public function findAndRemove($documentName = null)
255
    {
256 1
        $this->setDocumentName($documentName);
257 1
        parent::findAndRemove();
258
259 1
        return $this;
260
    }
261
262
    /**
263
     * @param string $documentName
264
     * @return $this
265
     */
266 12
    public function update($documentName = null)
267
    {
268 12
        $this->setDocumentName($documentName);
269 12
        parent::update();
270
271 12
        return $this;
272
    }
273
274
    /**
275
     * @param string $documentName
276
     * @return $this
277
     */
278 1
    public function insert($documentName = null)
279
    {
280 1
        $this->setDocumentName($documentName);
281 1
        parent::insert();
282
283 1
        return $this;
284
    }
285
286
    /**
287
     * @param string $documentName
288
     * @return $this
289
     */
290 1
    public function remove($documentName = null)
291
    {
292 1
        $this->setDocumentName($documentName);
293 1
        parent::remove();
294
295 1
        return $this;
296
    }
297
298
    /**
299
     * @param object $document
300
     * @return $this
301
     */
302 10
    public function references($document)
303
    {
304 10
        $this->expr->references($document);
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Doctrine\MongoDB\Query\Expr as the method references() does only exist in the following sub-classes of Doctrine\MongoDB\Query\Expr: Doctrine\ODM\MongoDB\Query\Expr. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends 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 sub-classes 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 parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
305 8
        return $this;
306
    }
307
308
    /**
309
     * @param object $document
310
     * @return $this
311
     */
312 7
    public function includesReferenceTo($document)
313
    {
314 7
        $this->expr->includesReferenceTo($document);
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Doctrine\MongoDB\Query\Expr as the method includesReferenceTo() does only exist in the following sub-classes of Doctrine\MongoDB\Query\Expr: Doctrine\ODM\MongoDB\Query\Expr. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends 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 sub-classes 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 parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
315 5
        return $this;
316
    }
317
318
    /**
319
     * Gets the Query executable.
320
     *
321
     * @param array $options
322
     * @return Query $query
323
     */
324 190
    public function getQuery(array $options = array())
325
    {
326 190
        if ($this->query['type'] === Query::TYPE_MAP_REDUCE) {
327 3
            $this->hydrate = false;
328
        }
329
330 190
        $documentPersister = $this->dm->getUnitOfWork()->getDocumentPersister($this->class->name);
331
332 190
        $query = $this->query;
333
334 190
        $query['query'] = $this->expr->getQuery();
335 190
        $query['query'] = $documentPersister->addDiscriminatorToPreparedQuery($query['query']);
0 ignored issues
show
Documentation introduced by
$query['query'] is of type string, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
336 190
        $query['query'] = $documentPersister->addFilterToPreparedQuery($query['query']);
337
338 190
        $query['newObj'] = $this->expr->getNewObj();
339
340 190
        if (isset($query['distinct'])) {
341 2
            $query['distinct'] = $documentPersister->prepareFieldName($query['distinct']);
0 ignored issues
show
Bug introduced by
It seems like $query['distinct'] can also be of type array; however, Doctrine\ODM\MongoDB\Per...ter::prepareFieldName() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
342
        }
343
344 190
        if ($this->class->inheritanceType === ClassMetadataInfo::INHERITANCE_TYPE_SINGLE_COLLECTION && ! empty($query['upsert']) &&
345 190
            (empty($query['query'][$this->class->discriminatorField]) || is_array($query['query'][$this->class->discriminatorField]))) {
346 1
            throw new \InvalidArgumentException('Upsert query that is to be performed on discriminated document does not have single ' .
347 1
                'discriminator. Either not use base class or set \'' . $this->class->discriminatorField . '\' field manually.');
348
        }
349
350 189
        if ( ! empty($query['select'])) {
351 14
            $query['select'] = $documentPersister->prepareSortOrProjection($query['select']);
352 14
            if ($this->hydrate && $this->class->inheritanceType === ClassMetadataInfo::INHERITANCE_TYPE_SINGLE_COLLECTION
353 14
                && ! isset($query['select'][$this->class->discriminatorField])) {
354
                $includeMode = 0 < count(array_filter($query['select'], function($mode) { return $mode == 1; }));
355 2
                if ($includeMode && ! isset($query['select'][$this->class->discriminatorField])) {
356 1
                    $query['select'][$this->class->discriminatorField] = 1;
357
                }
358
            }
359
        }
360
361 189
        if (isset($query['sort'])) {
362 26
            $query['sort'] = $documentPersister->prepareSortOrProjection($query['sort']);
363
        }
364
365 189
        if ($this->class->slaveOkay) {
366 1
            $query['slaveOkay'] = $this->class->slaveOkay;
367
        }
368
369 189
        return new Query(
370 189
            $this->dm,
371 189
            $this->class,
372 189
            $this->collection,
373
            $query,
374
            $options,
375 189
            $this->hydrate,
376 189
            $this->refresh,
377 189
            $this->primers,
378 189
            $this->requireIndexes,
379 189
            $this->readOnly
380
        );
381
    }
382
383
    /**
384
     * Create a new Expr instance that can be used as an expression with the Builder
385
     *
386
     * @return Expr $expr
387
     */
388 25
    public function expr()
389
    {
390 25
        $expr = new Expr($this->dm);
391 25
        $expr->setClassMetadata($this->class);
392
393 25
        return $expr;
394
    }
395
396
    /**
397
     * @param string[]|string $documentName an array of document names or just one.
398
     */
399 220
    private function setDocumentName($documentName)
400
    {
401 220
        if (is_array($documentName)) {
402 2
            $documentNames = $documentName;
403 2
            $documentName = $documentNames[0];
404
405 2
            $metadata = $this->dm->getClassMetadata($documentName);
406 2
            $discriminatorField = $metadata->discriminatorField;
407 2
            $discriminatorValues = $this->getDiscriminatorValues($documentNames);
408
409
            // If a defaultDiscriminatorValue is set and it is among the discriminators being queries, add NULL to the list
410 1 View Code Duplication
            if ($metadata->defaultDiscriminatorValue && array_search($metadata->defaultDiscriminatorValue, $discriminatorValues) !== false) {
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...
411 1
                $discriminatorValues[] = null;
412
            }
413
414 1
            $this->field($discriminatorField)->in($discriminatorValues);
415
        }
416
417 219
        if ($documentName !== null) {
418 219
            $this->collection = $this->dm->getDocumentCollection($documentName);
419 219
            $this->class = $this->dm->getClassMetadata($documentName);
420
421
            // Expr also needs to know
422 219
            $this->expr->setClassMetadata($this->class);
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Doctrine\MongoDB\Query\Expr as the method setClassMetadata() does only exist in the following sub-classes of Doctrine\MongoDB\Query\Expr: Doctrine\ODM\MongoDB\Query\Expr. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends 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 sub-classes 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 parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
423
        }
424 219
    }
425
426
    /**
427
     * Get Discriminator Values
428
     *
429
     * @param \Iterator|array $classNames
430
     * @return array an array of discriminatorValues (mixed type)
431
     * @throws \InvalidArgumentException if the number of found collections > 1
432
     */
433 2
    private function getDiscriminatorValues($classNames)
434
    {
435 2
        $discriminatorValues = array();
436 2
        $collections = array();
437 2
        foreach ($classNames as $className) {
438 2
            $class = $this->dm->getClassMetadata($className);
439 2
            $discriminatorValues[] = $class->discriminatorValue;
440 2
            $key = $this->dm->getDocumentDatabase($className)->getName() . '.' . $class->getCollection();
441 2
            $collections[$key] = $key;
442
        }
443 2
        if (count($collections) > 1) {
444 1
            throw new \InvalidArgumentException('Documents involved are not all mapped to the same database collection.');
445
        }
446 1
        return $discriminatorValues;
447
    }
448
}
449