Poznajemy funkcje Laravela, bez których trudno sobie dziś wyobrazić życie. Do dzieła.

Ok, najpierw withCount:

Artisan::command('movies-with-count', function () {

    $movies = Movie::withCount('reviews')->get();
    foreach($movies as $movie){
        $this->comment("Movie title: {$movie->title}");
        $this->comment("Movie length: {$movie->length}");
        $this->comment("Number of reviews: {$movie->reviews_count}");
    }
});

Dodaje nam ono pole reviews_count, zawierające ilość recenzji. Ciekawostka – można zaciągać wiele countów jednym wywołaniem, przekazując tablicę:

$users = User::withCount(['posts', 'comments'])->get();

Teraz withExists, już to robiliśmy, proste:

Artisan::command('movies-with-exists', function () {

    $movies = Movie::withExists('reviews')->get();
    foreach($movies as $movie){
        $this->comment("Movie title: {$movie->title}");
        $this->comment("Movie length: {$movie->length}");
        if($movie->reviews_exists)
            $this->comment("Has reviews: true");
        else
            $this->comment("Has reviews: false");
    }
});

Teraz withAvg, funkcja agregująca i musimy podać średnią czego ma wyliczać, czyli nazwa relacji i nazwa pola (kolumny):

Artisan::command('movies-with-avg', function () {

    $movies = Movie::withAvg('reviews', 'rating')->get();
    foreach($movies as $movie){
        $this->comment("Movie title: {$movie->title}");
        $this->comment("Movie length: {$movie->length}");
        $this->comment("Reviews avg rating: {$movie->reviews_avg_rating}");
    }
});

Pozostałe funkcje SQL, czyli min, max i sum też mają swój odpowiednik:

Artisan::command('movies-with-minmax', function () {

    $movies = Movie::withSum('reviews', 'rating')
    ->withMin('reviews','rating')
    ->withMax('reviews', 'rating')
    ->get();
    
    foreach($movies as $movie){
        $this->comment("Movie title: {$movie->title}");
        $this->comment("Reviews - min rating: {$movie->reviews_min_rating}");
        $this->comment("Reviews - max rating: {$movie->reviews_max_rating}");
        $this->comment("Reviews - sum of all ratings: {$movie->reviews_sum_rating}");
    }
});

Ok, ale jak to wszystko działa? Zdebugujmy:

Artisan::command('movies-with-count', function () {

    $m = Movie::withCount('reviews')->dd();
    $this->comment("Min rating:");
    
});

To dostaniemy:

select `movies`.*, 
(select count(*) from `reviews` where `movies`.`id` = `reviews`.`movie_id`) as `reviews_count` 
from `movies`

I to jest to, o czym mówiłem dawno temu, czyli selecty z subqueries są wykonywane dla każdego rekordu z selecta rodzica, taka jakby zagnieżdżona pętla.

Ok, sprawdźmy w sql czy zadziała. Następnie piszemy dla withAvg:

select `movies`.*, 
(select avg(`reviews`.`rating`) from `reviews` where `movies`.`id` = `reviews`.`movie_id`) as `reviews_avg_rating` 
from `movies`; 

I to jest ta pętla w pętli. Każdy rekord z głównego selekta ma swoje id i dla każdego id jest selekt z sub-query odpalany, inaczej by to wszystko nie było w stanie zadziałać.

Swoją drogą nienawidzę sposobu, w jaki Laravel buduje te queries. Wolę sobie nazwy tabel poaliasować, wtedy jest (moim zdaniem) dużo czytelniej:

select m.*, 
(select avg(r.rating) 
from `reviews` as r 
where m.id = r.movie_id) as `reviews_avg_rating` 
from `movies` as m; 

Mam nadzieję, że na tym poziomie te subqueries i inne bzdety nie robią na nas specjalnego wrażenia. Inaczej trudno pójść dalej z materiałem, jeżeli SQL będzie nas przerastać.

PS. Teraz powinniśmy umieć napisać czystym SQL withMin, withMax, withSum. Przerastać nas może withExists, już pokazuję, jak do tego podejść:

select m.*,
exists(
select * 
from `reviews` as r 
where m.id = r.movie_id
) as `reviews_exists` 
from `movies` as m;

Funkcja exists, kolejna funkcja SQL, którą warto zapamiętać.