Completed
Pull Request — master (#2116)
by
unknown
24:13
created

Builder::sortByCount()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7

Duplication

Lines 7
Ratio 100 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
dl 7
loc 7
ccs 3
cts 3
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ODM\MongoDB\Aggregation;
6
7
use Doctrine\ODM\MongoDB\DocumentManager;
8
use Doctrine\ODM\MongoDB\Iterator\UnrewindableIterator;
9
use Doctrine\ODM\MongoDB\Iterator\CachingIterator;
10
use Doctrine\ODM\MongoDB\Iterator\HydratingIterator;
11
use Doctrine\ODM\MongoDB\Iterator\Iterator;
12
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
13
use Doctrine\ODM\MongoDB\Persisters\DocumentPersister;
14
use Doctrine\ODM\MongoDB\Query\Expr as QueryExpr;
15
use GeoJson\Geometry\Point;
16
use MongoDB\Collection;
17
use MongoDB\Driver\Cursor;
18
use OutOfRangeException;
19
use function array_map;
20
use function array_merge;
21
use function array_unshift;
22
use function assert;
23
use function is_array;
24
use function sprintf;
25
26
/**
27
 * Fluent interface for building aggregation pipelines.
28
 */
29
class Builder
30
{
31
    /**
32
     * The DocumentManager instance for this query
33
     *
34
     * @var DocumentManager
35
     */
36
    private $dm;
37
38
    /**
39
     * The ClassMetadata instance.
40
     *
41
     * @var ClassMetadata
42
     */
43
    private $class;
44
45
    /** @var string */
46
    private $hydrationClass;
47
48
    /**
49
     * The Collection instance.
50
     *
51
     * @var Collection
52
     */
53
    private $collection;
54
55
    /** @var Stage[] */
56
    private $stages = [];
57
    
58
    /** @var bool */
59
    private $rewindable = true;
60 255
61
    /**
62 255
     * Create a new aggregation builder.
63 255
     */
64 255
    public function __construct(DocumentManager $dm, string $documentName)
65 255
    {
66
        $this->dm         = $dm;
67
        $this->class      = $this->dm->getClassMetadata($documentName);
68
        $this->collection = $this->dm->getDocumentCollection($documentName);
69
    }
70
71
    /**
72
     * Adds new fields to documents. $addFields outputs documents that contain all
73
     * existing fields from the input documents and newly added fields.
74
     *
75
     * The $addFields stage is equivalent to a $project stage that explicitly specifies
76
     * all existing fields in the input documents and adds the new fields.
77
     *
78
     * If the name of the new field is the same as an existing field name (including _id),
79
     * $addFields overwrites the existing value of that field with the value of the
80 1
     * specified expression.
81
     *
82 1
     * @see http://docs.mongodb.com/manual/reference/operator/aggregation/addFields/
83 1
     */
84
    public function addFields() : Stage\AddFields
85 1
    {
86
        $stage = new Stage\AddFields($this);
87
        $this->addStage($stage);
88
89
        return $stage;
90
    }
91
92
    /**
93
     * Categorizes incoming documents into groups, called buckets, based on a
94
     * specified expression and bucket boundaries.
95
     *
96
     * Each bucket is represented as a document in the output. The document for
97
     * each bucket contains an _id field, whose value specifies the inclusive
98
     * lower bound of the bucket and a count field that contains the number of
99
     * documents in the bucket. The count field is included by default when the
100 2
     * output is not specified.
101
     *
102 2
     * @see https://docs.mongodb.com/manual/reference/operator/aggregation/bucket/
103 2
     */
104
    public function bucket() : Stage\Bucket
105 2
    {
106
        $stage = new Stage\Bucket($this, $this->dm, $this->class);
107
        $this->addStage($stage);
108
109
        return $stage;
110
    }
111
112
    /**
113
     * Categorizes incoming documents into a specific number of groups, called
114
     * buckets, based on a specified expression.
115
     *
116
     * Bucket boundaries are automatically determined in an attempt to evenly
117
     * distribute the documents into the specified number of buckets. Each
118
     * bucket is represented as a document in the output. The document for each
119
     * bucket contains an _id field, whose value specifies the inclusive lower
120
     * bound and the exclusive upper bound for the bucket, and a count field
121
     * that contains the number of documents in the bucket. The count field is
122 2
     * included by default when the output is not specified.
123
     *
124 2
     * @see https://docs.mongodb.com/manual/reference/operator/aggregation/bucketAuto/
125 2
     */
126
    public function bucketAuto() : Stage\BucketAuto
127 2
    {
128
        $stage = new Stage\BucketAuto($this, $this->dm, $this->class);
129
        $this->addStage($stage);
130
131
        return $stage;
132
    }
133
134
    /**
135
     * Returns statistics regarding a collection or view.
136
     *
137
     * $collStats must be the first stage in an aggregation pipeline, or else
138 1
     * the pipeline returns an error.
139
     *
140 1
     * @see http://docs.mongodb.org/manual/reference/operator/aggregation/collStats/
141 1
     */
142
    public function collStats() : Stage\CollStats
143 1
    {
144
        $stage = new Stage\CollStats($this);
145
        $this->addStage($stage);
146
147
        return $stage;
148
    }
149
150
    /**
151
     * Returns a document that contains a count of the number of documents input
152 1
     * to the stage.
153
     *
154 1
     * @see https://docs.mongodb.com/manual/reference/operator/aggregation/count/
155 1
     */
156
    public function count(string $fieldName) : Stage\Count
157 1
    {
158
        $stage = new Stage\Count($this, $fieldName);
159
        $this->addStage($stage);
160
161
        return $stage;
162
    }
163 17
164
    /**
165
     * Executes the aggregation pipeline
166 17
     */
167
    public function execute(array $options = []) : Iterator
168 17
    {
169 17
        // Force cursor to be used
170
        $options = array_merge($options, ['cursor' => true]);
171 17
172
        $cursor = $this->collection->aggregate($this->getPipeline(), $options);
173
        assert($cursor instanceof Cursor);
0 ignored issues
show
Bug introduced by
The class MongoDB\Driver\Cursor does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
174 147
175
        return $this->prepareIterator($cursor);
176 147
    }
177
178
    public function expr() : Expr
179
    {
180
        return new Expr($this->dm, $this->class);
181
    }
182
183
    /**
184
     * Processes multiple aggregation pipelines within a single stage on the
185
     * same set of input documents.
186 1
     *
187
     * Each sub-pipeline has its own field in the output document where its
188 1
     * results are stored as an array of documents.
189 1
     */
190
    public function facet() : Stage\Facet
191 1
    {
192
        $stage = new Stage\Facet($this);
193
        $this->addStage($stage);
194
195
        return $stage;
196
    }
197
198
    /**
199
     * Outputs documents in order of nearest to farthest from a specified point.
200
     *
201
     * A GeoJSON point may be provided as the first and only argument for
202
     * 2dsphere queries. This single parameter may be a GeoJSON point object or
203
     * an array corresponding to the point's JSON representation. If GeoJSON is
204
     * used, the "spherical" option will default to true.
205
     *
206
     * You can only use this as the first stage of a pipeline.
207
     *
208
     * @see http://docs.mongodb.org/manual/reference/operator/aggregation/geoNear/
209 4
     *
210
     * @param float|array|Point $x
211 4
     * @param float             $y
212 4
     */
213
    public function geoNear($x, $y = null) : Stage\GeoNear
214 4
    {
215
        $stage = new Stage\GeoNear($this, $x, $y);
216
        $this->addStage($stage);
217
218
        return $stage;
219
    }
220
221
    /**
222
     * Returns the assembled aggregation pipeline
223
     *
224
     * For pipelines where the first stage is a $geoNear stage, it will apply
225 56
     * the document filters and discriminator queries to the query portion of
226
     * the geoNear operation. For all other pipelines, it prepends a $match stage
227 56
     * containing the required query.
228
     */
229 56
    public function getPipeline() : array
230 56
    {
231 56
        $pipeline = array_map(
232
            static function (Stage $stage) {
233
                return $stage->getExpression();
234 56
            },
235 4
            $this->stages
236 52
        );
237
238
        if ($this->getStage(0) instanceof Stage\GeoNear) {
239
            $pipeline[0]['$geoNear']['query'] = $this->applyFilters($pipeline[0]['$geoNear']['query']);
240
        } elseif ($this->getStage(0) instanceof Stage\IndexStats) {
241 2
            // Don't apply any filters when using an IndexStats stage: since it
242
            // needs to be the first pipeline stage, prepending a match stage
243 50
            // with discriminator information will not work
244 50
245 1
            return $pipeline;
246
        } else {
247
            $matchExpression = $this->applyFilters([]);
248
            if ($matchExpression !== []) {
249 54
                array_unshift($pipeline, ['$match' => $matchExpression]);
250
            }
251
        }
252
253
        return $pipeline;
254
    }
255 56
256
    /**
257 56
     * Returns a certain stage from the pipeline
258
     */
259
    public function getStage(int $index) : Stage
260
    {
261 56
        if (! isset($this->stages[$index])) {
262
            throw new OutOfRangeException(sprintf('Could not find stage with index %d.', $index));
263
        }
264
265
        return $this->stages[$index];
266
    }
267
268
    /**
269
     * Performs a recursive search on a collection, with options for restricting
270
     * the search by recursion depth and query filter.
271
     *
272
     * @see https://docs.mongodb.org/manual/reference/operator/aggregation/graphLookup/
273 10
     *
274
     * @param string $from Target collection for the $graphLookup operation to
275 10
     * search, recursively matching the connectFromField to the connectToField.
276 9
     */
277 View Code Duplication
    public function graphLookup(string $from) : Stage\GraphLookup
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
278 9
    {
279
        $stage = new Stage\GraphLookup($this, $from, $this->dm, $this->class);
280
        $this->addStage($stage);
281
282
        return $stage;
283
    }
284
285
    /**
286
     * Groups documents by some specified expression and outputs to the next
287 4
     * stage a document for each distinct grouping.
288
     *
289 4
     * @see http://docs.mongodb.org/manual/reference/operator/aggregation/group/
290 4
     */
291
    public function group() : Stage\Group
292 4
    {
293
        $stage = new Stage\Group($this);
294
        $this->addStage($stage);
295
296
        return $stage;
297
    }
298 4
299
    /**
300 4
     * Set which class to use when hydrating results as document class instances.
301
     */
302 4
    public function hydrate(string $className) : self
303
    {
304
        $this->hydrationClass = $className;
305
306
        return $this;
307
    }
308
309
    /**
310 2
     * Returns statistics regarding the use of each index for the collection.
311
     *
312 2
     * @see https://docs.mongodb.org/manual/reference/operator/aggregation/indexStats/
313 2
     */
314
    public function indexStats() : Stage\IndexStats
315 2
    {
316
        $stage = new Stage\IndexStats($this);
317
        $this->addStage($stage);
318
319
        return $stage;
320
    }
321
322
    /**
323 2
     * Limits the number of documents passed to the next stage in the pipeline.
324
     *
325 2
     * @see http://docs.mongodb.org/manual/reference/operator/aggregation/limit/
326 2
     */
327
    public function limit(int $limit) : Stage\Limit
328 2
    {
329
        $stage = new Stage\Limit($this, $limit);
330
        $this->addStage($stage);
331
332
        return $stage;
333
    }
334
335
    /**
336
     * Performs a left outer join to an unsharded collection in the same
337
     * database to filter in documents from the “joined” collection for
338 14
     * processing.
339
     *
340 14
     * @see https://docs.mongodb.org/manual/reference/operator/aggregation/lookup/
341 12
     */
342 View Code Duplication
    public function lookup(string $from) : Stage\Lookup
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
343 12
    {
344
        $stage = new Stage\Lookup($this, $from, $this->dm, $this->class);
345
        $this->addStage($stage);
346
347
        return $stage;
348
    }
349
350
    /**
351
     * Filters the documents to pass only the documents that match the specified
352 8
     * condition(s) to the next pipeline stage.
353
     *
354 8
     * @see http://docs.mongodb.org/manual/reference/operator/aggregation/match/
355 8
     */
356
    public function match() : Stage\Match
357 8
    {
358
        $stage = new Stage\Match($this);
359
        $this->addStage($stage);
360
361
        return $stage;
362
    }
363 60
364
    /**
365 60
     * Returns a query expression to be used in match stages
366 60
     */
367
    public function matchExpr() : QueryExpr
368 60
    {
369
        $expr = new QueryExpr($this->dm);
370
        $expr->setClassMetadata($this->class);
371
372
        return $expr;
373
    }
374
375
    /**
376
     * Takes the documents returned by the aggregation pipeline and writes them
377 6
     * to a specified collection. This must be the last stage in the pipeline.
378
     *
379 6
     * @see http://docs.mongodb.org/manual/reference/operator/aggregation/out/
380 5
     */
381
    public function out(string $from) : Stage\Out
382 5
    {
383
        $stage = new Stage\Out($this, $from, $this->dm);
384
        $this->addStage($stage);
385
386
        return $stage;
387
    }
388
389
    /**
390
     * Passes along the documents with only the specified fields to the next
391
     * stage in the pipeline. The specified fields can be existing fields from
392 4
     * the input documents or newly computed fields.
393
     *
394 4
     * @see http://docs.mongodb.org/manual/reference/operator/aggregation/project/
395 4
     */
396
    public function project() : Stage\Project
397 4
    {
398
        $stage = new Stage\Project($this);
399
        $this->addStage($stage);
400
401
        return $stage;
402
    }
403
404
    /**
405
     * Restricts the contents of the documents based on information stored in
406 2
     * the documents themselves.
407
     *
408 2
     * @see http://docs.mongodb.org/manual/reference/operator/aggregation/redact/
409 2
     */
410
    public function redact() : Stage\Redact
411 2
    {
412
        $stage = new Stage\Redact($this);
413
        $this->addStage($stage);
414
415
        return $stage;
416
    }
417
418
    /**
419
     * Promotes a specified document to the top level and replaces all other
420
     * fields.
421
     *
422
     * The operation replaces all existing fields in the input document,
423
     * including the _id field. You can promote an existing embedded document to
424
     * the top level, or create a new document for promotion.
425 6
     *
426
     * @param string|array|null $expression Optional. A replacement expression that
427 6
     * resolves to a document.
428 6
     */
429 View Code Duplication
    public function replaceRoot($expression = null) : Stage\ReplaceRoot
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
430 6
    {
431
        $stage = new Stage\ReplaceRoot($this, $this->dm, $this->class, $expression);
432
        $this->addStage($stage);
433
434
        return $stage;
435
    }
436
437
    /**
438 2
     * Controls if resulting iterator should be wrapped with CachingIterator.
439
     */
440 2
    public function rewindable(bool $rewindable = true) : self
441 2
    {
442
        $this->rewindable = $rewindable;
443 2
        
444
        return $this;
445
    }
446
447
    /**
448
     * Randomly selects the specified number of documents from its input.
449
     *
450
     * @see https://docs.mongodb.org/manual/reference/operator/aggregation/sample/
451
     */
452 2
    public function sample(int $size) : Stage\Sample
453
    {
454 2
        $stage = new Stage\Sample($this, $size);
455 2
        $this->addStage($stage);
456
457 2
        return $stage;
458
    }
459
460
    /**
461
     * Skips over the specified number of documents that pass into the stage and
462
     * passes the remaining documents to the next stage in the pipeline.
463
     *
464
     * @see http://docs.mongodb.org/manual/reference/operator/aggregation/skip/
465
     */
466
    public function skip(int $skip) : Stage\Skip
467
    {
468
        $stage = new Stage\Skip($this, $skip);
469
        $this->addStage($stage);
470
471
        return $stage;
472 7
    }
473
474 7
    /**
475
     * Sorts all input documents and returns them to the pipeline in sorted
476 7
     * order.
477 7
     *
478
     * If sorting by multiple fields, the first argument should be an array of
479 7
     * field name (key) and order (value) pairs.
480
     *
481
     * @see http://docs.mongodb.org/manual/reference/operator/aggregation/sort/
482
     *
483
     * @param array|string $fieldName Field name or array of field/order pairs
484
     * @param int|string   $order     Field order (if one field is specified)
485
     */
486
    public function sort($fieldName, $order = null) : Stage\Sort
487
    {
488 3
        $fields = is_array($fieldName) ? $fieldName : [$fieldName => $order];
489
        // fixme: move to sort stage
490 3
        $stage = new Stage\Sort($this, $this->getDocumentPersister()->prepareSort($fields));
491 3
        $this->addStage($stage);
492
493 3
        return $stage;
494
    }
495
496
    /**
497
     * Groups incoming documents based on the value of a specified expression,
498
     * then computes the count of documents in each distinct group.
499
     *
500
     * @see http://docs.mongodb.org/manual/reference/operator/aggregation/sortByCount/
501
     */
502 View Code Duplication
    public function sortByCount(string $expression) : Stage\SortByCount
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
503 7
    {
504
        $stage = new Stage\SortByCount($this, $expression, $this->dm, $this->class);
505
        $this->addStage($stage);
506 7
507 7
        return $stage;
508
    }
509 7
510
    /**
511
     * Deconstructs an array field from the input documents to output a document
512
     * for each element. Each output document is the input document with the
513
     * value of the array field replaced by the element.
514
     *
515
     * @see http://docs.mongodb.org/manual/reference/operator/aggregation/unwind/
516
     */
517 62
    public function unwind(string $fieldName) : Stage\Unwind
518
    {
519 62
        // Fixme: move field name translation to stage
520
        $stage = new Stage\Unwind($this, $this->getDocumentPersister()->prepareFieldName($fieldName));
521 62
        $this->addStage($stage);
522
523
        return $stage;
524
    }
525
526
    /**
527 54
     * Allows adding an arbitrary stage to the pipeline
528
     *
529 54
     * @return Stage The method returns the stage given as an argument
530
     */
531 54
    public function addStage(Stage $stage) : Stage
532 54
    {
533
        $this->stages[] = $stage;
534 54
535
        return $stage;
536
    }
537 11
538
    /**
539 11
     * Applies filters and discriminator queries to the pipeline
540
     */
541
    private function applyFilters(array $query) : array
542 17
    {
543
        $documentPersister = $this->dm->getUnitOfWork()->getDocumentPersister($this->class->name);
544 17
545 17
        $query = $documentPersister->addDiscriminatorToPreparedQuery($query);
546 4
        $query = $documentPersister->addFilterToPreparedQuery($query);
547
548
        return $query;
549 17
    }
550 4
551
    private function getDocumentPersister() : DocumentPersister
552
    {
553 17
        return $this->dm->getUnitOfWork()->getDocumentPersister($this->class->name);
554
    }
555
556
    private function prepareIterator(Cursor $cursor) : Iterator
557
    {
558
        $class = null;
559
        if ($this->hydrationClass) {
560
            $class = $this->dm->getClassMetadata($this->hydrationClass);
561
        }
562
563
        if ($class) {
564
            $cursor = new HydratingIterator($cursor, $this->dm->getUnitOfWork(), $class);
565
        }
566
567
        $cursor = $this->rewindable ? new CachingIterator($cursor) : new UnrewindableIterator($cursor);
568
569
        return $cursor;
570
    }
571
}
572