Completed
Push — master ( 7b9f4b...1cd743 )
by Andreas
13s queued 10s
created

lib/Doctrine/ODM/MongoDB/Query/Expr.php (1 issue)

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 BadMethodCallException;
8
use Doctrine\ODM\MongoDB\DocumentManager;
9
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
10
use Doctrine\ODM\MongoDB\Mapping\MappingException;
11
use GeoJson\Geometry\Geometry;
12
use GeoJson\Geometry\Point;
13
use InvalidArgumentException;
14
use LogicException;
15
use MongoDB\BSON\Binary;
16
use MongoDB\BSON\Javascript;
17
use function array_key_exists;
18
use function array_map;
19
use function array_merge;
20
use function array_values;
21
use function assert;
22
use function explode;
23
use function func_get_args;
24
use function in_array;
25
use function is_array;
26
use function is_string;
27
use function key;
28
use function sprintf;
29
use function strpos;
30
use function strtolower;
31
32
/**
33
 * Query expression builder for ODM.
34
 */
35
class Expr
36
{
37
    /**
38
     * The query criteria array.
39
     *
40
     * @var array
41
     */
42
    private $query = [];
43
44
    /**
45
     * The "new object" array containing either a full document or a number of
46
     * atomic update operators.
47
     *
48
     * @see docs.mongodb.org/manual/reference/method/db.collection.update/#update-parameter
49
     *
50
     * @var array
51
     */
52
    private $newObj = [];
53
54
    /**
55
     * The current field we are operating on.
56
     *
57
     * @var string|null
58
     */
59
    private $currentField;
60
61
    /**
62
     * The DocumentManager instance for this query
63
     *
64
     * @var DocumentManager
65
     */
66
    private $dm;
67
68
    /**
69
     * The ClassMetadata instance for the document being queried
70
     *
71
     * @var ClassMetadata
72
     */
73
    private $class;
74
75 392
    public function __construct(DocumentManager $dm)
76
    {
77 392
        $this->dm = $dm;
78 392
    }
79
80
    /**
81
     * Add one or more $and clauses to the current query.
82
     *
83
     * @see Builder::addAnd()
84
     * @see http://docs.mongodb.org/manual/reference/operator/and/
85
     *
86
     * @param array|Expr $expression
87
     * @param array|Expr ...$expressions
88
     */
89 3
    public function addAnd($expression, ...$expressions) : self
90
    {
91 3
        if (! isset($this->query['$and'])) {
92 3
            $this->query['$and'] = [];
93
        }
94
95 3
        $this->query['$and'] = array_merge(
96 3
            $this->query['$and'],
97 3
            array_map(
98
                static function ($expression) {
99 3
                    return $expression instanceof Expr ? $expression->getQuery() : $expression;
100 3
                },
101 3
                func_get_args()
102
            )
103
        );
104
105 3
        return $this;
106
    }
107
108
    /**
109
     * Add one or more $nor clauses to the current query.
110
     *
111
     * @see Builder::addNor()
112
     * @see http://docs.mongodb.org/manual/reference/operator/nor/
113
     *
114
     * @param array|Expr $expression
115
     * @param array|Expr ...$expressions
116
     */
117 1
    public function addNor($expression, ...$expressions) : self
118
    {
119 1
        if (! isset($this->query['$nor'])) {
120 1
            $this->query['$nor'] = [];
121
        }
122
123 1
        $this->query['$nor'] = array_merge(
124 1
            $this->query['$nor'],
125
            array_map(static function ($expression) {
126 1
                return $expression instanceof Expr ? $expression->getQuery() : $expression;
127 1
            }, func_get_args())
128
        );
129
130 1
        return $this;
131
    }
132
133
    /**
134
     * Add one or more $or clauses to the current query.
135
     *
136
     * @see Builder::addOr()
137
     * @see http://docs.mongodb.org/manual/reference/operator/or/
138
     *
139
     * @param array|Expr $expression
140
     * @param array|Expr ...$expressions
141
     */
142 5
    public function addOr($expression, ...$expressions) : self
143
    {
144 5
        if (! isset($this->query['$or'])) {
145 5
            $this->query['$or'] = [];
146
        }
147
148 5
        $this->query['$or'] = array_merge(
149 5
            $this->query['$or'],
150
            array_map(static function ($expression) {
151 5
                return $expression instanceof Expr ? $expression->getQuery() : $expression;
152 5
            }, func_get_args())
153
        );
154
155 5
        return $this;
156
    }
157
158
    /**
159
     * Append one or more values to the current array field only if they do not
160
     * already exist in the array.
161
     *
162
     * If the field does not exist, it will be set to an array containing the
163
     * unique value(s) in the argument. If the field is not an array, the query
164
     * will yield an error.
165
     *
166
     * Multiple values may be specified by provided an Expr object and using
167
     * {@link Expr::each()}.
168
     *
169
     * @see Builder::addToSet()
170
     * @see http://docs.mongodb.org/manual/reference/operator/addToSet/
171
     * @see http://docs.mongodb.org/manual/reference/operator/each/
172
     *
173
     * @param mixed|Expr $valueOrExpression
174
     */
175 5
    public function addToSet($valueOrExpression) : self
176
    {
177 5
        if ($valueOrExpression instanceof Expr) {
178 1
            $valueOrExpression = $valueOrExpression->getQuery();
179
        }
180
181 5
        $this->requiresCurrentField();
182 5
        $this->newObj['$addToSet'][$this->currentField] = $valueOrExpression;
183 5
        return $this;
184
    }
185
186
    /**
187
     * Specify $all criteria for the current field.
188
     *
189
     * @see Builder::all()
190
     * @see http://docs.mongodb.org/manual/reference/operator/all/
191
     */
192 2
    public function all(array $values) : self
193
    {
194 2
        return $this->operator('$all', $values);
195
    }
196
197
    /**
198
     * Apply a bitwise operation on the current field
199
     *
200
     * @see http://docs.mongodb.org/manual/reference/operator/update/bit/
201
     */
202 3
    protected function bit(string $operator, int $value) : self
203
    {
204 3
        $this->requiresCurrentField();
205 3
        $this->newObj['$bit'][$this->currentField][$operator] = $value;
206 3
        return $this;
207
    }
208
209
    /**
210
     * Apply a bitwise and operation on the current field.
211
     *
212
     * @see Builder::bitAnd()
213
     * @see http://docs.mongodb.org/manual/reference/operator/update/bit/
214
     */
215 1
    public function bitAnd(int $value) : self
216
    {
217 1
        return $this->bit('and', $value);
218
    }
219
220
    /**
221
     * Apply a bitwise or operation on the current field.
222
     *
223
     * @see Builder::bitOr()
224
     * @see http://docs.mongodb.org/manual/reference/operator/update/bit/
225
     */
226 1
    public function bitOr(int $value) : self
227
    {
228 1
        return $this->bit('or', $value);
229
    }
230
231
    /**
232
     * Matches documents where all of the bit positions given by the query are
233
     * clear.
234
     *
235
     * @see Builder::bitsAllClear()
236
     * @see https://docs.mongodb.org/manual/reference/operator/query/bitsAllClear/
237
     *
238
     * @param int|array|Binary $value
239
     */
240
    public function bitsAllClear($value) : self
241
    {
242
        $this->requiresCurrentField();
243
        return $this->operator('$bitsAllClear', $value);
244
    }
245
246
    /**
247
     * Matches documents where all of the bit positions given by the query are
248
     * set.
249
     *
250
     * @see Builder::bitsAllSet()
251
     * @see https://docs.mongodb.org/manual/reference/operator/query/bitsAllSet/
252
     *
253
     * @param int|array|Binary $value
254
     */
255
    public function bitsAllSet($value) : self
256
    {
257
        $this->requiresCurrentField();
258
        return $this->operator('$bitsAllSet', $value);
259
    }
260
261
    /**
262
     * Matches documents where any of the bit positions given by the query are
263
     * clear.
264
     *
265
     * @see Builder::bitsAnyClear()
266
     * @see https://docs.mongodb.org/manual/reference/operator/query/bitsAnyClear/
267
     *
268
     * @param int|array|Binary $value
269
     */
270
    public function bitsAnyClear($value) : self
271
    {
272
        $this->requiresCurrentField();
273
        return $this->operator('$bitsAnyClear', $value);
274
    }
275
276
    /**
277
     * Matches documents where any of the bit positions given by the query are
278
     * set.
279
     *
280
     * @see Builder::bitsAnySet()
281
     * @see https://docs.mongodb.org/manual/reference/operator/query/bitsAnySet/
282
     *
283
     * @param int|array|Binary $value
284
     */
285
    public function bitsAnySet($value) : self
286
    {
287
        $this->requiresCurrentField();
288
        return $this->operator('$bitsAnySet', $value);
289
    }
290
291
    /**
292
     * Apply a bitwise xor operation on the current field.
293
     *
294
     * @see Builder::bitXor()
295
     * @see http://docs.mongodb.org/manual/reference/operator/update/bit/
296
     */
297 1
    public function bitXor(int $value) : self
298
    {
299 1
        return $this->bit('xor', $value);
300
    }
301
302
    /**
303
     * A boolean flag to enable or disable case sensitive search for $text
304
     * criteria.
305
     *
306
     * This method must be called after text().
307
     *
308
     * @see Builder::caseSensitive()
309
     * @see http://docs.mongodb.org/manual/reference/operator/text/
310
     *
311
     * @throws BadMethodCallException If the query does not already have $text criteria.
312
     */
313 3
    public function caseSensitive(bool $caseSensitive) : self
314
    {
315 3
        if (! isset($this->query['$text'])) {
316 1
            throw new BadMethodCallException('This method requires a $text operator (call text() first)');
317
        }
318
319
        // Remove caseSensitive option to keep support for older database versions
320 2
        if ($caseSensitive) {
321 2
            $this->query['$text']['$caseSensitive'] = true;
322 1
        } elseif (isset($this->query['$text']['$caseSensitive'])) {
323 1
            unset($this->query['$text']['$caseSensitive']);
324
        }
325
326 2
        return $this;
327
    }
328
329
    /**
330
     * Associates a comment to any expression taking a query predicate.
331
     *
332
     * @see Builder::comment()
333
     * @see http://docs.mongodb.org/manual/reference/operator/query/comment/
334
     */
335
    public function comment(string $comment) : self
336
    {
337
        $this->query['$comment'] = $comment;
338
        return $this;
339
    }
340
341
    /**
342
     * Sets the value of the current field to the current date, either as a date or a timestamp.
343
     *
344
     * @see Builder::currentDate()
345
     * @see http://docs.mongodb.org/manual/reference/operator/update/currentDate/
346
     *
347
     * @throws InvalidArgumentException If an invalid type is given.
348
     */
349 3
    public function currentDate(string $type = 'date') : self
350
    {
351 3
        if (! in_array($type, ['date', 'timestamp'])) {
352 1
            throw new InvalidArgumentException('Type for currentDate operator must be date or timestamp.');
353
        }
354
355 2
        $this->requiresCurrentField();
356 2
        $this->newObj['$currentDate'][$this->currentField]['$type'] = $type;
357 2
        return $this;
358
    }
359
360
    /**
361
     * A boolean flag to enable or disable diacritic sensitive search for $text
362
     * criteria.
363
     *
364
     * This method must be called after text().
365
     *
366
     * @see Builder::diacriticSensitive()
367
     * @see http://docs.mongodb.org/manual/reference/operator/text/
368
     *
369
     * @throws BadMethodCallException If the query does not already have $text criteria.
370
     */
371 3
    public function diacriticSensitive(bool $diacriticSensitive) : self
372
    {
373 3
        if (! isset($this->query['$text'])) {
374 1
            throw new BadMethodCallException('This method requires a $text operator (call text() first)');
375
        }
376
377
        // Remove diacriticSensitive option to keep support for older database versions
378 2
        if ($diacriticSensitive) {
379 2
            $this->query['$text']['$diacriticSensitive'] = true;
380 1
        } elseif (isset($this->query['$text']['$diacriticSensitive'])) {
381 1
            unset($this->query['$text']['$diacriticSensitive']);
382
        }
383
384 2
        return $this;
385
    }
386
387
    /**
388
     * Add $each criteria to the expression for a $push operation.
389
     *
390
     * @see Expr::push()
391
     * @see http://docs.mongodb.org/manual/reference/operator/each/
392
     */
393 4
    public function each(array $values) : self
394
    {
395 4
        return $this->operator('$each', $values);
396
    }
397
398
    /**
399
     * Specify $elemMatch criteria for the current field.
400
     *
401
     * @see Builder::elemMatch()
402
     * @see http://docs.mongodb.org/manual/reference/operator/elemMatch/
403
     *
404
     * @param array|Expr $expression
405
     */
406 4
    public function elemMatch($expression) : self
407
    {
408 4
        return $this->operator('$elemMatch', $expression instanceof Expr ? $expression->getQuery() : $expression);
409
    }
410
411
    /**
412
     * Specify an equality match for the current field.
413
     *
414
     * @see Builder::equals()
415
     *
416
     * @param mixed $value
417
     */
418 104
    public function equals($value) : self
419
    {
420 104
        if ($this->currentField) {
421 103
            $this->query[$this->currentField] = $value;
422
        } else {
423 1
            $this->query = $value;
424
        }
425 104
        return $this;
426
    }
427
428
    /**
429
     * Specify $exists criteria for the current field.
430
     *
431
     * @see Builder::exists()
432
     * @see http://docs.mongodb.org/manual/reference/operator/exists/
433
     */
434 5
    public function exists(bool $bool) : self
435
    {
436 5
        return $this->operator('$exists', $bool);
437
    }
438
439
    /**
440
     * Set the current field for building the expression.
441
     *
442
     * @see Builder::field()
443
     */
444 192
    public function field(string $field) : self
445
    {
446 192
        $this->currentField = $field;
447 192
        return $this;
448
    }
449
450
    /**
451
     * Add $geoIntersects criteria with a GeoJSON geometry to the expression.
452
     *
453
     * The geometry parameter GeoJSON object or an array corresponding to the
454
     * geometry's JSON representation.
455
     *
456
     * @see Builder::geoIntersects()
457
     * @see http://docs.mongodb.org/manual/reference/operator/geoIntersects/
458
     *
459
     * @param array|Geometry $geometry
460
     */
461 2
    public function geoIntersects($geometry) : self
462
    {
463 2
        if ($geometry instanceof Geometry) {
464 1
            $geometry = $geometry->jsonSerialize();
465
        }
466
467 2
        return $this->operator('$geoIntersects', ['$geometry' => $geometry]);
468
    }
469
470
    /**
471
     * Add $geoWithin criteria with a GeoJSON geometry to the expression.
472
     *
473
     * The geometry parameter GeoJSON object or an array corresponding to the
474
     * geometry's JSON representation.
475
     *
476
     * @see Builder::geoWithin()
477
     * @see http://docs.mongodb.org/manual/reference/operator/geoIntersects/
478
     *
479
     * @param array|Geometry $geometry
480
     */
481 2
    public function geoWithin($geometry) : self
482
    {
483 2
        if ($geometry instanceof Geometry) {
484 1
            $geometry = $geometry->jsonSerialize();
485
        }
486
487 2
        return $this->operator('$geoWithin', ['$geometry' => $geometry]);
488
    }
489
490
    /**
491
     * Add $geoWithin criteria with a $box shape to the expression.
492
     *
493
     * A rectangular polygon will be constructed from a pair of coordinates
494
     * corresponding to the bottom left and top right corners.
495
     *
496
     * Note: the $box operator only supports legacy coordinate pairs and 2d
497
     * indexes. This cannot be used with 2dsphere indexes and GeoJSON shapes.
498
     *
499
     * @see Builder::geoWithinBox()
500
     * @see http://docs.mongodb.org/manual/reference/operator/box/
501
     */
502 1
    public function geoWithinBox(float $x1, float $y1, float $x2, float $y2) : self
503
    {
504 1
        $shape = ['$box' => [[$x1, $y1], [$x2, $y2]]];
505
506 1
        return $this->operator('$geoWithin', $shape);
507
    }
508
509
    /**
510
     * Add $geoWithin criteria with a $center shape to the expression.
511
     *
512
     * Note: the $center operator only supports legacy coordinate pairs and 2d
513
     * indexes. This cannot be used with 2dsphere indexes and GeoJSON shapes.
514
     *
515
     * @see Builider::geoWithinCenter()
516
     * @see http://docs.mongodb.org/manual/reference/operator/center/
517
     */
518 1
    public function geoWithinCenter(float $x, float $y, float $radius) : self
519
    {
520 1
        $shape = ['$center' => [[$x, $y], $radius]];
521
522 1
        return $this->operator('$geoWithin', $shape);
523
    }
524
525
    /**
526
     * Add $geoWithin criteria with a $centerSphere shape to the expression.
527
     *
528
     * Note: the $centerSphere operator supports both 2d and 2dsphere indexes.
529
     *
530
     * @see Builder::geoWithinCenterSphere()
531
     * @see http://docs.mongodb.org/manual/reference/operator/centerSphere/
532
     */
533 1
    public function geoWithinCenterSphere(float $x, float $y, float $radius) : self
534
    {
535 1
        $shape = ['$centerSphere' => [[$x, $y], $radius]];
536
537 1
        return $this->operator('$geoWithin', $shape);
538
    }
539
540
    /**
541
     * Add $geoWithin criteria with a $polygon shape to the expression.
542
     *
543
     * Point coordinates are in x, y order (easting, northing for projected
544
     * coordinates, longitude, latitude for geographic coordinates).
545
     *
546
     * The last point coordinate is implicitly connected with the first.
547
     *
548
     * Note: the $polygon operator only supports legacy coordinate pairs and 2d
549
     * indexes. This cannot be used with 2dsphere indexes and GeoJSON shapes.
550
     *
551
     * @see Builder::geoWithinPolygon()
552
     * @see http://docs.mongodb.org/manual/reference/operator/polygon/
553
     *
554
     * @param array $point1    First point of the polygon
555
     * @param array $point2    Second point of the polygon
556
     * @param array $point3    Third point of the polygon
557
     * @param array ...$points Additional points of the polygon
558
     *
559
     * @throws InvalidArgumentException If less than three points are given.
560
     */
561 1
    public function geoWithinPolygon($point1, $point2, $point3, ...$points) : self
562
    {
563 1
        $shape = ['$polygon' => func_get_args()];
564
565 1
        return $this->operator('$geoWithin', $shape);
566
    }
567
568
    /**
569
     * Return the current field.
570
     */
571 2
    public function getCurrentField() : ?string
572
    {
573 2
        return $this->currentField;
574
    }
575
576
    /**
577
     * Gets prepared newObj part of expression.
578
     */
579 177
    public function getNewObj() : array
580
    {
581 177
        return $this->dm->getUnitOfWork()
582 177
            ->getDocumentPersister($this->class->name)
583 177
            ->prepareQueryOrNewObj($this->newObj, true);
584
    }
585
586
    /**
587
     * Gets prepared query part of expression.
588
     */
589 253
    public function getQuery() : array
590
    {
591 253
        return $this->dm->getUnitOfWork()
592 253
            ->getDocumentPersister($this->class->name)
593 253
            ->prepareQueryOrNewObj($this->query);
594
    }
595
596
    /**
597
     * Specify $gt criteria for the current field.
598
     *
599
     * @see Builder::gt()
600
     * @see http://docs.mongodb.org/manual/reference/operator/gt/
601
     *
602
     * @param mixed $value
603
     */
604 2
    public function gt($value) : self
605
    {
606 2
        return $this->operator('$gt', $value);
607
    }
608
609
    /**
610
     * Specify $gte criteria for the current field.
611
     *
612
     * @see Builder::gte()
613
     * @see http://docs.mongodb.org/manual/reference/operator/gte/
614
     *
615
     * @param mixed $value
616
     */
617 2
    public function gte($value) : self
618
    {
619 2
        return $this->operator('$gte', $value);
620
    }
621
622
    /**
623
     * Specify $in criteria for the current field.
624
     *
625
     * @see Builder::in()
626
     * @see http://docs.mongodb.org/manual/reference/operator/in/
627
     */
628 31
    public function in(array $values) : self
629
    {
630 31
        return $this->operator('$in', array_values($values));
631
    }
632
633
    /**
634
     * Increment the current field.
635
     *
636
     * If the field does not exist, it will be set to this value.
637
     *
638
     * @see Builder::inc()
639
     * @see http://docs.mongodb.org/manual/reference/operator/inc/
640
     *
641
     * @param float|int $value
642
     */
643 5
    public function inc($value) : self
644
    {
645 5
        $this->requiresCurrentField();
646 5
        $this->newObj['$inc'][$this->currentField] = $value;
647 5
        return $this;
648
    }
649
650
    /**
651
     * Checks that the current field includes a reference to the supplied document.
652
     */
653 6
    public function includesReferenceTo(object $document) : self
654
    {
655 6
        $this->requiresCurrentField();
656 6
        $mapping   = $this->getReferenceMapping();
657 4
        $reference = $this->dm->createReference($document, $mapping);
658 4
        $storeAs   = $mapping['storeAs'] ?? null;
659 4
        $keys      = [];
660
661
        switch ($storeAs) {
662 4
            case ClassMetadata::REFERENCE_STORE_AS_ID:
663 2
                $this->query[$mapping['name']] = $reference;
664 2
                return $this;
665
                break;
666
667 3
            case ClassMetadata::REFERENCE_STORE_AS_REF:
668
                $keys = ['id' => true];
669
                break;
670
671 3
            case ClassMetadata::REFERENCE_STORE_AS_DB_REF:
672 1
            case ClassMetadata::REFERENCE_STORE_AS_DB_REF_WITH_DB:
673 3
                $keys = ['$ref' => true, '$id' => true, '$db' => true];
674
675 3
                if ($storeAs === ClassMetadata::REFERENCE_STORE_AS_DB_REF) {
676 2
                    unset($keys['$db']);
677
                }
678
679 3
                if (isset($mapping['targetDocument'])) {
680 1
                    unset($keys['$ref'], $keys['$db']);
681
                }
682 3
                break;
683
684
            default:
685
                throw new InvalidArgumentException(sprintf('Reference type %s is invalid.', $storeAs));
686
        }
687
688 3
        foreach ($keys as $key => $value) {
689 3
            $this->query[$mapping['name']]['$elemMatch'][$key] = $reference[$key];
690
        }
691
692 3
        return $this;
693
    }
694
695
    /**
696
     * Set the $language option for $text criteria.
697
     *
698
     * This method must be called after text().
699
     *
700
     * @see Builder::language()
701
     * @see http://docs.mongodb.org/manual/reference/operator/text/
702
     *
703
     * @throws BadMethodCallException If the query does not already have $text criteria.
704
     */
705 2
    public function language(string $language) : self
706
    {
707 2
        if (! isset($this->query['$text'])) {
708 1
            throw new BadMethodCallException('This method requires a $text operator (call text() first)');
709
        }
710
711 1
        $this->query['$text']['$language'] = $language;
712
713 1
        return $this;
714
    }
715
716
    /**
717
     * Specify $lt criteria for the current field.
718
     *
719
     * @see Builder::lte()
720
     * @see http://docs.mongodb.org/manual/reference/operator/lte/
721
     *
722
     * @param mixed $value
723
     */
724 4
    public function lt($value) : self
725
    {
726 4
        return $this->operator('$lt', $value);
727
    }
728
729
    /**
730
     * Specify $lte criteria for the current field.
731
     *
732
     * @see Builder::lte()
733
     * @see http://docs.mongodb.org/manual/reference/operator/lte/
734
     *
735
     * @param mixed $value
736
     */
737 2
    public function lte($value) : self
738
    {
739 2
        return $this->operator('$lte', $value);
740
    }
741
742
    /**
743
     * Updates the value of the field to a specified value if the specified value is greater than the current value of the field.
744
     *
745
     * @see Builder::max()
746
     * @see http://docs.mongodb.org/manual/reference/operator/update/max/
747
     *
748
     * @param mixed $value
749
     */
750
    public function max($value) : self
751
    {
752
        $this->requiresCurrentField();
753
        $this->newObj['$max'][$this->currentField] = $value;
754
        return $this;
755
    }
756
757
    /**
758
     * Updates the value of the field to a specified value if the specified value is less than the current value of the field.
759
     *
760
     * @see Builder::min()
761
     * @see http://docs.mongodb.org/manual/reference/operator/update/min/
762
     *
763
     * @param mixed $value
764
     */
765
    public function min($value) : self
766
    {
767
        $this->requiresCurrentField();
768
        $this->newObj['$min'][$this->currentField] = $value;
769
        return $this;
770
    }
771
772
    /**
773
     * Specify $mod criteria for the current field.
774
     *
775
     * @see Builder::mod()
776
     * @see http://docs.mongodb.org/manual/reference/operator/mod/
777
     *
778
     * @param float|int $divisor
779
     * @param float|int $remainder
780
     */
781
    public function mod($divisor, $remainder = 0) : self
782
    {
783
        return $this->operator('$mod', [$divisor, $remainder]);
784
    }
785
786
    /**
787
     * Multiply the current field.
788
     *
789
     * If the field does not exist, it will be set to 0.
790
     *
791
     * @see Builder::mul()
792
     * @see http://docs.mongodb.org/manual/reference/operator/mul/
793
     *
794
     * @param float|int $value
795
     */
796
    public function mul($value) : self
797
    {
798
        $this->requiresCurrentField();
799
        $this->newObj['$mul'][$this->currentField] = $value;
800
        return $this;
801
    }
802
803
    /**
804
     * Add $near criteria to the expression.
805
     *
806
     * A GeoJSON point may be provided as the first and only argument for
807
     * 2dsphere queries. This single parameter may be a GeoJSON point object or
808
     * an array corresponding to the point's JSON representation.
809
     *
810
     * @see Builder::near()
811
     * @see http://docs.mongodb.org/manual/reference/operator/near/
812
     *
813
     * @param float|array|Point $x
814
     * @param float             $y
815
     */
816 3
    public function near($x, $y = null) : self
817
    {
818 3
        if ($x instanceof Point) {
819 1
            $x = $x->jsonSerialize();
820
        }
821
822 3
        if (is_array($x)) {
823 2
            return $this->operator('$near', ['$geometry' => $x]);
824
        }
825
826 1
        return $this->operator('$near', [$x, $y]);
827
    }
828
829
    /**
830
     * Add $nearSphere criteria to the expression.
831
     *
832
     * A GeoJSON point may be provided as the first and only argument for
833
     * 2dsphere queries. This single parameter may be a GeoJSON point object or
834
     * an array corresponding to the point's JSON representation.
835
     *
836
     * @see Builder::nearSphere()
837
     * @see http://docs.mongodb.org/manual/reference/operator/nearSphere/
838
     *
839
     * @param float|array|Point $x
840
     * @param float             $y
841
     */
842 3
    public function nearSphere($x, $y = null) : self
843
    {
844 3
        if ($x instanceof Point) {
845 1
            $x = $x->jsonSerialize();
846
        }
847
848 3
        if (is_array($x)) {
849 2
            return $this->operator('$nearSphere', ['$geometry' => $x]);
850
        }
851
852 1
        return $this->operator('$nearSphere', [$x, $y]);
853
    }
854
855
    /**
856
     * Negates an expression for the current field.
857
     *
858
     * @see Builder::not()
859
     * @see http://docs.mongodb.org/manual/reference/operator/not/
860
     *
861
     * @param array|Expr $expression
862
     */
863 2
    public function not($expression) : self
864
    {
865 2
        return $this->operator('$not', $expression instanceof Expr ? $expression->getQuery() : $expression);
866
    }
867
868
    /**
869
     * Specify $ne criteria for the current field.
870
     *
871
     * @see Builder::notEqual()
872
     * @see http://docs.mongodb.org/manual/reference/operator/ne/
873
     *
874
     * @param mixed $value
875
     */
876 6
    public function notEqual($value) : self
877
    {
878 6
        return $this->operator('$ne', $value);
879
    }
880
881
    /**
882
     * Specify $nin criteria for the current field.
883
     *
884
     * @see Builder::notIn()
885
     * @see http://docs.mongodb.org/manual/reference/operator/nin/
886
     */
887 6
    public function notIn(array $values) : self
888
    {
889 6
        return $this->operator('$nin', array_values($values));
890
    }
891
892
    /**
893
     * Defines an operator and value on the expression.
894
     *
895
     * If there is a current field, the operator will be set on it; otherwise,
896
     * the operator is set at the top level of the query.
897
     *
898
     * @param mixed $value
899
     */
900 81
    public function operator(string $operator, $value) : self
901
    {
902 81
        $this->wrapEqualityCriteria();
903
904 81
        if ($this->currentField) {
905 56
            $this->query[$this->currentField][$operator] = $value;
906
        } else {
907 27
            $this->query[$operator] = $value;
908
        }
909 81
        return $this;
910
    }
911
912
    /**
913
     * Remove the first element from the current array field.
914
     *
915
     * @see Builder::popFirst()
916
     * @see http://docs.mongodb.org/manual/reference/operator/pop/
917
     */
918 2
    public function popFirst() : self
919
    {
920 2
        $this->requiresCurrentField();
921 2
        $this->newObj['$pop'][$this->currentField] = -1;
922 2
        return $this;
923
    }
924
925
    /**
926
     * Remove the last element from the current array field.
927
     *
928
     * @see Builder::popLast()
929
     * @see http://docs.mongodb.org/manual/reference/operator/pop/
930
     */
931 1
    public function popLast() : self
932
    {
933 1
        $this->requiresCurrentField();
934 1
        $this->newObj['$pop'][$this->currentField] = 1;
935 1
        return $this;
936
    }
937
938
    /**
939
     * Add $position criteria to the expression for a $push operation.
940
     *
941
     * This is useful in conjunction with {@link Expr::each()} for a
942
     * {@link Expr::push()} operation.
943
     *
944
     * @see http://docs.mongodb.org/manual/reference/operator/update/position/
945
     */
946 1
    public function position(int $position) : self
947
    {
948 1
        return $this->operator('$position', $position);
949
    }
950
951
    /**
952
     * Remove all elements matching the given value or expression from the
953
     * current array field.
954
     *
955
     * @see Builder::pull()
956
     * @see http://docs.mongodb.org/manual/reference/operator/pull/
957
     *
958
     * @param mixed|Expr $valueOrExpression
959
     */
960 2
    public function pull($valueOrExpression) : self
961
    {
962 2
        if ($valueOrExpression instanceof Expr) {
963 1
            $valueOrExpression = $valueOrExpression->getQuery();
964
        }
965
966 2
        $this->requiresCurrentField();
967 2
        $this->newObj['$pull'][$this->currentField] = $valueOrExpression;
968 2
        return $this;
969
    }
970
971
    /**
972
     * Remove all elements matching any of the given values from the current
973
     * array field.
974
     *
975
     * @see Builder::pullAll()
976
     * @see http://docs.mongodb.org/manual/reference/operator/pullAll/
977
     */
978
    public function pullAll(array $values) : self
979
    {
980
        $this->requiresCurrentField();
981
        $this->newObj['$pullAll'][$this->currentField] = $values;
982
        return $this;
983
    }
984
985
    /**
986
     * Append one or more values to the current array field.
987
     *
988
     * If the field does not exist, it will be set to an array containing the
989
     * value(s) in the argument. If the field is not an array, the query
990
     * will yield an error.
991
     *
992
     * Multiple values may be specified by providing an Expr object and using
993
     * {@link Expr::each()}. {@link Expr::slice()} and {@link Expr::sort()} may
994
     * also be used to limit and order array elements, respectively.
995
     *
996
     * @see Builder::push()
997
     * @see http://docs.mongodb.org/manual/reference/operator/push/
998
     * @see http://docs.mongodb.org/manual/reference/operator/each/
999
     * @see http://docs.mongodb.org/manual/reference/operator/slice/
1000
     * @see http://docs.mongodb.org/manual/reference/operator/sort/
1001
     *
1002
     * @param mixed|Expr $valueOrExpression
1003
     */
1004 8
    public function push($valueOrExpression) : self
1005
    {
1006 8
        if ($valueOrExpression instanceof Expr) {
1007 3
            $valueOrExpression = array_merge(
1008 3
                ['$each' => []],
1009 3
                $valueOrExpression->getQuery()
1010
            );
1011
        }
1012
1013 8
        $this->requiresCurrentField();
1014 8
        $this->newObj['$push'][$this->currentField] = $valueOrExpression;
1015 8
        return $this;
1016
    }
1017
1018
    /**
1019
     * Specify $gte and $lt criteria for the current field.
1020
     *
1021
     * This method is shorthand for specifying $gte criteria on the lower bound
1022
     * and $lt criteria on the upper bound. The upper bound is not inclusive.
1023
     *
1024
     * @see Builder::range()
1025
     *
1026
     * @param mixed $start
1027
     * @param mixed $end
1028
     */
1029 2
    public function range($start, $end) : self
1030
    {
1031 2
        return $this->operator('$gte', $start)->operator('$lt', $end);
1032
    }
1033
1034
    /**
1035
     * Checks that the value of the current field is a reference to the supplied document.
1036
     */
1037 13
    public function references(object $document) : self
1038
    {
1039 13
        $this->requiresCurrentField();
1040 13
        $mapping   = $this->getReferenceMapping();
1041 11
        $reference = $this->dm->createReference($document, $mapping);
1042 11
        $storeAs   = $mapping['storeAs'] ?? null;
1043 11
        $keys      = [];
1044
1045
        switch ($storeAs) {
1046 11
            case ClassMetadata::REFERENCE_STORE_AS_ID:
1047 4
                $this->query[$mapping['name']] = $reference;
1048 4
                return $this;
1049
                break;
1050
1051 8
            case ClassMetadata::REFERENCE_STORE_AS_REF:
1052
                $keys = ['id' => true];
1053
                break;
1054
1055 8
            case ClassMetadata::REFERENCE_STORE_AS_DB_REF:
1056 3
            case ClassMetadata::REFERENCE_STORE_AS_DB_REF_WITH_DB:
1057 8
                $keys = ['$ref' => true, '$id' => true, '$db' => true];
1058
1059 8
                if ($storeAs === ClassMetadata::REFERENCE_STORE_AS_DB_REF) {
1060 5
                    unset($keys['$db']);
1061
                }
1062
1063 8
                if (isset($mapping['targetDocument'])) {
1064 4
                    unset($keys['$ref'], $keys['$db']);
1065
                }
1066 8
                break;
1067
1068
            default:
1069
                throw new InvalidArgumentException(sprintf('Reference type %s is invalid.', $storeAs));
1070
        }
1071
1072 8
        foreach ($keys as $key => $value) {
1073 8
            $this->query[$mapping['name'] . '.' . $key] = $reference[$key];
1074
        }
1075
1076 8
        return $this;
1077
    }
1078
1079
    /**
1080
     * Rename the current field.
1081
     *
1082
     * @see Builder::rename()
1083
     * @see http://docs.mongodb.org/manual/reference/operator/rename/
1084
     */
1085
    public function rename(string $name) : self
1086
    {
1087
        $this->requiresCurrentField();
1088
        $this->newObj['$rename'][$this->currentField] = $name;
1089
        return $this;
1090
    }
1091
1092
    /**
1093
     * Set the current field to a value.
1094
     *
1095
     * This is only relevant for insert, update, or findAndUpdate queries. For
1096
     * update and findAndUpdate queries, the $atomic parameter will determine
1097
     * whether or not a $set operator is used.
1098
     *
1099
     * @see Builder::set()
1100
     * @see http://docs.mongodb.org/manual/reference/operator/set/
1101
     *
1102
     * @param mixed $value
1103
     */
1104 19
    public function set($value, bool $atomic = true) : self
1105
    {
1106 19
        $this->requiresCurrentField();
1107 19
        assert($this->currentField !== null);
1108
1109 19
        if ($atomic) {
1110 16
            $this->newObj['$set'][$this->currentField] = $value;
1111 16
            return $this;
1112
        }
1113
1114 3
        if (strpos($this->currentField, '.') === false) {
1115 2
            $this->newObj[$this->currentField] = $value;
1116 2
            return $this;
1117
        }
1118
1119 2
        $keys    = explode('.', $this->currentField);
1120 2
        $current = &$this->newObj;
1121 2
        foreach ($keys as $key) {
1122 2
            $current = &$current[$key];
1123
        }
1124 2
        $current = $value;
1125
1126 2
        return $this;
1127
    }
1128
1129
    /**
1130
     * Sets ClassMetadata for document being queried.
1131
     */
1132 390
    public function setClassMetadata(ClassMetadata $class) : void
1133
    {
1134 390
        $this->class = $class;
1135 390
    }
1136
1137
    /**
1138
     * Set the "new object".
1139
     *
1140
     * @see Builder::setNewObj()
1141
     */
1142
    public function setNewObj(array $newObj) : self
1143
    {
1144
        $this->newObj = $newObj;
1145
        return $this;
1146
    }
1147
1148
    /**
1149
     * Set the current field to the value if the document is inserted in an
1150
     * upsert operation.
1151
     *
1152
     * If an update operation with upsert: true results in an insert of a
1153
     * document, then $setOnInsert assigns the specified values to the fields in
1154
     * the document. If the update operation does not result in an insert,
1155
     * $setOnInsert does nothing.
1156
     *
1157
     * @see Builder::setOnInsert()
1158
     * @see https://docs.mongodb.org/manual/reference/operator/update/setOnInsert/
1159
     *
1160
     * @param mixed $value
1161
     */
1162 1
    public function setOnInsert($value) : self
1163
    {
1164 1
        $this->requiresCurrentField();
1165 1
        $this->newObj['$setOnInsert'][$this->currentField] = $value;
1166
1167 1
        return $this;
1168
    }
1169
1170
    /**
1171
     * Set the query criteria.
1172
     *
1173
     * @see Builder::setQueryArray()
1174
     */
1175 18
    public function setQuery(array $query) : self
1176
    {
1177 18
        $this->query = $query;
1178 18
        return $this;
1179
    }
1180
1181
    /**
1182
     * Specify $size criteria for the current field.
1183
     *
1184
     * @see Builder::size()
1185
     * @see http://docs.mongodb.org/manual/reference/operator/size/
1186
     */
1187
    public function size(int $size) : self
1188
    {
1189
        return $this->operator('$size', $size);
1190
    }
1191
1192
    /**
1193
     * Add $slice criteria to the expression for a $push operation.
1194
     *
1195
     * This is useful in conjunction with {@link Expr::each()} for a
1196
     * {@link Expr::push()} operation. {@link Builder::selectSlice()} should be
1197
     * used for specifying $slice for a query projection.
1198
     *
1199
     * @see http://docs.mongodb.org/manual/reference/operator/slice/
1200
     */
1201 2
    public function slice(int $slice) : self
1202
    {
1203 2
        return $this->operator('$slice', $slice);
1204
    }
1205
1206
    /**
1207
     * Add $sort criteria to the expression for a $push operation.
1208
     *
1209
     * If sorting by multiple fields, the first argument should be an array of
1210
     * field name (key) and order (value) pairs.
1211
     *
1212
     * This is useful in conjunction with {@link Expr::each()} for a
1213
     * {@link Expr::push()} operation. {@link Builder::sort()} should be used to
1214
     * sort the results of a query.
1215
     *
1216
     * @see http://docs.mongodb.org/manual/reference/operator/sort/
1217
     *
1218
     * @param array|string $fieldName Field name or array of field/order pairs
1219
     * @param int|string   $order     Field order (if one field is specified)
1220
     */
1221 2
    public function sort($fieldName, $order = null) : self
1222
    {
1223 2
        $fields = is_array($fieldName) ? $fieldName : [$fieldName => $order];
1224
1225
        return $this->operator('$sort', array_map(function ($order) {
1226 2
            return $this->normalizeSortOrder($order);
1227 2
        }, $fields));
1228
    }
1229
1230
    /**
1231
     * Specify $text criteria for the current query.
1232
     *
1233
     * The $language option may be set with {@link Expr::language()}.
1234
     *
1235
     * @see Builder::text()
1236
     * @see http://docs.mongodb.org/master/reference/operator/query/text/
1237
     */
1238 6
    public function text(string $search) : self
1239
    {
1240 6
        $this->query['$text'] = ['$search' => $search];
1241 6
        return $this;
1242
    }
1243
1244
    /**
1245
     * Specify $type criteria for the current field.
1246
     *
1247
     * @see Builder::type()
1248
     * @see http://docs.mongodb.org/manual/reference/operator/type/
1249
     *
1250
     * @param int|string $type
1251
     */
1252 1
    public function type($type) : self
1253
    {
1254 1
        return $this->operator('$type', $type);
1255
    }
1256
1257
    /**
1258
     * Unset the current field.
1259
     *
1260
     * The field will be removed from the document (not set to null).
1261
     *
1262
     * @see Builder::unsetField()
1263
     * @see http://docs.mongodb.org/manual/reference/operator/unset/
1264
     */
1265 3
    public function unsetField() : self
1266
    {
1267 3
        $this->requiresCurrentField();
1268 3
        $this->newObj['$unset'][$this->currentField] = 1;
1269 3
        return $this;
1270
    }
1271
1272
    /**
1273
     * Specify a JavaScript expression to use for matching documents.
1274
     *
1275
     * @see Builder::where()
1276
     * @see http://docs.mongodb.org/manual/reference/operator/where/
1277
     *
1278
     * @param string|Javascript $javascript
1279
     */
1280 3
    public function where($javascript) : self
1281
    {
1282 3
        $this->query['$where'] = $javascript;
1283 3
        return $this;
1284
    }
1285
1286
    /**
1287
     * Gets reference mapping for current field from current class or its descendants.
1288
     *
1289
     * @throws MappingException
1290
     */
1291 19
    private function getReferenceMapping() : array
1292
    {
1293 19
        $this->requiresCurrentField();
1294 19
        assert($this->currentField !== null);
1295
1296
        try {
1297 19
            return $this->class->getFieldMapping($this->currentField);
1298 6
        } catch (MappingException $e) {
1299 6
            if (empty($this->class->discriminatorMap)) {
1300
                throw $e;
1301
            }
1302 6
            $mapping = null;
1303 6
            $foundIn = null;
1304 6
            foreach ($this->class->discriminatorMap as $child) {
1305 6
                $childClass = $this->dm->getClassMetadata($child);
1306 6
                if (! $childClass->hasAssociation($this->currentField)) {
1307 4
                    continue;
1308
                }
1309
1310 4
                if ($foundIn !== null && $mapping !== null && $mapping !== $childClass->getFieldMapping($this->currentField)) {
1311 2
                    throw MappingException::referenceFieldConflict($this->currentField, $foundIn->name, $childClass->name);
0 ignored issues
show
Accessing name on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
1312
                }
1313 4
                $mapping = $childClass->getFieldMapping($this->currentField);
1314 4
                $foundIn = $childClass;
1315
            }
1316 4
            if ($mapping === null) {
1317 2
                throw MappingException::mappingNotFoundInClassNorDescendants($this->class->name, $this->currentField);
1318
            }
1319
1320 2
            return $mapping;
1321
        }
1322
    }
1323
1324
    /**
1325
     * @param int|string $order
1326
     */
1327 2
    private function normalizeSortOrder($order) : int
1328
    {
1329 2
        if (is_string($order)) {
1330
            $order = strtolower($order) === 'asc' ? 1 : -1;
1331
        }
1332
1333 2
        return $order;
1334
    }
1335
1336
    /**
1337
     * Ensure that a current field has been set.
1338
     *
1339
     * @throws LogicException If a current field has not been set.
1340
     */
1341 69
    private function requiresCurrentField() : void
1342
    {
1343 69
        if (! $this->currentField) {
1344
            throw new LogicException('This method requires you set a current field using field().');
1345
        }
1346 69
    }
1347
1348
    /**
1349
     * Wraps equality criteria with an operator.
1350
     *
1351
     * If equality criteria was previously specified for a field, it cannot be
1352
     * merged with other operators without first being wrapped in an operator of
1353
     * its own. Ideally, we would wrap it with $eq, but that is only available
1354
     * in MongoDB 2.8. Using a single-element $in is backwards compatible.
1355
     *
1356
     * @see Expr::operator()
1357
     */
1358 81
    private function wrapEqualityCriteria() : void
1359
    {
1360
        /* If the current field has no criteria yet, do nothing. This ensures
1361
         * that we do not inadvertently inject {"$in": null} into the query.
1362
         */
1363 81
        if ($this->currentField && ! isset($this->query[$this->currentField]) && ! array_key_exists($this->currentField, $this->query)) {
1364 55
            return;
1365
        }
1366
1367 31
        if ($this->currentField) {
1368 5
            $query = &$this->query[$this->currentField];
1369
        } else {
1370 27
            $query = &$this->query;
1371
        }
1372
1373
        /* If the query is an empty array, we'll assume that the user has not
1374
         * specified criteria. Otherwise, check if the array includes a query
1375
         * operator (checking the first key is sufficient). If neither of these
1376
         * conditions are met, we'll wrap the query value with $in.
1377
         */
1378 31
        if (is_array($query)) {
1379 31
            $key = key($query);
1380
1381 31
            if (empty($query) || (is_string($key) && strpos($key, '$') === 0)) {
1382 31
                return;
1383
            }
1384
        }
1385
1386 2
        $query = ['$in' => [$query]];
1387 2
    }
1388
}
1389