نماد سایت حامد نادرفر

آشنایی با اسکوپ‌های لاراول Query Scopes

در هر برنامه‌ای یک سری کوئری‌های الوکوئنت وجود داره که توی اونها مجبوریم همیشه شرط و محدودیت‌های خاصی رو رعایت کنیم. مثلاً:

Post::where('status', 'published')->get();
Post::where('status', 'published')->where('category', 9)->get();
Post::where('status', 'published')->where('id', 429)->first();

همونطور که می‌بینیم شرط where('status', 'published') داره توی همه کوئری‌های ما تکرار میشه. یک اصل توی برنامه‌نویسی و توسعه نرم‌افزارها وجود داره اینه که «تا جایی که می‌تونیم از تکرار جلوگیری کنیم».

این موضوع زمانی دردسر ساز میشه که می‌خوایم عنوان‌های status و published رو عوض کنیم که برای اعمال تغییرات باید تمام کوئری‌های توی برنامه رو پیدا کنیم که کار بهینه‌ای نیست. برای حل دو مشکل بالا، لاراول یک راه حل جالب در اختیار ما قرار داده به اسم Query Scope که توی این پست می‌خوایم با اونها آشنا بشیم.

Query Scope چیه؟

با اسکوپ‌های کوئری می‌تونیم کوئری‌های کوتاه‌تر، معنادارتر و زیباتری توی Eloquent داشته باشیم. کوئری ابتدای پست رو در نظر بگیرید. با اسکوپ‌ها می‌تونیم اون رو بصورت زیر بنویسیم:

Post::active()->get();

حتی می‌تونیم کاری کنیم که بدون نوشتن شرط‌ها (مثلاً با نوشتن Post::all()) شرط‌های مورد نظر ما اعمال بشن.

ما دو نوع اسکوپ داریم. اسکوپ سراسری (Global Scope) و اسکوپ محلی (Local Scope)

اسکوپ سراسری

این نوع اسکوپ، محدودیت‌های مد نظر ما رو به صورت سراسری و روی همه کوئری‌های الوکوئنت اعمال می‌کنه. وقتی یک محدودیت سراسری درست می‌کنیم، دیگه لازم نیست محدودیت‌ها رو توی کوئری‌ها تکرار کنیم. اگه از Soft Delete لاراول استفاده کرده باشید، جالبه که بدونیم این ویژگی در واقع یک محدودیت سراسری روی همه کوئری‌ها اعمال می‌کنه.

نوشتن یک اسکوپ سراسری

برای این کار، باید یک کلاس درست کنیم. اینکه فایل کلاس رو کجا قرار بدیم تفاوتی نداره، اما بهتره توی مسیر app یک پوشه درست کنیم به اسم Scopes و کلاس رو اونجا قرار بدیم. اسم فایل و کلاس رو می‌ذاریم Published:

<?php

namespace App\Scopes;

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

class Published implements Scope
{
    public function apply(Builder $builder, Model $model)
    {
        // ...
    }
}

کلاس ما باید اینترفیس Illuminate\Database\Eloquent\Scope رو پیاده‌سازی کنه. این اینترفیس یک متد داره به اسم apply با پارامترهایی که توی خط ۱۱ می‌بینید که باید توسط کلاس پیاده‌سازی بشه.

توی متد apply می‌تونیم شرط و محدودیت‌های دلخواهمون رو بنویسیم:

public function apply(Builder $builder, Model $model)
{
    return $builder->where('status', 'published');
}

مرحله بعد باید این کلاس رو به مدل شناسایی کنیم. مدل مد نظرمون رو باز می‌کنیم. باید متد booted مدل رو رونوشت کنیم. این متد رو می‌نویسم و توی اون با استفاده از متد addGlobalScope، کلاس اسکوپ رو به مدل شناسایی می‌کنیم:

<?php

namespace App\Models;

// ...
use App\Scopes\Published;

class Post extends Model
{
    // ...

    protected static function booted()
    {
        static::addGlobalScope(new Published());
    }
}

خب از حالا به بعد، هر کوئری که توی برنامه بنویسیم، بطور خودکار شامل محدودیت تعریف شده میشه. یعنی اگه بنویسیم:

Post::all();

کوئری SQL اون به صورت زیر خواهد بود:

select * from `post` where `status` = "published"

اسکوپ‌های سراسری بی‌نام

بجای نوشتن یک کلاس اختصاصی، می‌تونیم توی خود مدل، یک اسکوپ سراسری درست کنیم. این کار با کلوژرها امکان پذیر هست. متد booted و addGlobalScope رو به صورت زیر باز نویسی می‌کنیم:

protected static function booted()
{
    static::addGlobalScope('published', function(Builder $builder) {
        $builder->where('status', 'published');
    });
}

توی این روش، آرگومان اول متد addGlobalScope اسم دلخواه ما برای اسکوپ هست.

کوئری‌های معاف از اسکوپ سراسری

اگه می‌خوایم برای یک کوئری خاص، اسکوپ سراسری اعمال نشه، می‌تونیم توی کوئری از متد withoutGlobalScope استفاده کنیم و مسیر کلاس اسکوپ مد نظرمون رو بهش پاس می‌دیم:

Post::withoutGlobalScope(App\Scopes\Published::class)->get();

اگه اسکوپ سراسری ما بصورت کلوژر هست، کافیه اسم اسکوپ رو پاس بدیم:

Post::withoutGlobalScope('published')->get();

اگه می‌خوایم چندین اسکوپ رو حذف کنیم از متد withoutGlobalScopes استفاده می‌کنیم:

Post::withoutGlobalScopes([
    FirstScope::class, SecondScope::class
])->get();

اگه به این متد هیچ چیزی پاس ندیم، همه اسکوپ‌های سراسری برای این کوئری حذف میشن:

Post::withoutGlobalScopes()->get();

اسکوپ‌های محلی (Local Scope)

اگه توی بعضی (نه همه) کوئری‌های ما شرط‌ها و محدودیت‌هایی وجود داره که تکراری هستن و یا ظاهر پیچیده‌ای دارن، می‌تونیم از اسکوپ‌های محلی استفاده کنیم.

ما اسکوپ‌های محلی رو توی خود مدل می‌نویسیم. فرض کنیم اسم اسکوپ مد نظر ما published هست. برای تعریف کردن یک اسکوپ محلی، باید یک متد به اسم scopePublished توی مدل بنویسیم.

یک متد به همین اسم توی مدل مد نظرمون می‌نویسیم:

public function scopePublished($query)
{
    return $query->where('status', 'published');
}

می‌تونیم اسکوپ‌های دیگه هم اضافه کنیم:

public function scopePopular($query)
{
    return $query->where('visits', '>', 1000);
}

برای استفاده از اسکوپ‌هایی که نوشتیم، کافیه اسم اونها رو به صورت زیر توی کوئری‌ها ذکر کنیم (بدون پیشوند scope):

Post::popular()->published()->orderBy('created_at')->get();

همونطور که می‌بینیم، کوئری ما کوتاه‌تر و خواناتر شد

بدون استفاده از اسکوپ‌ها، این کوئری به صورت زیر خواهد بود:

Post::where('visits', '>', 1000)->where('status', 'published')->orderBy('created_at')->get();

توی کوئری بالا دو تا where دیده میشه. اگه می‌خوایم برای این اسکوپ‌ها orWhere داشته باشیم، از روش زیر استفاده می‌کنیم:

Post::popular()->orWhere->published()->get();

اسکوپ‌های محلی داینامیک

این نوع اسکوپ‌ها برای زمانی استفاده میشن که می‌خوایم هنگام نوشتن کوئری، به اسکوپ آرگومان پاس بدیم. نحوه نوشتن این اسکوپ‌ها بصورت زیر هست. کافیه هنگام تعریف اسکوپ، پارامتر مورد نظرمون رو تعریف کنیم:

public function scopeStatus($query, $status)
{
    return $query->where('status', $status);
}

و به صورت زیر از اونها استفاده می‌کنیم:

Post::status('published')->get();

منبع: https://ditty.ir/posts/laravel-eloquent-scopes/nxLDn