Completed
Push — master ( 4e10a4...a50624 )
by Jonathan
28:32 queued 19:49
created

Builder::count()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 7
ccs 4
cts 4
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\CachingIterator;
9
use Doctrine\ODM\MongoDB\Iterator\HydratingIterator;
10
use Doctrine\ODM\MongoDB\Iterator\Iterator;
11
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
12
use Doctrine\ODM\MongoDB\Persisters\DocumentPersister;
13
use Doctrine\ODM\MongoDB\Query\Expr as QueryExpr;
14
use GeoJson\Geometry\Point;
15
use MongoDB\Collection;
16
use MongoDB\Driver\Cursor;
17
use OutOfRangeException;
18
use function array_map;
19
use function array_merge;
20
use function array_unshift;
21
use function assert;
22
use function is_array;
23
use function sprintf;
24
25
/**
26
 * Fluent interface for building aggregation pipelines.
27
 */
28
class Builder
29
{
30
    /**
31
     * The DocumentManager instance for this query
32
     *
33
     * @var DocumentManager
34
     */
35
    private $dm;
36
37
    /**
38
     * The ClassMetadata instance.
39
     *
40
     * @var ClassMetadata
41
     */
42
    private $class;
43
44
    /** @var string */
45
    private $hydrationClass;
46
47
    /**
48
     * The Collection instance.
49
     *
50
     * @var Collection
51
     */
52
    private $collection;
53
54
    /** @var Stage[] */
55
    private $stages = [];
56
57
    /**
58
     * Create a new aggregation builder.
59
     */
60 255
    public function __construct(DocumentManager $dm, string $documentName)
61
    {
62 255
        $this->dm         = $dm;
63 255
        $this->class      = $this->dm->getClassMetadata($documentName);
64 255
        $this->collection = $this->dm->getDocumentCollection($documentName);
65 255
    }
66
67
    /**
68
     * Adds new fields to documents. $addFields outputs documents that contain all
69
     * existing fields from the input documents and newly added fields.
70
     *
71
     * The $addFields stage is equivalent to a $project stage that explicitly specifies
72
     * all existing fields in the input documents and adds the new fields.
73
     *
74
     * If the name of the new field is the same as an existing field name (including _id),
75
     * $addFields overwrites the existing value of that field with the value of the
76
     * specified expression.
77
     *
78
     * @see http://docs.mongodb.com/manual/reference/operator/aggregation/addFields/
79
     */
80 1
    public function addFields() : Stage\AddFields
81
    {
82 1
        $stage = new Stage\AddFields($this);
83 1
        $this->addStage($stage);
84
85 1
        return $stage;
86
    }
87
88
    /**
89
     * Categorizes incoming documents into groups, called buckets, based on a
90
     * specified expression and bucket boundaries.
91
     *
92
     * Each bucket is represented as a document in the output. The document for
93
     * each bucket contains an _id field, whose value specifies the inclusive
94
     * lower bound of the bucket and a count field that contains the number of
95
     * documents in the bucket. The count field is included by default when the
96
     * output is not specified.
97
     *
98
     * @see https://docs.mongodb.com/manual/reference/operator/aggregation/bucket/
99
     */
100 2
    public function bucket() : Stage\Bucket
101
    {
102 2
        $stage = new Stage\Bucket($this, $this->dm, $this->class);
103 2
        $this->addStage($stage);
104
105 2
        return $stage;
106
    }
107
108
    /**
109
     * Categorizes incoming documents into a specific number of groups, called
110
     * buckets, based on a specified expression.
111
     *
112
     * Bucket boundaries are automatically determined in an attempt to evenly
113
     * distribute the documents into the specified number of buckets. Each
114
     * bucket is represented as a document in the output. The document for each
115
     * bucket contains an _id field, whose value specifies the inclusive lower
116
     * bound and the exclusive upper bound for the bucket, and a count field
117
     * that contains the number of documents in the bucket. The count field is
118
     * included by default when the output is not specified.
119
     *
120
     * @see https://docs.mongodb.com/manual/reference/operator/aggregation/bucketAuto/
121
     */
122 2
    public function bucketAuto() : Stage\BucketAuto
123
    {
124 2
        $stage = new Stage\BucketAuto($this, $this->dm, $this->class);
125 2
        $this->addStage($stage);
126
127 2
        return $stage;
128
    }
129
130
    /**
131
     * Returns statistics regarding a collection or view.
132
     *
133
     * $collStats must be the first stage in an aggregation pipeline, or else
134
     * the pipeline returns an error.
135
     *
136
     * @see http://docs.mongodb.org/manual/reference/operator/aggregation/collStats/
137
     */
138 1
    public function collStats() : Stage\CollStats
139
    {
140 1
        $stage = new Stage\CollStats($this);
141 1
        $this->addStage($stage);
142
143 1
        return $stage;
144
    }
145
146
    /**
147
     * Returns a document that contains a count of the number of documents input
148
     * to the stage.
149
     *
150
     * @see https://docs.mongodb.com/manual/reference/operator/aggregation/count/
151
     */
152 1
    public function count(string $fieldName) : Stage\Count
153
    {
154 1
        $stage = new Stage\Count($this, $fieldName);
155 1
        $this->addStage($stage);
156
157 1
        return $stage;
158
    }
159
160
    /**
161
     * Executes the aggregation pipeline
162
     */
163 17
    public function execute(array $options = []) : Iterator
164
    {
165
        // Force cursor to be used
166 17
        $options = array_merge($options, ['cursor' => true]);
167
168 17
        $cursor = $this->collection->aggregate($this->getPipeline(), $options);
169 17
        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...
170
171 17
        return $this->prepareIterator($cursor);
172
    }
173
174 147
    public function expr() : Expr
175
    {
176 147
        return new Expr($this->dm, $this->class);
177
    }
178
179
    /**
180
     * Processes multiple aggregation pipelines within a single stage on the
181
     * same set of input documents.
182
     *
183
     * Each sub-pipeline has its own field in the output document where its
184
     * results are stored as an array of documents.
185
     */
186 1
    public function facet() : Stage\Facet
187
    {
188 1
        $stage = new Stage\Facet($this);
189 1
        $this->addStage($stage);
190
191 1
        return $stage;
192
    }
193
194
    /**
195
     * Outputs documents in order of nearest to farthest from a specified point.
196
     *
197
     * A GeoJSON point may be provided as the first and only argument for
198
     * 2dsphere queries. This single parameter may be a GeoJSON point object or
199
     * an array corresponding to the point's JSON representation. If GeoJSON is
200
     * used, the "spherical" option will default to true.
201
     *
202
     * You can only use this as the first stage of a pipeline.
203
     *
204
     * @see http://docs.mongodb.org/manual/reference/operator/aggregation/geoNear/
205
     *
206
     * @param float|array|Point $x
207
     * @param float             $y
208
     */
209 4
    public function geoNear($x, $y = null) : Stage\GeoNear
210
    {
211 4
        $stage = new Stage\GeoNear($this, $x, $y);
212 4
        $this->addStage($stage);
213
214 4
        return $stage;
215
    }
216
217
    /**
218
     * Returns the assembled aggregation pipeline
219
     *
220
     * For pipelines where the first stage is a $geoNear stage, it will apply
221
     * the document filters and discriminator queries to the query portion of
222
     * the geoNear operation. For all other pipelines, it prepends a $match stage
223
     * containing the required query.
224
     */
225 56
    public function getPipeline() : array
226
    {
227 56
        $pipeline = array_map(
228
            static function (Stage $stage) {
229 56
                return $stage->getExpression();
230 56
            },
231 56
            $this->stages
232
        );
233
234 56
        if ($this->getStage(0) instanceof Stage\GeoNear) {
235 4
            $pipeline[0]['$geoNear']['query'] = $this->applyFilters($pipeline[0]['$geoNear']['query']);
236 52
        } elseif ($this->getStage(0) instanceof Stage\IndexStats) {
237
            // Don't apply any filters when using an IndexStats stage: since it
238
            // needs to be the first pipeline stage, prepending a match stage
239
            // with discriminator information will not work
240
241 2
            return $pipeline;
242
        } else {
243 50
            $matchExpression = $this->applyFilters([]);
244 50
            if ($matchExpression !== []) {
245 1
                array_unshift($pipeline, ['$match' => $matchExpression]);
246
            }
247
        }
248
249 54
        return $pipeline;
250
    }
251
252
    /**
253
     * Returns a certain stage from the pipeline
254
     */
255 56
    public function getStage(int $index) : Stage
256
    {
257 56
        if (! isset($this->stages[$index])) {
258
            throw new OutOfRangeException(sprintf('Could not find stage with index %d.', $index));
259
        }
260
261 56
        return $this->stages[$index];
262
    }
263
264
    /**
265
     * Performs a recursive search on a collection, with options for restricting
266
     * the search by recursion depth and query filter.
267
     *
268
     * @see https://docs.mongodb.org/manual/reference/operator/aggregation/graphLookup/
269
     *
270
     * @param string $from Target collection for the $graphLookup operation to
271
     * search, recursively matching the connectFromField to the connectToField.
272
     */
273 10 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...
274
    {
275 10
        $stage = new Stage\GraphLookup($this, $from, $this->dm, $this->class);
276 9
        $this->addStage($stage);
277
278 9
        return $stage;
279
    }
280
281
    /**
282
     * Groups documents by some specified expression and outputs to the next
283
     * stage a document for each distinct grouping.
284
     *
285
     * @see http://docs.mongodb.org/manual/reference/operator/aggregation/group/
286
     */
287 4
    public function group() : Stage\Group
288
    {
289 4
        $stage = new Stage\Group($this);
290 4
        $this->addStage($stage);
291
292 4
        return $stage;
293
    }
294
295
    /**
296
     * Set which class to use when hydrating results as document class instances.
297
     */
298 4
    public function hydrate(string $className) : self
299
    {
300 4
        $this->hydrationClass = $className;
301
302 4
        return $this;
303
    }
304
305
    /**
306
     * Returns statistics regarding the use of each index for the collection.
307
     *
308
     * @see https://docs.mongodb.org/manual/reference/operator/aggregation/indexStats/
309
     */
310 2
    public function indexStats() : Stage\IndexStats
311
    {
312 2
        $stage = new Stage\IndexStats($this);
313 2
        $this->addStage($stage);
314
315 2
        return $stage;
316
    }
317
318
    /**
319
     * Limits the number of documents passed to the next stage in the pipeline.
320
     *
321
     * @see http://docs.mongodb.org/manual/reference/operator/aggregation/limit/
322
     */
323 2
    public function limit(int $limit) : Stage\Limit
324
    {
325 2
        $stage = new Stage\Limit($this, $limit);
326 2
        $this->addStage($stage);
327
328 2
        return $stage;
329
    }
330
331
    /**
332
     * Performs a left outer join to an unsharded collection in the same
333
     * database to filter in documents from the “joined” collection for
334
     * processing.
335
     *
336
     * @see https://docs.mongodb.org/manual/reference/operator/aggregation/lookup/
337
     */
338 14 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...
339
    {
340 14
        $stage = new Stage\Lookup($this, $from, $this->dm, $this->class);
341 12
        $this->addStage($stage);
342
343 12
        return $stage;
344
    }
345
346
    /**
347
     * Filters the documents to pass only the documents that match the specified
348
     * condition(s) to the next pipeline stage.
349
     *
350
     * @see http://docs.mongodb.org/manual/reference/operator/aggregation/match/
351
     */
352 8
    public function match() : Stage\Match
353
    {
354 8
        $stage = new Stage\Match($this);
355 8
        $this->addStage($stage);
356
357 8
        return $stage;
358
    }
359
360
    /**
361
     * Returns a query expression to be used in match stages
362
     */
363 60
    public function matchExpr() : QueryExpr
364
    {
365 60
        $expr = new QueryExpr($this->dm);
366 60
        $expr->setClassMetadata($this->class);
367
368 60
        return $expr;
369
    }
370
371
    /**
372
     * Takes the documents returned by the aggregation pipeline and writes them
373
     * to a specified collection. This must be the last stage in the pipeline.
374
     *
375
     * @see http://docs.mongodb.org/manual/reference/operator/aggregation/out/
376
     */
377 6
    public function out(string $from) : Stage\Out
378
    {
379 6
        $stage = new Stage\Out($this, $from, $this->dm);
380 5
        $this->addStage($stage);
381
382 5
        return $stage;
383
    }
384
385
    /**
386
     * Passes along the documents with only the specified fields to the next
387
     * stage in the pipeline. The specified fields can be existing fields from
388
     * the input documents or newly computed fields.
389
     *
390
     * @see http://docs.mongodb.org/manual/reference/operator/aggregation/project/
391
     */
392 4
    public function project() : Stage\Project
393
    {
394 4
        $stage = new Stage\Project($this);
395 4
        $this->addStage($stage);
396
397 4
        return $stage;
398
    }
399
400
    /**
401
     * Restricts the contents of the documents based on information stored in
402
     * the documents themselves.
403
     *
404
     * @see http://docs.mongodb.org/manual/reference/operator/aggregation/redact/
405
     */
406 2
    public function redact() : Stage\Redact
407
    {
408 2
        $stage = new Stage\Redact($this);
409 2
        $this->addStage($stage);
410
411 2
        return $stage;
412
    }
413
414
    /**
415
     * Promotes a specified document to the top level and replaces all other
416
     * fields.
417
     *
418
     * The operation replaces all existing fields in the input document,
419
     * including the _id field. You can promote an existing embedded document to
420
     * the top level, or create a new document for promotion.
421
     *
422
     * @param string|array|null $expression Optional. A replacement expression that
423
     * resolves to a document.
424
     */
425 6 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...
426
    {
427 6
        $stage = new Stage\ReplaceRoot($this, $this->dm, $this->class, $expression);
428 6
        $this->addStage($stage);
429
430 6
        return $stage;
431
    }
432
433
    /**
434
     * Randomly selects the specified number of documents from its input.
435
     *
436
     * @see https://docs.mongodb.org/manual/reference/operator/aggregation/sample/
437
     */
438 2
    public function sample(int $size) : Stage\Sample
439
    {
440 2
        $stage = new Stage\Sample($this, $size);
441 2
        $this->addStage($stage);
442
443 2
        return $stage;
444
    }
445
446
    /**
447
     * Skips over the specified number of documents that pass into the stage and
448
     * passes the remaining documents to the next stage in the pipeline.
449
     *
450
     * @see http://docs.mongodb.org/manual/reference/operator/aggregation/skip/
451
     */
452 2
    public function skip(int $skip) : Stage\Skip
453
    {
454 2
        $stage = new Stage\Skip($this, $skip);
455 2
        $this->addStage($stage);
456
457 2
        return $stage;
458
    }
459
460
    /**
461
     * Sorts all input documents and returns them to the pipeline in sorted
462
     * order.
463
     *
464
     * If sorting by multiple fields, the first argument should be an array of
465
     * field name (key) and order (value) pairs.
466
     *
467
     * @see http://docs.mongodb.org/manual/reference/operator/aggregation/sort/
468
     *
469
     * @param array|string $fieldName Field name or array of field/order pairs
470
     * @param int|string   $order     Field order (if one field is specified)
471
     */
472 7
    public function sort($fieldName, $order = null) : Stage\Sort
473
    {
474 7
        $fields = is_array($fieldName) ? $fieldName : [$fieldName => $order];
475
        // fixme: move to sort stage
476 7
        $stage = new Stage\Sort($this, $this->getDocumentPersister()->prepareSort($fields));
477 7
        $this->addStage($stage);
478
479 7
        return $stage;
480
    }
481
482
    /**
483
     * Groups incoming documents based on the value of a specified expression,
484
     * then computes the count of documents in each distinct group.
485
     *
486
     * @see http://docs.mongodb.org/manual/reference/operator/aggregation/sortByCount/
487
     */
488 3 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...
489
    {
490 3
        $stage = new Stage\SortByCount($this, $expression, $this->dm, $this->class);
491 3
        $this->addStage($stage);
492
493 3
        return $stage;
494
    }
495
496
    /**
497
     * Deconstructs an array field from the input documents to output a document
498
     * for each element. Each output document is the input document with the
499
     * value of the array field replaced by the element.
500
     *
501
     * @see http://docs.mongodb.org/manual/reference/operator/aggregation/unwind/
502
     */
503 7
    public function unwind(string $fieldName) : Stage\Unwind
504
    {
505
        // Fixme: move field name translation to stage
506 7
        $stage = new Stage\Unwind($this, $this->getDocumentPersister()->prepareFieldName($fieldName));
507 7
        $this->addStage($stage);
508
509 7
        return $stage;
510
    }
511
512
    /**
513
     * Allows adding an arbitrary stage to the pipeline
514
     *
515
     * @return Stage The method returns the stage given as an argument
516
     */
517 62
    public function addStage(Stage $stage) : Stage
518
    {
519 62
        $this->stages[] = $stage;
520
521 62
        return $stage;
522
    }
523
524
    /**
525
     * Applies filters and discriminator queries to the pipeline
526
     */
527 54
    private function applyFilters(array $query) : array
528
    {
529 54
        $documentPersister = $this->dm->getUnitOfWork()->getDocumentPersister($this->class->name);
530
531 54
        $query = $documentPersister->addDiscriminatorToPreparedQuery($query);
532 54
        $query = $documentPersister->addFilterToPreparedQuery($query);
533
534 54
        return $query;
535
    }
536
537 11
    private function getDocumentPersister() : DocumentPersister
538
    {
539 11
        return $this->dm->getUnitOfWork()->getDocumentPersister($this->class->name);
540
    }
541
542 17
    private function prepareIterator(Cursor $cursor) : Iterator
543
    {
544 17
        $class = null;
545 17
        if ($this->hydrationClass) {
546 4
            $class = $this->dm->getClassMetadata($this->hydrationClass);
547
        }
548
549 17
        if ($class) {
550 4
            $cursor = new HydratingIterator($cursor, $this->dm->getUnitOfWork(), $class);
551
        }
552
553 17
        return new CachingIterator($cursor);
554
    }
555
}
556