Completed
Push — master ( 26ecbc...8c0c5d )
by Maciej
14s
created

lib/Doctrine/ODM/MongoDB/Aggregation/Builder.php (5 issues)

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\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 = 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
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 55
    public function getPipeline() : array
226
    {
227 55
        $pipeline = array_map(
228
            static function (Stage $stage) {
229 55
                return $stage->getExpression();
230 55
            },
231 55
            $this->stages
232
        );
233
234 55
        if ($this->getStage(0) instanceof Stage\GeoNear) {
235 4
            $pipeline[0]['$geoNear']['query'] = $this->applyFilters($pipeline[0]['$geoNear']['query']);
236
        } else {
237 51
            $matchExpression = $this->applyFilters([]);
238 51
            if ($matchExpression !== []) {
239 1
                array_unshift($pipeline, ['$match' => $matchExpression]);
240
            }
241
        }
242
243 55
        return $pipeline;
244
    }
245
246
    /**
247
     * Returns a certain stage from the pipeline
248
     */
249 55
    public function getStage(int $index) : Stage
250
    {
251 55
        if (! isset($this->stages[$index])) {
252
            throw new OutOfRangeException(sprintf('Could not find stage with index %d.', $index));
253
        }
254
255 55
        return $this->stages[$index];
256
    }
257
258
    /**
259
     * Performs a recursive search on a collection, with options for restricting
260
     * the search by recursion depth and query filter.
261
     *
262
     * @see https://docs.mongodb.org/manual/reference/operator/aggregation/graphLookup/
263
     *
264
     * @param string $from Target collection for the $graphLookup operation to
265
     * search, recursively matching the connectFromField to the connectToField.
266
     */
267 10 View Code Duplication
    public function graphLookup(string $from) : Stage\GraphLookup
0 ignored issues
show
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...
268
    {
269 10
        $stage = new Stage\GraphLookup($this, $from, $this->dm, $this->class);
270 9
        $this->addStage($stage);
271
272 9
        return $stage;
273
    }
274
275
    /**
276
     * Groups documents by some specified expression and outputs to the next
277
     * stage a document for each distinct grouping.
278
     *
279
     * @see http://docs.mongodb.org/manual/reference/operator/aggregation/group/
280
     */
281 4
    public function group() : Stage\Group
282
    {
283 4
        $stage = new Stage\Group($this);
284 4
        $this->addStage($stage);
285
286 4
        return $stage;
287
    }
288
289
    /**
290
     * Set which class to use when hydrating results as document class instances.
291
     */
292 4
    public function hydrate(string $className) : self
293
    {
294 4
        $this->hydrationClass = $className;
295
296 4
        return $this;
297
    }
298
299
    /**
300
     * Returns statistics regarding the use of each index for the collection.
301
     *
302
     * @see https://docs.mongodb.org/manual/reference/operator/aggregation/indexStats/
303
     */
304 1
    public function indexStats() : Stage\IndexStats
305
    {
306 1
        $stage = new Stage\IndexStats($this);
307 1
        $this->addStage($stage);
308
309 1
        return $stage;
310
    }
311
312
    /**
313
     * Limits the number of documents passed to the next stage in the pipeline.
314
     *
315
     * @see http://docs.mongodb.org/manual/reference/operator/aggregation/limit/
316
     */
317 2
    public function limit(int $limit) : Stage\Limit
318
    {
319 2
        $stage = new Stage\Limit($this, $limit);
320 2
        $this->addStage($stage);
321
322 2
        return $stage;
323
    }
324
325
    /**
326
     * Performs a left outer join to an unsharded collection in the same
327
     * database to filter in documents from the “joined” collection for
328
     * processing.
329
     *
330
     * @see https://docs.mongodb.org/manual/reference/operator/aggregation/lookup/
331
     */
332 14 View Code Duplication
    public function lookup(string $from) : Stage\Lookup
0 ignored issues
show
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...
333
    {
334 14
        $stage = new Stage\Lookup($this, $from, $this->dm, $this->class);
335 12
        $this->addStage($stage);
336
337 12
        return $stage;
338
    }
339
340
    /**
341
     * Filters the documents to pass only the documents that match the specified
342
     * condition(s) to the next pipeline stage.
343
     *
344
     * @see http://docs.mongodb.org/manual/reference/operator/aggregation/match/
345
     */
346 8
    public function match() : Stage\Match
347
    {
348 8
        $stage = new Stage\Match($this);
349 8
        $this->addStage($stage);
350
351 8
        return $stage;
352
    }
353
354
    /**
355
     * Returns a query expression to be used in match stages
356
     */
357 60
    public function matchExpr() : QueryExpr
358
    {
359 60
        $expr = new QueryExpr($this->dm);
360 60
        $expr->setClassMetadata($this->class);
361
362 60
        return $expr;
363
    }
364
365
    /**
366
     * Takes the documents returned by the aggregation pipeline and writes them
367
     * to a specified collection. This must be the last stage in the pipeline.
368
     *
369
     * @see http://docs.mongodb.org/manual/reference/operator/aggregation/out/
370
     */
371 6
    public function out(string $from) : Stage\Out
372
    {
373 6
        $stage = new Stage\Out($this, $from, $this->dm);
374 5
        $this->addStage($stage);
375
376 5
        return $stage;
377
    }
378
379
    /**
380
     * Passes along the documents with only the specified fields to the next
381
     * stage in the pipeline. The specified fields can be existing fields from
382
     * the input documents or newly computed fields.
383
     *
384
     * @see http://docs.mongodb.org/manual/reference/operator/aggregation/project/
385
     */
386 4
    public function project() : Stage\Project
387
    {
388 4
        $stage = new Stage\Project($this);
389 4
        $this->addStage($stage);
390
391 4
        return $stage;
392
    }
393
394
    /**
395
     * Restricts the contents of the documents based on information stored in
396
     * the documents themselves.
397
     *
398
     * @see http://docs.mongodb.org/manual/reference/operator/aggregation/redact/
399
     */
400 2
    public function redact() : Stage\Redact
401
    {
402 2
        $stage = new Stage\Redact($this);
403 2
        $this->addStage($stage);
404
405 2
        return $stage;
406
    }
407
408
    /**
409
     * Promotes a specified document to the top level and replaces all other
410
     * fields.
411
     *
412
     * The operation replaces all existing fields in the input document,
413
     * including the _id field. You can promote an existing embedded document to
414
     * the top level, or create a new document for promotion.
415
     *
416
     * @param string|array|null $expression Optional. A replacement expression that
417
     * resolves to a document.
418
     */
419 6 View Code Duplication
    public function replaceRoot($expression = null) : Stage\ReplaceRoot
0 ignored issues
show
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...
420
    {
421 6
        $stage = new Stage\ReplaceRoot($this, $this->dm, $this->class, $expression);
422 6
        $this->addStage($stage);
423
424 6
        return $stage;
425
    }
426
427
    /**
428
     * Randomly selects the specified number of documents from its input.
429
     *
430
     * @see https://docs.mongodb.org/manual/reference/operator/aggregation/sample/
431
     */
432 2
    public function sample(int $size) : Stage\Sample
433
    {
434 2
        $stage = new Stage\Sample($this, $size);
435 2
        $this->addStage($stage);
436
437 2
        return $stage;
438
    }
439
440
    /**
441
     * Skips over the specified number of documents that pass into the stage and
442
     * passes the remaining documents to the next stage in the pipeline.
443
     *
444
     * @see http://docs.mongodb.org/manual/reference/operator/aggregation/skip/
445
     */
446 2
    public function skip(int $skip) : Stage\Skip
447
    {
448 2
        $stage = new Stage\Skip($this, $skip);
449 2
        $this->addStage($stage);
450
451 2
        return $stage;
452
    }
453
454
    /**
455
     * Sorts all input documents and returns them to the pipeline in sorted
456
     * order.
457
     *
458
     * If sorting by multiple fields, the first argument should be an array of
459
     * field name (key) and order (value) pairs.
460
     *
461
     * @see http://docs.mongodb.org/manual/reference/operator/aggregation/sort/
462
     *
463
     * @param array|string $fieldName Field name or array of field/order pairs
464
     * @param int|string   $order     Field order (if one field is specified)
465
     */
466 7
    public function sort($fieldName, $order = null) : Stage\Sort
467
    {
468 7
        $fields = is_array($fieldName) ? $fieldName : [$fieldName => $order];
469
        // fixme: move to sort stage
470 7
        $stage = new Stage\Sort($this, $this->getDocumentPersister()->prepareSort($fields));
471 7
        $this->addStage($stage);
472
473 7
        return $stage;
474
    }
475
476
    /**
477
     * Groups incoming documents based on the value of a specified expression,
478
     * then computes the count of documents in each distinct group.
479
     *
480
     * @see http://docs.mongodb.org/manual/reference/operator/aggregation/sortByCount/
481
     */
482 3 View Code Duplication
    public function sortByCount(string $expression) : Stage\SortByCount
0 ignored issues
show
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...
483
    {
484 3
        $stage = new Stage\SortByCount($this, $expression, $this->dm, $this->class);
485 3
        $this->addStage($stage);
486
487 3
        return $stage;
488
    }
489
490
    /**
491
     * Deconstructs an array field from the input documents to output a document
492
     * for each element. Each output document is the input document with the
493
     * value of the array field replaced by the element.
494
     *
495
     * @see http://docs.mongodb.org/manual/reference/operator/aggregation/unwind/
496
     */
497 7
    public function unwind(string $fieldName) : Stage\Unwind
498
    {
499
        // Fixme: move field name translation to stage
500 7
        $stage = new Stage\Unwind($this, $this->getDocumentPersister()->prepareFieldName($fieldName));
501 7
        $this->addStage($stage);
502
503 7
        return $stage;
504
    }
505
506 61
    protected function addStage(Stage $stage) : Stage
507
    {
508 61
        $this->stages[] = $stage;
509
510 61
        return $stage;
511
    }
512
513
    /**
514
     * Applies filters and discriminator queries to the pipeline
515
     */
516 55
    private function applyFilters(array $query) : array
517
    {
518 55
        $documentPersister = $this->dm->getUnitOfWork()->getDocumentPersister($this->class->name);
519
520 55
        $query = $documentPersister->addDiscriminatorToPreparedQuery($query);
521 55
        $query = $documentPersister->addFilterToPreparedQuery($query);
522
523 55
        return $query;
524
    }
525
526 11
    private function getDocumentPersister() : DocumentPersister
527
    {
528 11
        return $this->dm->getUnitOfWork()->getDocumentPersister($this->class->name);
529
    }
530
531 17
    private function prepareIterator(Cursor $cursor) : Iterator
532
    {
533 17
        $class = null;
534 17
        if ($this->hydrationClass) {
535 4
            $class = $this->dm->getClassMetadata($this->hydrationClass);
536
        }
537
538 17
        if ($class) {
539 4
            $cursor = new HydratingIterator($cursor, $this->dm->getUnitOfWork(), $class);
540
        }
541
542 17
        return new CachingIterator($cursor);
543
    }
544
}
545