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