Completed
Push — master ( 11f27c...4713a5 )
by Andreas
08:33
created

Builder   D

Complexity

Total Complexity 39

Size/Duplication

Total Lines 536
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 32

Test Coverage

Coverage 98.92%

Importance

Changes 0
Metric Value
wmc 39
lcom 1
cbo 32
dl 0
loc 536
ccs 92
cts 93
cp 0.9892
rs 4.2439
c 0
b 0
f 0

33 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A addFields() 0 4 1
A bucket() 0 4 1
A bucketAuto() 0 4 1
A collStats() 0 4 1
A count() 0 4 1
A execute() 0 9 1
A expr() 0 4 1
A facet() 0 4 1
A geoNear() 0 4 1
A getPipeline() 0 18 3
A getStage() 0 8 2
A graphLookup() 0 4 1
A group() 0 4 1
A hydrate() 0 6 1
A indexStats() 0 4 1
A limit() 0 4 1
A lookup() 0 4 1
A match() 0 4 1
A matchExpr() 0 7 1
A out() 0 4 1
A project() 0 4 1
A redact() 0 4 1
A replaceRoot() 0 4 1
A sample() 0 4 1
A skip() 0 4 1
A sort() 0 6 2
A sortByCount() 0 4 1
A unwind() 0 5 1
A addStage() 0 6 1
A applyFilters() 0 9 1
A getDocumentPersister() 0 4 1
A prepareIterator() 0 13 3
1
<?php
2
/*
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license. For more information, see
17
 * <http://www.doctrine-project.org>.
18
 */
19
20
namespace Doctrine\ODM\MongoDB\Aggregation;
21
22
use Doctrine\ODM\MongoDB\DocumentManager;
23
use Doctrine\ODM\MongoDB\Iterator\CachingIterator;
24
use Doctrine\ODM\MongoDB\Iterator\HydratingIterator;
25
use Doctrine\ODM\MongoDB\Iterator\Iterator;
26
use Doctrine\ODM\MongoDB\Query\Expr as QueryExpr;
27
use GeoJson\Geometry\Point;
28
use MongoDB\Collection;
29
use MongoDB\Driver\Cursor;
30
31
/**
32
 * Fluent interface for building aggregation pipelines.
33
 */
34
class Builder
35
{
36
    /**
37
     * The DocumentManager instance for this query
38
     *
39
     * @var DocumentManager
40
     */
41
    private $dm;
42
43
    /**
44
     * The ClassMetadata instance.
45
     *
46
     * @var \Doctrine\ODM\MongoDB\Mapping\ClassMetadata
47
     */
48
    private $class;
49
50
    /**
51
     * @var string
52
     */
53
    private $hydrationClass;
54
55
    /**
56
     * The Collection instance.
57
     *
58
     * @var Collection
59
     */
60
    private $collection;
61
62
    /**
63
     * @var Stage[]
64
     */
65
    private $stages = [];
66
67
    /**
68
     * Create a new aggregation builder.
69
     *
70
     * @param DocumentManager $dm
71
     * @param string $documentName
72
     */
73 254
    public function __construct(DocumentManager $dm, $documentName)
74
    {
75 254
        $this->dm = $dm;
76 254
        $this->class = $this->dm->getClassMetadata($documentName);
77 254
        $this->collection = $this->dm->getDocumentCollection($documentName);
78 254
    }
79
80
    /**
81
     * Adds new fields to documents. $addFields outputs documents that contain all
82
     * existing fields from the input documents and newly added fields.
83
     *
84
     * The $addFields stage is equivalent to a $project stage that explicitly specifies
85
     * all existing fields in the input documents and adds the new fields.
86
     *
87
     * If the name of the new field is the same as an existing field name (including _id),
88
     * $addFields overwrites the existing value of that field with the value of the
89
     * specified expression.
90
     *
91
     * @see http://docs.mongodb.com/manual/reference/operator/aggregation/addFields/
92
     *
93
     * @return Stage\AddFields
94
     */
95 1
    public function addFields()
96
    {
97 1
        return $this->addStage(new Stage\AddFields($this));
98
    }
99
100
    /**
101
     * Categorizes incoming documents into groups, called buckets, based on a
102
     * specified expression and bucket boundaries.
103
     *
104
     * Each bucket is represented as a document in the output. The document for
105
     * each bucket contains an _id field, whose value specifies the inclusive
106
     * lower bound of the bucket and a count field that contains the number of
107
     * documents in the bucket. The count field is included by default when the
108
     * output is not specified.
109
     *
110
     * @see https://docs.mongodb.com/manual/reference/operator/aggregation/bucket/
111
     *
112
     * @return Stage\Bucket
113
     */
114 2
    public function bucket()
115
    {
116 2
        return $this->addStage(new Stage\Bucket($this, $this->dm, $this->class));
117
    }
118
119
    /**
120
     * Categorizes incoming documents into a specific number of groups, called
121
     * buckets, based on a specified expression.
122
     *
123
     * Bucket boundaries are automatically determined in an attempt to evenly
124
     * distribute the documents into the specified number of buckets. Each
125
     * bucket is represented as a document in the output. The document for each
126
     * bucket contains an _id field, whose value specifies the inclusive lower
127
     * bound and the exclusive upper bound for the bucket, and a count field
128
     * that contains the number of documents in the bucket. The count field is
129
     * included by default when the output is not specified.
130
     *
131
     * @see https://docs.mongodb.com/manual/reference/operator/aggregation/bucketAuto/
132
     *
133
     * @return Stage\BucketAuto
134
     */
135 2
    public function bucketAuto()
136
    {
137 2
        return $this->addStage(new Stage\BucketAuto($this, $this->dm, $this->class));
138
    }
139
140
    /**
141
     * Returns statistics regarding a collection or view.
142
     *
143
     * $collStats must be the first stage in an aggregation pipeline, or else
144
     * the pipeline returns an error.
145
     *
146
     * @see http://docs.mongodb.org/manual/reference/operator/aggregation/collStats/
147
     * @since 1.5
148
     *
149
     * @return Stage\CollStats
150
     */
151 1
    public function collStats()
152
    {
153 1
        return $this->addStage(new Stage\CollStats($this));
154
    }
155
156
    /**
157
     * Returns a document that contains a count of the number of documents input
158
     * to the stage.
159
     *
160
     * @see https://docs.mongodb.com/manual/reference/operator/aggregation/count/
161
     *
162
     * @return Stage\Count
163
     */
164 1
    public function count($fieldName)
165
    {
166 1
        return $this->addStage(new Stage\Count($this, $fieldName));
167
    }
168
169
    /**
170
     * Executes the aggregation pipeline
171
     *
172
     * @param array $options
173
     * @return Iterator
174
     */
175 17
    public function execute($options = [])
176
    {
177
        // Force cursor to be used
178 17
        $options = array_merge($options, ['cursor' => true]);
179
180 17
        $cursor = $this->collection->aggregate($this->getPipeline(), $options);
181
182 17
        return $this->prepareIterator($cursor);
0 ignored issues
show
Documentation introduced by
$cursor is of type object<Traversable>, but the function expects a object<MongoDB\Driver\Cursor>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
183
    }
184
185
    /**
186
     * @return Expr
187
     */
188 147
    public function expr()
189
    {
190 147
        return new Expr($this->dm, $this->class);
191
    }
192
193
    /**
194
     * Processes multiple aggregation pipelines within a single stage on the
195
     * same set of input documents.
196
     *
197
     * Each sub-pipeline has its own field in the output document where its
198
     * results are stored as an array of documents.
199
     *
200
     * @return Stage\Facet
201
     */
202 1
    public function facet()
203
    {
204 1
        return $this->addStage(new Stage\Facet($this));
205
    }
206
207
    /**
208
     * Outputs documents in order of nearest to farthest from a specified point.
209
     *
210
     * A GeoJSON point may be provided as the first and only argument for
211
     * 2dsphere queries. This single parameter may be a GeoJSON point object or
212
     * an array corresponding to the point's JSON representation. If GeoJSON is
213
     * used, the "spherical" option will default to true.
214
     *
215
     * You can only use this as the first stage of a pipeline.
216
     *
217
     * @see http://docs.mongodb.org/manual/reference/operator/aggregation/geoNear/
218
     *
219
     * @param float|array|Point $x
220
     * @param float $y
221
     * @return Stage\GeoNear
222
     */
223 4
    public function geoNear($x, $y = null)
224
    {
225 4
        return $this->addStage(new Stage\GeoNear($this, $x, $y));
226
    }
227
228
    /**
229
     * Returns the assembled aggregation pipeline
230
     *
231
     * For pipelines where the first stage is a $geoNear stage, it will apply
232
     * the document filters and discriminator queries to the query portion of
233
     * the geoNear operation. For all other pipelines, it prepends a $match stage
234
     * containing the required query.
235
     *
236
     * @return array
237
     */
238 55
    public function getPipeline()
239
    {
240 55
        $pipeline = array_map(
241
            function (Stage $stage) { return $stage->getExpression(); },
242 55
            $this->stages
243
        );
244
245 55
        if ($this->getStage(0) instanceof Stage\GeoNear) {
246 4
            $pipeline[0]['$geoNear']['query'] = $this->applyFilters($pipeline[0]['$geoNear']['query']);
247
        } else {
248 51
            $matchExpression = $this->applyFilters([]);
249 51
            if ($matchExpression !== []) {
250 1
                array_unshift($pipeline, ['$match' => $matchExpression]);
251
            }
252
        }
253
254 55
        return $pipeline;
255
    }
256
257
    /**
258
     * Returns a certain stage from the pipeline
259
     *
260
     * @param integer $index
261
     * @return Stage
262
     */
263 55
    public function getStage($index)
264
    {
265 55
        if ( ! isset($this->stages[$index])) {
266
            throw new \OutOfRangeException("Could not find stage with index {$index}.");
267
        }
268
269 55
        return $this->stages[$index];
270
    }
271
272
    /**
273
     * Performs a recursive search on a collection, with options for restricting
274
     * the search by recursion depth and query filter.
275
     *
276
     * @see https://docs.mongodb.org/manual/reference/operator/aggregation/graphLookup/
277
     *
278
     * @param string $from Target collection for the $graphLookup operation to
279
     * search, recursively matching the connectFromField to the connectToField.
280
     * @return Stage\GraphLookup
281
     */
282 10
    public function graphLookup($from)
283
    {
284 10
        return $this->addStage(new Stage\GraphLookup($this, $from, $this->dm, $this->class));
285
    }
286
287
    /**
288
     * Groups documents by some specified expression and outputs to the next
289
     * stage a document for each distinct grouping.
290
     *
291
     * @see http://docs.mongodb.org/manual/reference/operator/aggregation/group/
292
     *
293
     * @return Stage\Group
294
     */
295 4
    public function group()
296
    {
297 4
        return $this->addStage(new Stage\Group($this));
298
    }
299
300
    /**
301
     * Set which class to use when hydrating results as document class instances.
302
     *
303
     * @param string $className
304
     *
305
     * @return self
306
     */
307 4
    public function hydrate($className)
308
    {
309 4
        $this->hydrationClass = $className;
310
311 4
        return $this;
312
    }
313
314
    /**
315
     * Returns statistics regarding the use of each index for the collection.
316
     *
317
     * @see https://docs.mongodb.org/manual/reference/operator/aggregation/indexStats/
318
     *
319
     * @return Stage\IndexStats
320
     */
321 1
    public function indexStats()
322
    {
323 1
        return $this->addStage(new Stage\IndexStats($this));
324
    }
325
326
    /**
327
     * Limits the number of documents passed to the next stage in the pipeline.
328
     *
329
     * @see http://docs.mongodb.org/manual/reference/operator/aggregation/limit/
330
     *
331
     * @param integer $limit
332
     * @return Stage\Limit
333
     */
334 2
    public function limit($limit)
335
    {
336 2
        return $this->addStage(new Stage\Limit($this, $limit));
337
    }
338
339
    /**
340
     * Performs a left outer join to an unsharded collection in the same
341
     * database to filter in documents from the “joined” collection for
342
     * processing.
343
     *
344
     * @see https://docs.mongodb.org/manual/reference/operator/aggregation/lookup/
345
     *
346
     * @param string $from
347
     * @return Stage\Lookup
348
     */
349 14
    public function lookup($from)
350
    {
351 14
        return $this->addStage(new Stage\Lookup($this, $from, $this->dm, $this->class));
352
    }
353
354
    /**
355
     * Filters the documents to pass only the documents that match the specified
356
     * condition(s) to the next pipeline stage.
357
     *
358
     * @see http://docs.mongodb.org/manual/reference/operator/aggregation/match/
359
     *
360
     * @return Stage\Match
361
     */
362 8
    public function match()
363
    {
364 8
        return $this->addStage(new Stage\Match($this));
365
    }
366
367
    /**
368
     * Returns a query expression to be used in match stages
369
     *
370
     * @return QueryExpr
371
     */
372 60
    public function matchExpr()
373
    {
374 60
        $expr = new QueryExpr($this->dm);
375 60
        $expr->setClassMetadata($this->class);
376
377 60
        return $expr;
378
    }
379
380
    /**
381
     * Takes the documents returned by the aggregation pipeline and writes them
382
     * to a specified collection. This must be the last stage in the pipeline.
383
     *
384
     * @see http://docs.mongodb.org/manual/reference/operator/aggregation/out/
385
     *
386
     * @param string $collection
0 ignored issues
show
Bug introduced by
There is no parameter named $collection. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
387
     * @return Stage\Out
388
     */
389 6
    public function out($from)
390
    {
391 6
        return $this->addStage(new Stage\Out($this, $from, $this->dm));
392
    }
393
394
    /**
395
     * Passes along the documents with only the specified fields to the next
396
     * stage in the pipeline. The specified fields can be existing fields from
397
     * the input documents or newly computed fields.
398
     *
399
     * @see http://docs.mongodb.org/manual/reference/operator/aggregation/project/
400
     *
401
     * @return Stage\Project
402
     */
403 4
    public function project()
404
    {
405 4
        return $this->addStage(new Stage\Project($this));
406
    }
407
408
    /**
409
     * Restricts the contents of the documents based on information stored in
410
     * the documents themselves.
411
     *
412
     * @see http://docs.mongodb.org/manual/reference/operator/aggregation/redact/
413
     *
414
     * @return Stage\Redact
415
     */
416 2
    public function redact()
417
    {
418 2
        return $this->addStage(new Stage\Redact($this));
419
    }
420
421
    /**
422
     * Promotes a specified document to the top level and replaces all other
423
     * fields.
424
     *
425
     * The operation replaces all existing fields in the input document,
426
     * including the _id field. You can promote an existing embedded document to
427
     * the top level, or create a new document for promotion.
428
     *
429
     * @param string|null $expression Optional. A replacement expression that
430
     * resolves to a document.
431
     *
432
     * @return Stage\ReplaceRoot
433
     */
434 6
    public function replaceRoot($expression = null)
435
    {
436 6
        return $this->addStage(new Stage\ReplaceRoot($this, $this->dm, $this->class, $expression));
437
    }
438
439
    /**
440
     * Randomly selects the specified number of documents from its input.
441
     *
442
     * @see https://docs.mongodb.org/manual/reference/operator/aggregation/sample/
443
     *
444
     * @param integer $size
445
     * @return Stage\Sample
446
     */
447 2
    public function sample($size)
448
    {
449 2
        return $this->addStage(new Stage\Sample($this, $size));
450
    }
451
452
    /**
453
     * Skips over the specified number of documents that pass into the stage and
454
     * passes the remaining documents to the next stage in the pipeline.
455
     *
456
     * @see http://docs.mongodb.org/manual/reference/operator/aggregation/skip/
457
     *
458
     * @param integer $skip
459
     * @return Stage\Skip
460
     */
461 2
    public function skip($skip)
462
    {
463 2
        return $this->addStage(new Stage\Skip($this, $skip));
464
    }
465
466
    /**
467
     * Sorts all input documents and returns them to the pipeline in sorted
468
     * order.
469
     *
470
     * If sorting by multiple fields, the first argument should be an array of
471
     * field name (key) and order (value) pairs.
472
     *
473
     * @see http://docs.mongodb.org/manual/reference/operator/aggregation/sort/
474
     *
475
     * @param array|string $fieldName Field name or array of field/order pairs
476
     * @param integer|string $order   Field order (if one field is specified)
477
     * @return Stage\Sort
478
     */
479 7
    public function sort($fieldName, $order = null)
480
    {
481 7
        $fields = is_array($fieldName) ? $fieldName : [$fieldName => $order];
482
        // fixme: move to sort stage
483 7
        return $this->addStage(new Stage\Sort($this, $this->getDocumentPersister()->prepareSort($fields)));
484
    }
485
486
    /**
487
     * Groups incoming documents based on the value of a specified expression,
488
     * then computes the count of documents in each distinct group.
489
     *
490
     * @see http://docs.mongodb.org/manual/reference/operator/aggregation/sortByCount/
491
     *
492
     * @param string $expression The expression to group by
493
     * @return Stage\SortByCount
494
     */
495 3
    public function sortByCount($expression)
496
    {
497 3
        return $this->addStage(new Stage\SortByCount($this, $expression, $this->dm, $this->class));
498
    }
499
500
    /**
501
     * Deconstructs an array field from the input documents to output a document
502
     * for each element. Each output document is the input document with the
503
     * value of the array field replaced by the element.
504
     *
505
     * @see http://docs.mongodb.org/manual/reference/operator/aggregation/unwind/
506
     *
507
     * @param string $fieldName The field to unwind. It is automatically prefixed with the $ sign
508
     * @return Stage\Unwind
509
     */
510 7
    public function unwind($fieldName)
511
    {
512
        // Fixme: move field name translation to stage
513 7
        return $this->addStage(new Stage\Unwind($this, $this->getDocumentPersister()->prepareFieldName($fieldName)));
514
    }
515
516
    /**
517
     * @param Stage $stage
518
     * @return Stage
519
     */
520 61
    protected function addStage(Stage $stage)
521
    {
522 61
        $this->stages[] = $stage;
523
524 61
        return $stage;
525
    }
526
527
    /**
528
     * Applies filters and discriminator queries to the pipeline
529
     *
530
     * @param array $query
531
     * @return array
532
     */
533 55
    private function applyFilters(array $query)
534
    {
535 55
        $documentPersister = $this->dm->getUnitOfWork()->getDocumentPersister($this->class->name);
536
537 55
        $query = $documentPersister->addDiscriminatorToPreparedQuery($query);
538 55
        $query = $documentPersister->addFilterToPreparedQuery($query);
539
540 55
        return $query;
541
    }
542
543
    /**
544
     * @return \Doctrine\ODM\MongoDB\Persisters\DocumentPersister
545
     */
546 11
    private function getDocumentPersister()
547
    {
548 11
        return $this->dm->getUnitOfWork()->getDocumentPersister($this->class->name);
549
    }
550
551
    /**
552
     * @param Cursor $cursor
553
     *
554
     * @return Iterator
555
     */
556 17
    private function prepareIterator(Cursor $cursor): Iterator
557
    {
558 17
        $class = null;
559 17
        if ($this->hydrationClass) {
560 4
            $class = $this->dm->getClassMetadata($this->hydrationClass);
561
        }
562
563 17
        if ($class) {
564 4
            $cursor = new HydratingIterator($cursor, $this->dm->getUnitOfWork(), $class);
565
        }
566
567 17
        return new CachingIterator($cursor);
568
    }
569
}
570