12

I have three tables:

products:   id|name|description|slug|category_id|...
reviews:    id|product_id|review_text|name|email|...
review_rows id|review_id|criteria|rating

the review table stores the review text, writer of the review and has a foreign product_id key. The review_rows table stores the ratings for different criteria like:

----------------------------------------
| id |  criteria  | rating | review_id |
----------------------------------------
|  1 |  price     | 9      | 12        |
----------------------------------------
|  2 |  service   | 8      | 12        |
----------------------------------------
|  3 |  price     | 6      | 54        |
----------------------------------------
|  4 |  service   | 10     | 54        |
----------------------------------------

review rows are linked to the review table with the review_id foreign key. I've set up my model relationships like this:

Product   -> hasMany   -> Review
Review    -> belongsTo -> Product
Review    -> hasMany   -> ReviewRow
ReviewRow -> belongsTo -> Review

Now I would like to display the average rating for a product on my category and product pages. How can I achieve this?

I need to sum and average all the reviewRows per review and then sum and average all of those for each review to end up with the overall rating for that product. Is this possible via Eloquent or do I need a different solution or a different database design/structure?

Thanks in advance!

Jarek Tkaczyk
  • 78,987
  • 25
  • 159
  • 157
Luuk Van Dongen
  • 2,391
  • 6
  • 26
  • 40

5 Answers5

31

You need something like this http://softonsofa.com/tweaking-eloquent-relations-how-to-get-hasmany-relation-count-efficiently/ only slightly adjusted to match your needs:

public function reviewRows()
{
    return $this->hasManyThrough('ReviewRow', 'Review');
}

public function avgRating()
{
    return $this->reviewRows()
      ->selectRaw('avg(rating) as aggregate, product_id')
      ->groupBy('product_id');
}

public function getAvgRatingAttribute()
{
    if ( ! array_key_exists('avgRating', $this->relations)) {
       $this->load('avgRating');
    }

    $relation = $this->getRelation('avgRating')->first();

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

Then as simple as this:

// eager loading
$products = Product::with('avgRating')->get();
$products->first()->avgRating; // '82.200' | null

// lazy loading via dynamic property
$product = Product::first()
$product->avgRating; // '82.200' | null
Jarek Tkaczyk
  • 78,987
  • 25
  • 159
  • 157
  • Wow, really neat solution! This fits nicely with the Laravel's syntax and ideology! I have one question though. If I run the query, it returns the rating with all the other information on the first review_row, but I would like to only return the avg(rating) value. – Luuk Van Dongen Dec 30 '14 at 11:45
  • It returns only `aggregate` and `product_id` - I just edited the answer, previously I forgot the latter. Anyway it shouldn't include anything but the two. – Jarek Tkaczyk Dec 30 '14 at 13:47
2

Maybe you can try with Eloquent relationships and a little help from php function array_reduce

//model/Reviews.php
public function sum() {
    return array_reduce($this->hasMany('ReviewRows')->lists('rating'), "sumItems");  
}

public function sumItems ($carry, $item) {
    $carry += $item;
    return $carry;
}

Or with Eloquent RAW querys like:

//model/Reviews.php
public function avg() {
   $result = $this->hasMany('ReviewRows')
   ->select(DB::raw('avg(rating) average'))
   ->first();
   return $result->average;
}
Edgar Orozco
  • 2,722
  • 29
  • 33
1

Simple and easy solution. Add this into product model

protected $appends = ["avg_rating"];  

public function reviewRows()
{
    return $this->hasManyThrough('App\ReviewRow','App\Review','product_id','review_id');
}

public function getAvgRatingAttribute()
{
    return round($this->reviewRows->average('rating'),2);
}
0

see https://github.com/faustbrian/laravel-commentable

public function comments(): MorphMany
    {
        return $this->morphMany($this->commentableModel(), 'commentable');
    }

    public function avgRating()
    {
        return $this->comments()->avg("rating");
    }

    $products = \App\Models\Products::with(
        [
            "comments" => function ($q) {
                $q->with(["children" => function ($qch) {
                    $qch->take(2);
                }
                ])->withCount("children")->where("parent_id", '=', null);
            },]
    )->take(5)->get();

    foreach ($products as &$product) {
        $product["avgRating"] = $product->avgRating();
    }

   dd($products);


yemenpoint
  • 167
  • 3
  • 5
0

use withAvg() as mentioned in laravel official documentation here

Prafful Panwar
  • 439
  • 7
  • 19