Completed
Pull Request — master (#1803)
by Maciej
20:22
created

Builder::bucketAuto()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

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