2222use MongoDB \Laravel \Relations \EmbedsOneOrMany ;
2323use MongoDB \Laravel \Relations \HasMany ;
2424use MongoDB \Model \BSONDocument ;
25+ use RuntimeException ;
26+ use TypeError ;
2527
2628use function array_key_exists ;
2729use function array_merge ;
30+ use function assert ;
2831use function collect ;
2932use function count ;
3033use function explode ;
34+ use function get_debug_type ;
3135use function is_array ;
3236use function is_object ;
37+ use function is_string ;
3338use function iterator_to_array ;
3439use function property_exists ;
3540use function sprintf ;
@@ -43,7 +48,11 @@ class Builder extends EloquentBuilder
4348 private const DUPLICATE_KEY_ERROR = 11000 ;
4449 use QueriesRelationships;
4550
46- /** @var array{relation: Relation, function: string, constraints: array, column: string, alias: string}[] */
51+ /**
52+ * List of aggregations on the related models after the main query.
53+ *
54+ * @var array{relation: Relation, function: string, constraints: array, column: string, alias: string}[]
55+ */
4756 private array $ withAggregate = [];
4857
4958 /**
@@ -306,19 +315,37 @@ public function createOrFirst(array $attributes = [], array $values = [])
306315 }
307316 }
308317
318+ /**
319+ * Add subsequent queries to include an aggregate value for a relationship.
320+ * For embedded relations, a projection is used to calculate the aggregate.
321+ *
322+ * @see \Illuminate\Database\Eloquent\Concerns\QueriesRelationships::withAggregate()
323+ *
324+ * @param mixed $relations Name of the relationship or an array of relationships to closure for constraint
325+ * @param string $column Name of the field to aggregate
326+ * @param string $function Required aggregation function name (count, min, max, avg)
327+ *
328+ * @return $this
329+ */
309330 public function withAggregate ($ relations , $ column , $ function = null )
310331 {
311332 if (empty ($ relations )) {
312333 return $ this ;
313334 }
314335
336+ assert (is_string ($ function ), new TypeError ('Argument 3 ($function) passed to withAggregate must be of the type string, ' . get_debug_type ($ function ) . ' given ' ));
337+
315338 $ relations = is_array ($ relations ) ? $ relations : [$ relations ];
316339
317340 foreach ($ this ->parseWithRelations ($ relations ) as $ name => $ constraints ) {
318341 $ segments = explode (' ' , $ name );
319342
343+ $ alias = match (true ) {
344+ count ($ segments ) === 1 => Str::snake ($ segments [0 ]) . '_ ' . $ function ,
345+ count ($ segments ) === 3 && Str::lower ($ segments [1 ]) => $ segments [2 ],
346+ default => throw new InvalidArgumentException (sprintf ('Invalid relation name format. Expected "relation as alias" or "relation", got "%s" ' , $ name )),
347+ };
320348 $ name = $ segments [0 ];
321- $ alias = (count ($ segments ) === 3 && Str::lower ($ segments [1 ]) === 'as ' ? $ segments [2 ] : Str::snake ($ name ) . '_ ' . $ function );
322349
323350 $ relation = $ this ->getRelationWithoutConstraints ($ name );
324351
@@ -347,6 +374,7 @@ public function withAggregate($relations, $column, $function = null)
347374 throw new InvalidArgumentException (sprintf ('Invalid aggregate function "%s" ' , $ function ));
348375 }
349376 } else {
377+ // The aggregation will be performed after the main query, during eager loading.
350378 $ this ->withAggregate [$ alias ] = [
351379 'relation ' => $ relation ,
352380 'function ' => $ function ,
@@ -384,6 +412,8 @@ public function eagerLoadRelations(array $models)
384412
385413 $ model ->setAttribute ($ withAggregate ['alias ' ], $ value );
386414 }
415+ } else {
416+ throw new RuntimeException (sprintf ('Unsupported relation type for aggregation ' , $ withAggregate ['relation ' ]::class));
387417 }
388418 }
389419 }
0 commit comments