Web Development
20-11-2023

Jak używać niestandardowe query scope w eloquent we Frameworku Laravel

Dmytro Tus
Full Stack Web developer

Cześć. W tym wpisie jak pokażę, jak używać niestandardowe query scope w eloquent we Frameworku Laravel. Przyznam się, że pierwszy wpis pisałem w języku angielskim, i można ten wpis zobaczyć, jeśli kliknąć "EN" u góry strony.

Ten angielski wpis jest dosyć długi, i też częściowo pokrywa temat migracji, relacji. W tym nie będę opisywać ustawienia baz danych oraz ustawienia relacji, natomiast pokaże któtko pewne zagadnienia, które dotyczą właśnie niestandardowych scope'ów w Laravelu.

Zagadnienie nr.1: Laravel ma scope

Pierszę co warto zapamiętać, to jest to, że Eloquent ( ORM używany w Laravelu ) ma tzw. scopy. 

Co to są query scopes Odpowiem swoimi słowy: scope to jest zestaw zapytań do bazy danych, który można potem łatwo wykorzystać. Pokaże to na przykładzie

Mamy aplikację gdzie jest blog. I w admin panelu możemy ustawiać czy wpis jest opublikowany czy nie. Podobne podejście można zobaczyć np. w CMS Wordpress. Żeby to realizować najprawdopodobniej w bazie danych w tabeli z wpisami będziemy mieli tabele is_published, która może mieć wartość true czy false. Jeśli potrzebujemy pokazać tyko opublikowane posty to nasz kod będzie wyglądać tak:

namespace App\Services;

use App\Models\Post;

class PostService
{
    public function getPublishedPosts()
    {   
        $posts = Post::where('is_published', true)->get();

        return $posts;
    }
}

Jeśli potrzebujemy pobrać tylko opublikowane wpisy to napiszemy taki sam kod w innym miejscu. Na razie problemu nie ma.

A wyobraźmy sobie, że mamy w naszym blogu też lajki, i na przykład uważamy że wpis jest popularny jeśli ma więcej niż 100 lajków.

Teraz pobierzemy posty które są opublikowane i popularne i też sortujemy te posty od najpopularniejszego do najmniej popularnego.

namespace App\Services;

use App\Models\Post;

class PostService
{
    public function getPublishedPosts()
    {   
        $posts = Post::where('is_published', true)->where('likes', '>', 100)->orderByDesc('likes')->get();

        return $posts;
    }
}

 

Zrobiliśmy to, czego żyśmy chcieli, ale teraz, jeśli będziemy chcieli pobrać dokładnie takie same najpopularniejsze posty w innym miesjcu to będziemy musieli przepisywać cały kod jeszcze raz, a to nie jest dobrze.

I tutaj nam się przydadzą query scope'y czyli te same zestawy zapytań, o których pisałem wcześniej. Jak zaimplementować to wszytko. Już pokazuję:

Idziemy do Modelu Post i dopisujemy jedną metodę:

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;

class Post extends Model
{
    public function scopePublishedPopular(Builder $query): void
    {
        $query->where('is_published', true)->where('likes', '>', 100)->orderByDesc('likes');
    }
}

 

I teraz w naszym serwisie robimy tak:

namespace App\Services;

use App\Models\Post;

class PostService
{
    public function getPublishedPosts()
    {   
        $posts = Post::publishedPopular()->get();

        return $posts;
    }
}

 

W ten sposób uzyskamy te same wpisy, ale kod w serwisie jest krótszy i możemy go wykorzystać w innym miejscu.

Pełna dokumentacja o scopach ( Lokalnych i też Globalnych ) w Laravelu jest na stronie dokumentacji:

https://laravel.com/docs/10.x/eloquent#local-scopes

Zagadnienie nr.2: Eloquent is macroable ;)

Co to oznacza? Macro to to jest technika, za pomocą której możemy dodać dodatkowe metody do klas. W danym przypadku nas interesuje dodanie subQuery do Eloquent Builder. Dodamy taki kod do naszego AppServiceProvider:

class AppServiceProvider extends ServiceProvider
{

    public function boot()
    {

        Builder::macro('addSubSelect', function ($column, $query) {
            if (is_null($this->columns)) {
                $this->select($this->from . '.*');
            }
            return $this->selectSub($query, $column);
        });
    }

 

Teraz nasza klasa Builder może tworzyć tzw. wirtulna kolumnę. Cała logika pracuje po stronie bazy danych, i dlatego aplikacje będzie szybka.

Przykład: 

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;

class Post extends Model
{
    public function scopePublishedPopular(Builder $query): void
    {
        $query->where('is_published', true)->where('likes', '>', 100)->orderByDesc('likes');
    }

    public function scopeWithCopyOfTitle(Builder $query): void
    {
        $query->addSubSelect('copy_of_title', function ($query) {
            $query->select('title')
                ->limit(1);
        });
    }

}

I wtedy nasz wpis będzie miał wirtualną kolumnę, która będzie wyliczana na podstawie mysql, a nie php. I dzięki temu będziemy mieli czystszy kod i większą wydajność aplikacji.

 


Inne wpisy