2222use MongoDB \BSON \Regex ;
2323use MongoDB \BSON \UTCDateTime ;
2424use MongoDB \Builder \Accumulator ;
25- use MongoDB \Builder \BuilderEncoder ;
2625use MongoDB \Builder \Expression \FieldPath ;
2726use MongoDB \Builder \Variable ;
2827use MongoDB \Driver \Cursor ;
@@ -87,7 +86,7 @@ class Builder extends BaseBuilder
8786 *
8887 * @var array
8988 */
90- public $ projections ;
89+ public $ projections = [] ;
9190
9291 /**
9392 * The maximum amount of seconds to allow the query to run.
@@ -283,23 +282,73 @@ public function dump(mixed ...$args)
283282 return $ this ;
284283 }
285284
286- protected function getPipelineBuilder (): PipelineBuilder
285+ private function getPipelineBuilder (): PipelineBuilder
287286 {
288- $ columns = $ this ->columns ?? [];
289-
290287 $ pipelineBuilder = new PipelineBuilder ([], $ this ->collection , $ this ->options );
291288
289+ $ wheres = $ this ->compileWheres ();
290+
291+ if (count ($ wheres )) {
292+ $ pipelineBuilder ->match (...$ wheres );
293+ }
294+
295+ // Distinct query
296+ if ($ this ->distinct ) {
297+ // Return distinct results directly
298+ $ column = $ columns [0 ] ?? '_id ' ;
299+
300+ $ pipelineBuilder ->group (
301+ _id: \MongoDB \Builder \Expression::fieldPath ($ column ),
302+ _document: Accumulator::first (Variable::root ()),
303+ );
304+ $ pipelineBuilder ->replaceRoot (
305+ newRoot: new FieldPath ('_document ' ),
306+ );
307+ }
308+
309+ if ($ this ->orders ) {
310+ $ pipelineBuilder ->sort (...$ this ->orders );
311+ }
312+
313+ if ($ this ->offset ) {
314+ $ pipelineBuilder ->skip ($ this ->offset );
315+ }
316+
317+ if ($ this ->limit ) {
318+ $ pipelineBuilder ->limit ($ this ->limit );
319+ }
320+
321+ // Normal query
322+ // Add custom projections.
323+ if ($ this ->projections || $ this ->columns ) {
324+ $ columns = in_array ('* ' , $ this ->columns ) ? [] : $ this ->columns ;
325+ $ projection = array_fill_keys ($ columns , true ) + $ this ->projections ;
326+ if ($ projection ) {
327+ $ pipelineBuilder ->project (...$ projection );
328+ }
329+ }
330+
331+ return $ pipelineBuilder ;
332+ }
333+
334+ /**
335+ * Return the MongoDB query to be run in the form of an element array like ['method' => [arguments]].
336+ *
337+ * Example: ['find' => [['name' => 'John Doe'], ['projection' => ['birthday' => 1]]]]
338+ *
339+ * @return array<string, mixed[]>
340+ */
341+ public function toMql (): array
342+ {
343+ $ columns = $ this ->columns ?? [];
344+
292345 // Drop all columns if * is present, MongoDB does not work this way.
293346 if (in_array ('* ' , $ columns )) {
294347 $ columns = [];
295348 }
296349
297350 $ wheres = $ this ->compileWheres ();
298351
299- if (count ($ wheres )) {
300- $ pipelineBuilder ->match (...$ wheres );
301- }
302-
303352 // Use MongoDB's aggregation framework when using grouping or aggregation functions.
304353 if ($ this ->groups || $ this ->aggregate ) {
305354 $ group = [];
@@ -359,59 +408,60 @@ protected function getPipelineBuilder(): PipelineBuilder
359408 $ group ['_id ' ] = null ;
360409 }
361410
411+ // Build the aggregation pipeline.
412+ $ pipeline = [];
413+ if ($ wheres ) {
414+ $ pipeline [] = ['$match ' => $ wheres ];
415+ }
416+
362417 // apply unwinds for subdocument array aggregation
363418 foreach ($ unwinds as $ unwind ) {
364- $ pipelineBuilder -> unwind ( $ unwind) ;
419+ $ pipeline [] = [ ' $ unwind' => ' $ ' . $ unwind] ;
365420 }
366421
367422 if ($ group ) {
368- $ pipelineBuilder -> group (... $ group) ;
423+ $ pipeline [] = [ ' $ group' => $ group] ;
369424 }
370425
371426 // Apply order and limit
372427 if ($ this ->orders ) {
373- $ pipelineBuilder -> sort ( $ this ->orders ) ;
428+ $ pipeline [] = [ ' $ sort' => $ this ->orders ] ;
374429 }
375430
376431 if ($ this ->offset ) {
377- $ pipelineBuilder -> skip ( $ this ->offset ) ;
432+ $ pipeline [] = [ ' $ skip' => $ this ->offset ] ;
378433 }
379434
380435 if ($ this ->limit ) {
381- $ pipelineBuilder -> limit ( $ this ->limit ) ;
436+ $ pipeline [] = [ ' $ limit' => $ this ->limit ] ;
382437 }
383438
384439 if ($ this ->projections ) {
385- $ pipelineBuilder ->project (...$ this ->projections );
440+ $ pipeline [] = ['$project ' => $ this ->projections ];
441+ }
442+
443+ $ options = [
444+ 'typeMap ' => ['root ' => 'array ' , 'document ' => 'array ' ],
445+ ];
446+
447+ // Add custom query options
448+ if (count ($ this ->options )) {
449+ $ options = array_merge ($ options , $ this ->options );
386450 }
387451
388- return $ pipelineBuilder ;
452+ $ options = $ this ->inheritConnectionOptions ($ options );
453+
454+ return ['aggregate ' => [$ pipeline , $ options ]];
389455 }
390456
391457 // Distinct query
392458 if ($ this ->distinct ) {
393459 // Return distinct results directly
394460 $ column = $ columns [0 ] ?? '_id ' ;
395461
396- $ pipelineBuilder ->group (
397- _id: \MongoDB \Builder \Expression::fieldPath ($ column ),
398- _document: Accumulator::first (Variable::root ()),
399- );
400- $ pipelineBuilder ->replaceRoot (
401- newRoot: new FieldPath ('_document ' ),
402- );
403- }
404-
405- if ($ this ->orders ) {
406- $ pipelineBuilder ->sort (...$ this ->orders );
407- }
408-
409- if ($ this ->offset ) {
410- $ pipelineBuilder ->skip ($ this ->offset );
411- }
462+ $ options = $ this ->inheritConnectionOptions ();
412463
413- if ($ this ->limit ) {
414- $ pipelineBuilder ->limit ($ this ->limit );
464+ return ['distinct ' => [$ column , $ wheres , $ options ]];
415465 }
416466
417467 // Normal query
@@ -423,39 +473,44 @@ protected function getPipelineBuilder(): PipelineBuilder
423473 $ projection = array_merge ($ projection , $ this ->projections );
424474 }
425475
426- if ($ projection ) {
427- $ pipelineBuilder ->project (...$ projection );
428- }
476+ $ options = [];
429477
430- return $ pipelineBuilder ;
431- }
478+ // Apply order, offset, limit and projection
479+ if ($ this ->timeout ) {
480+ $ options ['maxTimeMS ' ] = $ this ->timeout * 1000 ;
481+ }
432482
433- /**
434- * Return the MongoDB query to be run in the form of an element array like ['method' => [arguments]].
435- *
436- * Example: ['find' => [['name' => 'John Doe'], ['projection' => ['birthday' => 1]]]]
437- *
438- * @return array<string, mixed[]>
439- */
440- public function toMql (): array
441- {
442- $ encoder = new BuilderEncoder ();
443- $ pipeline = $ encoder ->encode ($ this ->getPipelineBuilder ()->getPipeline ());
483+ if ($ this ->orders ) {
484+ $ options ['sort ' ] = $ this ->orders ;
485+ }
444486
445- $ options = ['typeMap ' => ['root ' => 'array ' , 'document ' => 'array ' ]];
487+ if ($ this ->offset ) {
488+ $ options ['skip ' ] = $ this ->offset ;
489+ }
446490
447- if ($ this ->timeout ) {
448- $ options ['maxTimeMS ' ] = $ this ->timeout * 1000 ;
491+ if ($ this ->limit ) {
492+ $ options ['limit ' ] = $ this ->limit ;
449493 }
450494
451495 if ($ this ->hint ) {
452496 $ options ['hint ' ] = $ this ->hint ;
453497 }
454498
455- $ options = array_merge ($ options , $ this ->options );
499+ if ($ projection ) {
500+ $ options ['projection ' ] = $ projection ;
501+ }
502+
503+ // Fix for legacy support, converts the results to arrays instead of objects.
504+ $ options ['typeMap ' ] = ['root ' => 'array ' , 'document ' => 'array ' ];
505+
506+ // Add custom query options
507+ if (count ($ this ->options )) {
508+ $ options = array_merge ($ options , $ this ->options );
509+ }
510+
456511 $ options = $ this ->inheritConnectionOptions ($ options );
457512
458- return ['aggregate ' => [$ pipeline , $ options ]];
513+ return ['find ' => [$ wheres , $ options ]];
459514 }
460515
461516 /**
@@ -538,37 +593,32 @@ public function generateCacheKey()
538593 /** @return ($function === null ? PipelineBuilder : self) */
539594 public function aggregate ($ function = null , $ columns = [])
540595 {
541- $ builder = $ this ->getPipelineBuilder ();
542-
543596 if ($ function === null ) {
544- return $ builder ;
545- }
546-
547- match ($ function ) {
548- 'count ' => $ builder ->group (
549- _id: null ,
550- aggregate: Accumulator::sum (1 ),
551- ),
552- 'sum ' => $ builder ->group (
553- _id: null ,
554- aggregate: Accumulator::sum (\MongoDB \Builder \Expression::fieldPath ($ columns [0 ])),
555- ),
556- 'avg ' => $ builder ->group (
557- _id: null ,
558- aggregate: Accumulator::avg (\MongoDB \Builder \Expression::fieldPath ($ columns [0 ])),
559- ),
560- 'min ' => $ builder ->group (
561- _id: null ,
562- aggregate: Accumulator::min (\MongoDB \Builder \Expression::fieldPath ($ columns [0 ])),
563- ),
564- 'max ' => $ builder ->group (
565- _id: null ,
566- aggregate: Accumulator::max (\MongoDB \Builder \Expression::fieldPath ($ columns [0 ])),
567- ),
568- default => throw new InvalidArgumentException ('Unknown aggregate function: ' . $ function ),
569- };
597+ return $ this ->getPipelineBuilder ();
598+ }
599+
600+ $ this ->aggregate = [
601+ 'function ' => $ function ,
602+ 'columns ' => $ columns ,
603+ ];
604+
605+ $ previousColumns = $ this ->columns ;
570606
571- $ results = $ builder ->get ();
607+ // We will also back up the select bindings since the select clause will be
608+ // removed when performing the aggregate function. Once the query is run
609+ // we will add the bindings back onto this query so they can get used.
610+ $ previousSelectBindings = $ this ->bindings ['select ' ];
611+
612+ $ this ->bindings ['select ' ] = [];
613+
614+ $ results = $ this ->get ($ columns );
615+
616+ // Once we have executed the query, we will reset the aggregate property so
617+ // that more select queries can be executed against the database without
618+ // the aggregate value getting in the way when the grammar builds it.
619+ $ this ->aggregate = null ;
620+ $ this ->columns = $ previousColumns ;
621+ $ this ->bindings ['select ' ] = $ previousSelectBindings ;
572622
573623 if (isset ($ results [0 ])) {
574624 $ result = (array ) $ results [0 ];
@@ -577,19 +627,6 @@ public function aggregate($function = null, $columns = [])
577627 }
578628 }
579629
580- public function count ($ columns = '* ' ): int
581- {
582- if ($ columns !== '* ' ) {
583- // @todo trigger warning, $columns is ignored
584- }
585-
586- return $ this
587- ->aggregate ()
588- ->count ('aggregate ' )
589- ->get ()
590- ->value ('aggregate ' , 0 );
591- }
592-
593630 /** @inheritdoc */
594631 public function exists ()
595632 {
@@ -963,14 +1000,14 @@ public function runPaginationCountQuery($columns = ['*'])
9631000 if ($ this ->groups || $ this ->havings ) {
9641001 $ without = $ this ->unions ? ['orders ' , 'limit ' , 'offset ' ] : ['columns ' , 'orders ' , 'limit ' , 'offset ' ];
9651002
966- $ pipelienBuilder = $ this ->cloneWithout ($ without )
1003+ $ mql = $ this ->cloneWithout ($ without )
9671004 ->cloneWithoutBindings ($ this ->unions ? ['order ' ] : ['select ' , 'order ' ])
968- ->getPipelineBuilder ();
1005+ ->toMql ();
9691006
9701007 // Adds the $count stage to the pipeline
971- $ pipelienBuilder -> count ( ' aggregate ') ;
1008+ $ mql [ ' aggregate ' ][ 0 ][] = [ ' $ count' => ' aggregate '] ;
9721009
973- return $ pipelienBuilder -> get ();
1010+ return $ this -> collection -> aggregate ( $ mql [ ' aggregate ' ][ 0 ], $ mql [ ' aggregate ' ][ 1 ])-> toArray ();
9741011 }
9751012
9761013 return parent ::runPaginationCountQuery ($ columns );
@@ -1173,11 +1210,6 @@ protected function compileWheres(): array
11731210 return $ compiled ;
11741211 }
11751212
1176- /**
1177- * @param array $where
1178- *
1179- * @return array
1180- */
11811213 protected function compileWhereBasic (array $ where ): array
11821214 {
11831215 $ column = $ where ['column ' ];
0 commit comments