Completed
Pull Request — master (#1518)
by Maciej
10:54
created

Builder::includesReferenceTo()   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 0
Metric Value
dl 0
loc 5
ccs 3
cts 3
cp 1
rs 9.4285
c 0
b 0
f 0
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 223
    public function __construct(DocumentManager $dm, $documentName = null)
97
    {
98 223
        $this->dm = $dm;
99 223
        $this->expr = new Expr($dm);
100 223
        if ($documentName !== null) {
101 215
            $this->setDocumentName($documentName);
102
        }
103 222
    }
104
105
    /**
106
     * Set whether or not to require indexes.
107
     *
108
     * @param bool $requireIndexes
109
     * @return $this
110
     *
111
     * @deprecated method was deprecated in 1.2 and will be removed in 2.0
112
     */
113 2
    public function requireIndexes($requireIndexes = true)
114
    {
115 2
        $this->requireIndexes = $requireIndexes;
116 2
        return $this;
117
    }
118
119
    /**
120
     * Set the current field to operate on.
121
     *
122
     * @param string $field
123
     * @return $this
124
     */
125 143
    public function field($field)
126
    {
127 143
        $this->currentField = $field;
128 143
        parent::field($field);
129
130 143
        return $this;
131
    }
132
133
    /**
134
     * Use a primer to eagerly load all references in the current field.
135
     *
136
     * If $primer is true or a callable is provided, referenced documents for
137
     * this field will loaded into UnitOfWork immediately after the query is
138
     * executed. This will avoid multiple queries due to lazy initialization of
139
     * Proxy objects.
140
     *
141
     * If $primer is false, no priming will take place. That is also the default
142
     * behavior.
143
     *
144
     * If a custom callable is used, its signature should conform to the default
145
     * Closure defined in {@link ReferencePrimer::__construct()}.
146
     *
147
     * @param boolean|callable $primer
148
     * @return $this
149
     * @throws \InvalidArgumentException If $primer is not boolean or callable
150
     */
151 23
    public function prime($primer = true)
152
    {
153 23
        if ( ! is_bool($primer) && ! is_callable($primer)) {
154 1
            throw new \InvalidArgumentException('$primer is not a boolean or callable');
155
        }
156
157 22
        if ($primer === false) {
158 1
            unset($this->primers[$this->currentField]);
159
160 1
            return $this;
161
        }
162
163 22
        if (array_key_exists('eagerCursor', $this->query) && !$this->query['eagerCursor']) {
164 1
            throw new \BadMethodCallException("Can't call prime() when setting eagerCursor to false");
165
        }
166
167 21
        $this->primers[$this->currentField] = $primer;
168 21
        return $this;
169
    }
170
171
    /**
172
     * {@inheritdoc}
173
     */
174 5
    public function eagerCursor($bool = true)
175
    {
176 5
        if ( ! $bool && ! empty($this->primers)) {
177 1
            throw new \BadMethodCallException("Can't set eagerCursor to false when using reference primers");
178
        }
179
180 4
        return parent::eagerCursor($bool);
181
    }
182
183
184
    /**
185
     * @param bool $bool
186
     * @return $this
187
     */
188 16
    public function hydrate($bool = true)
189
    {
190 16
        $this->hydrate = $bool;
191 16
        return $this;
192
    }
193
194
    /**
195
     * @param bool $bool
196
     * @return $this
197
     */
198 2
    public function readOnly($bool = true)
199
    {
200 2
        $this->readOnly = $bool;
201 2
        return $this;
202
    }
203
204
    /**
205
     * @param bool $bool
206
     * @return $this
207
     */
208 5
    public function refresh($bool = true)
209
    {
210 5
        $this->refresh = $bool;
211 5
        return $this;
212
    }
213
214
    /**
215
     * Change the query type to find and optionally set and change the class being queried.
216
     *
217
     * @param string $documentName
218
     * @return $this
219
     */
220 12
    public function find($documentName = null)
221
    {
222 12
        $this->setDocumentName($documentName);
223 12
        parent::find();
224
225 12
        return $this;
226
    }
227
228
    /**
229
     * @param string $documentName
230
     * @return $this
231
     */
232 7
    public function findAndUpdate($documentName = null)
233
    {
234 7
        $this->setDocumentName($documentName);
235 7
        parent::findAndUpdate();
236
237 7
        return $this;
238
    }
239
240
    /**
241
     * @param bool $bool
242
     * @return $this
243
     */
244 4
    public function returnNew($bool = true)
245
    {
246 4
        $this->refresh(true);
247 4
        parent::returnNew($bool);
248
249 4
        return $this;
250
    }
251
252
    /**
253
     * @param string $documentName
254
     * @return $this
255
     */
256 1
    public function findAndRemove($documentName = null)
257
    {
258 1
        $this->setDocumentName($documentName);
259 1
        parent::findAndRemove();
260
261 1
        return $this;
262
    }
263
264
    /**
265
     * @param string $documentName
266
     * @return $this
267
     *
268
     * @deprecated Deprecated in version 1.2 - use updateOne or updateMany instead
269
     */
270 12
    public function update($documentName = null)
271
    {
272 12
        $this->setDocumentName($documentName);
273 12
        parent::update();
0 ignored issues
show
Deprecated Code introduced by
The method Doctrine\MongoDB\Query\Builder::update() has been deprecated with message: Deprecated in version 1.4 - use updateOne or updateMany instead

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...
274
275 12
        return $this;
276
    }
277
278
    /**
279
     * @param string $documentName
280
     * @return $this
281
     */
282
    public function updateOne($documentName = null)
283
    {
284
        $this->setDocumentName($documentName);
285
        parent::updateOne();
286
287
        return $this;
288
    }
289
290
    /**
291
     * @param string $documentName
292
     * @return $this
293
     */
294
    public function updateMany($documentName = null)
295
    {
296
        $this->setDocumentName($documentName);
297
        parent::updateMany();
298
299
        return $this;
300
    }
301
302
    /**
303
     * @param string $documentName
304
     * @return $this
305
     */
306 1
    public function insert($documentName = null)
307
    {
308 1
        $this->setDocumentName($documentName);
309 1
        parent::insert();
310
311 1
        return $this;
312
    }
313
314
    /**
315
     * @param string $documentName
316
     * @return $this
317
     */
318 1
    public function remove($documentName = null)
319
    {
320 1
        $this->setDocumentName($documentName);
321 1
        parent::remove();
322
323 1
        return $this;
324
    }
325
326
    /**
327
     * @param object $document
328
     * @return $this
329
     */
330 10
    public function references($document)
331
    {
332 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...
333 8
        return $this;
334
    }
335
336
    /**
337
     * @param object $document
338
     * @return $this
339
     */
340 7
    public function includesReferenceTo($document)
341
    {
342 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...
343 5
        return $this;
344
    }
345
346
    /**
347
     * Gets the Query executable.
348
     *
349
     * @param array $options
350
     * @return Query $query
351
     */
352 192
    public function getQuery(array $options = array())
353
    {
354 192
        if ($this->query['type'] === Query::TYPE_MAP_REDUCE) {
355 3
            $this->hydrate = false;
356
        }
357
358 192
        $documentPersister = $this->dm->getUnitOfWork()->getDocumentPersister($this->class->name);
359
360 192
        $query = $this->query;
361
362 192
        $query['query'] = $this->expr->getQuery();
363 192
        $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...
364 192
        $query['query'] = $documentPersister->addFilterToPreparedQuery($query['query']);
365
366 192
        $query['newObj'] = $this->expr->getNewObj();
367
368 192
        if (isset($query['distinct'])) {
369 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...
370
        }
371
372 192
        if ($this->class->inheritanceType === ClassMetadataInfo::INHERITANCE_TYPE_SINGLE_COLLECTION && ! empty($query['upsert']) &&
373 192
            (empty($query['query'][$this->class->discriminatorField]) || is_array($query['query'][$this->class->discriminatorField]))) {
374 1
            throw new \InvalidArgumentException('Upsert query that is to be performed on discriminated document does not have single ' .
375 1
                'discriminator. Either not use base class or set \'' . $this->class->discriminatorField . '\' field manually.');
376
        }
377
378 191
        if ( ! empty($query['select'])) {
379 14
            $query['select'] = $documentPersister->prepareSortOrProjection($query['select']);
380 14
            if ($this->hydrate && $this->class->inheritanceType === ClassMetadataInfo::INHERITANCE_TYPE_SINGLE_COLLECTION
381 14
                && ! isset($query['select'][$this->class->discriminatorField])) {
382
                $includeMode = 0 < count(array_filter($query['select'], function($mode) { return $mode == 1; }));
383 2
                if ($includeMode && ! isset($query['select'][$this->class->discriminatorField])) {
384 1
                    $query['select'][$this->class->discriminatorField] = 1;
385
                }
386
            }
387
        }
388
389 191
        if (isset($query['sort'])) {
390 26
            $query['sort'] = $documentPersister->prepareSortOrProjection($query['sort']);
391
        }
392
393 191
        if ($this->class->slaveOkay) {
394 1
            $query['slaveOkay'] = $this->class->slaveOkay;
395
        }
396
397 191
        return new Query(
398 191
            $this->dm,
399 191
            $this->class,
400 191
            $this->collection,
401
            $query,
402
            $options,
403 191
            $this->hydrate,
404 191
            $this->refresh,
405 191
            $this->primers,
406 191
            $this->requireIndexes,
407 191
            $this->readOnly
408
        );
409
    }
410
411
    /**
412
     * Create a new Expr instance that can be used as an expression with the Builder
413
     *
414
     * @return Expr $expr
415
     */
416 25
    public function expr()
417
    {
418 25
        $expr = new Expr($this->dm);
419 25
        $expr->setClassMetadata($this->class);
420
421 25
        return $expr;
422
    }
423
424
    /**
425
     * @param string[]|string $documentName an array of document names or just one.
426
     */
427 222
    private function setDocumentName($documentName)
428
    {
429 222
        if (is_array($documentName)) {
430 2
            $documentNames = $documentName;
431 2
            $documentName = $documentNames[0];
432
433 2
            $metadata = $this->dm->getClassMetadata($documentName);
434 2
            $discriminatorField = $metadata->discriminatorField;
435 2
            $discriminatorValues = $this->getDiscriminatorValues($documentNames);
436
437
            // If a defaultDiscriminatorValue is set and it is among the discriminators being queries, add NULL to the list
438 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...
439 1
                $discriminatorValues[] = null;
440
            }
441
442 1
            $this->field($discriminatorField)->in($discriminatorValues);
443
        }
444
445 221
        if ($documentName !== null) {
446 221
            $this->collection = $this->dm->getDocumentCollection($documentName);
447 221
            $this->class = $this->dm->getClassMetadata($documentName);
448
449
            // Expr also needs to know
450 221
            $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...
451
        }
452 221
    }
453
454
    /**
455
     * Get Discriminator Values
456
     *
457
     * @param \Iterator|array $classNames
458
     * @return array an array of discriminatorValues (mixed type)
459
     * @throws \InvalidArgumentException if the number of found collections > 1
460
     */
461 2
    private function getDiscriminatorValues($classNames)
462
    {
463 2
        $discriminatorValues = array();
464 2
        $collections = array();
465 2
        foreach ($classNames as $className) {
466 2
            $class = $this->dm->getClassMetadata($className);
467 2
            $discriminatorValues[] = $class->discriminatorValue;
468 2
            $key = $this->dm->getDocumentDatabase($className)->getName() . '.' . $class->getCollection();
469 2
            $collections[$key] = $key;
470
        }
471 2
        if (count($collections) > 1) {
472 1
            throw new \InvalidArgumentException('Documents involved are not all mapped to the same database collection.');
473
        }
474 1
        return $discriminatorValues;
475
    }
476
}
477