Yahya Uddin
Yahya Uddin

Reputation: 28861

Laravel: Summing an attribute in a nested relation

I want to sum a nested collection.

I have the following tables:

A venue can have many offers and a offer can have many orders.

Example data may be:

Venues

id        |  name
==========================
5         |  Pizza 4 Less
10        |  Poundland

Offers

id | venue_id | name
==================================
17 | 5        | Buy 1 get one free 
24 | 5        | 30% off pizza
32 | 10       | 50% off
50 | 10       | 20% off

Orders

id | offer_id | bill | paid
===========================
85 | 17       | 15   | true
86 | 17       | 20   | true
87 | 17       | 90   | true
88 | 24       | 14   | true
89 | 32       | 15   | true
90 | 32       | 65   | true
91 | 50       | 24   | true
92 | 50       | 1000 | false

I want to use Laravel Elqouent model to get the total amount paid for each venue. So for the above data I want to get the following result:

id | name          | total_paid
===============================
5  | Pizza 4 Less  | 139
10 | Poundland     | 104

Note that the totals did not include orders that was not paid (i.e. order 92)

The way I currently do this is as follows:

$venues = Venue::with(['offers.orders' => function ($query) {
        $query->where('paid', '=', true);
    }])
    ->get();

$totals = [];
foreach ($venues as $venue) {
    $totalPaid = 0;
    foreach ($venue->offers as $offer) {
        $totalPaid += $offer->orders->sum('bill');
    }
    $totals[$venue->name] = $totalPaid;
}

As you can see, the above code is inefficient and long.

Is there a better way to do this?

Upvotes: 2

Views: 1123

Answers (1)

Mathieu Ferre
Mathieu Ferre

Reputation: 4412

Clean Version, but not efficient

// In your Venue Model
public function getTotalPaidAttribute()
{
    return $this->offers->sum('TotalPaid');
}

// In your Offer Model
protected $appends = ['totalPaid'];

public function getTotalPaidAttribute()
{
    return $this->orders->sum('paid');
}

// use it : 
foreach($venues as $venue){
     //do what you want with it
     $venue->totalPaid;
}

(Edited)

As said in the comments, this method may be cleaner, but not efficient :

Efficient Way:

// In your Venue Model
public function orders(){
    return $this->hasManyThrough(Order::class, Offer::class)
        ->selectRaw('sum(paid) as aggregate, venue_id')
        ->groupBy('venue_id');
}

public function totalPaid(){
    if ( ! array_key_exists('orders', $this->relations)) $this->load('orders');
    $relation = $this->getRelation('orders');

     return ($relation) ? $relation->aggregate : 0;
}

public function getTotalPaidAttribute()
{
    return $this->totalPaid();
}

maybe I messed up with your keys, you might have to use the full declaration of the relationship:

return $this->hasManyThrough(
        Order::class, Offer::class
        'venue_id', 'offer_id', 'id'
    );

but I just did with my project and it works like a charm

Upvotes: 3

Related Questions