Completed
Push — master ( 4ab32a...22189c )
by Maciej
17s queued 10s
created

lib/Doctrine/ODM/MongoDB/Aggregation/Builder.php (1 issue)

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