22
22
use MongoDB \Laravel \Relations \EmbedsOneOrMany ;
23
23
use MongoDB \Laravel \Relations \HasMany ;
24
24
use MongoDB \Model \BSONDocument ;
25
+ use RuntimeException ;
26
+ use TypeError ;
25
27
26
28
use function array_key_exists ;
27
29
use function array_merge ;
30
+ use function assert ;
28
31
use function collect ;
29
32
use function count ;
30
33
use function explode ;
34
+ use function get_debug_type ;
31
35
use function is_array ;
32
36
use function is_object ;
37
+ use function is_string ;
33
38
use function iterator_to_array ;
34
39
use function property_exists ;
35
40
use function sprintf ;
@@ -43,7 +48,11 @@ class Builder extends EloquentBuilder
43
48
private const DUPLICATE_KEY_ERROR = 11000 ;
44
49
use QueriesRelationships;
45
50
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
+ */
47
56
private array $ withAggregate = [];
48
57
49
58
/**
@@ -306,19 +315,37 @@ public function createOrFirst(array $attributes = [], array $values = [])
306
315
}
307
316
}
308
317
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
+ */
309
330
public function withAggregate ($ relations , $ column , $ function = null )
310
331
{
311
332
if (empty ($ relations )) {
312
333
return $ this ;
313
334
}
314
335
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
+
315
338
$ relations = is_array ($ relations ) ? $ relations : [$ relations ];
316
339
317
340
foreach ($ this ->parseWithRelations ($ relations ) as $ name => $ constraints ) {
318
341
$ segments = explode (' ' , $ name );
319
342
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
+ };
320
348
$ name = $ segments [0 ];
321
- $ alias = (count ($ segments ) === 3 && Str::lower ($ segments [1 ]) === 'as ' ? $ segments [2 ] : Str::snake ($ name ) . '_ ' . $ function );
322
349
323
350
$ relation = $ this ->getRelationWithoutConstraints ($ name );
324
351
@@ -347,6 +374,7 @@ public function withAggregate($relations, $column, $function = null)
347
374
throw new InvalidArgumentException (sprintf ('Invalid aggregate function "%s" ' , $ function ));
348
375
}
349
376
} else {
377
+ // The aggregation will be performed after the main query, during eager loading.
350
378
$ this ->withAggregate [$ alias ] = [
351
379
'relation ' => $ relation ,
352
380
'function ' => $ function ,
@@ -384,6 +412,8 @@ public function eagerLoadRelations(array $models)
384
412
385
413
$ model ->setAttribute ($ withAggregate ['alias ' ], $ value );
386
414
}
415
+ } else {
416
+ throw new RuntimeException (sprintf ('Unsupported relation type for aggregation ' , $ withAggregate ['relation ' ]::class));
387
417
}
388
418
}
389
419
}
0 commit comments