Farshid Rezaei's Logo

الگوی فیلتر در لاراول به صورت شی‌گرا

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

Farshid Rezaei 2 weeks ago
0 7 0 7:45

الگوی فیلتر در لاراول به صورت شی‌گرا


فرض کنید که کنترلر PostController رو داریم و می خواهیم فیلتر هایی رو روی مدل Post اعمال کنیم. برای پیاده سازی یک API جهت فیلتر، فیلترهای مورد انتظار رو از پارامتر های درخواست میگیریم و با اعمال یک سری شرط روی مدل مربوطه، کوئری متناسب را می سازیم.

ساده‌ترین روشی که برای اعمال شرط‌ها و ساختن کوئری متناسب با فیلترهای درخواستی به ذهن میرسه، به صورت زیره:

class PostController
{
    public function index()
    {
        /** @var Builder $userQuery */
        $query = User::query();

        if ($title=request()->has("title")) {
           $title=request()->input("title");
           $query->where("title","like","%$title%");
        }

        if ($title=request()->has("author")) {
           $author=request()->input("author");
           $query->whereRealtion("autohrs","name","like","%$author%");
        }

        $limit = request()->get("limit") ?? 15;

        return $query->latest()->paginate($limit);
    }

}

در کد بالا ، مقادیر title و author ، پارامتر هایی هستند که می خواهیم Post را بر اساس آن فیلتر کنیم. 

 

 این روش برای برگرداندن داده های فیلتر شده، فقط در برنامه های کوچک کاربرد دارد. اگر یک برنامه با مجموعه ی سنگینی از فیلترها داشته باشیم، مجبوریم منطق فیلتر را با تکرار شدن IF در کنترلر خودمان پیاده سازی کنیم. این باعث می شود کنترلر ما بسیار حجیم و پیچیده شود. 

حال بیایید برخی از اصول OOP را برای استخراج منطق ها و encapsulate کردن آن ها در کلاس های جداگانه، پیاده سازی کنیم. 

  1. ابتدا یک کلاس abstract به نام AbstractFilter ایجاد می کنیم. سپس متد ()apply را در داخل آن می نویسیم که در زیر نشان داده شده است. ما در مورد این سازوکار در ادامه این مقاله صحبت خواهیم کرد. 
namespace App\Services\Filter;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;

/**
 * Class AbstractFilter
 *
 * @package App\CriteriaFilters
 */
abstract class AbstractFilter
{
    /**
     * @var
     */
    private $filters;
    /**
     * @var
     */
    protected $query;

    /**
     * BaseCriteria constructor.
     *
     * @param Builder $query
     * @param array $filters
     */
    public function __construct()
    {
        $this->filters = request()->toArray();
    }

    /**
     * @return Builder
     */
    public function apply(Builder $query): Builder
    {
        $this->query = $query;
        foreach ($this->filters as $filter => $value) {
            if ($this->isFilterApplicable($filter)) {
                $this->query = call_user_func_array([$this, $this->getFilterMethodName($filter)], [$value]);
            }
        }

        return $this->query;
    }

    /**
     * @param string $filter
     *
     * @return bool
     */
    private function isFilterApplicable(string $filter): bool
    {
        if ($filter!=='apply' && empty(Arr::get($this->filters, $filter))) {
            return false;
        }

        return $this->hasSuitableFilterMethod($filter);
    }

    /**
     * @param string $filter
     *
     * @return bool
     */
    private function hasSuitableFilterMethod(string $filter): bool
    {
        $methodName = $this->getFilterMethodName($filter);

        return method_exists($this, $methodName);
    }

    /**
     * @param string $filter
     *
     * @return string
     */
    private function getFilterMethodName(string $filter): string
    {
        return Str::camel($filter);
    }
}

در متد construct این کلاس ما فیلترها و مقادیر رو از آبجکت  request میگیریم. در متد apply  یکرپارامتر تعریف شده که کوئری ابتدایی هستش. داخل این متد ، یک حلقه بر اساس آرایه مقادیر فیلتر ایجاد شده که عملیاتی را روی تک تک پارامتر های فیلتر انجام می دهد. این آرایه، یک آرایه ی key-value است که key نشان دهنده ی نوع فیلتر و value، مقدار مورد نظر است و بر اساس پردازش های این کلاس، نام متدی که به آن ارجاع داده می شود، به دست خواهد آمد برای مثال، خالی بودن مقدار آن را بررسی کرده و رشته را به camelCase تبدیل می کند. 

یک اینترفیس FilterInterface هم به صورت زیر ساخته میشود.

namespace App\Services\Filter;

use Illuminate\Database\Eloquent\Builder;


interface FilterInterface
{
      public function apply(BUilder $builder): Builder;  
}

2. برای یکپارچه کردن منطق فیلتر برای کنترلر Post یک کلاس فیلتر جداگانه ایجاد می کنیم که AbstractFilter را extends می کند و از اینترفیس FilterInterface ، Implement میکند. اگر یادتان باشد، در ابتدای مقاله ما با استفاده از IF های تو در تو، فیلتر را در کلاس کنترلر قرار داده بودیم اما اکنون آن ها را به روش های جداگانه استخراج می کنیم.

 

namespace App\Http\Filters;

use Illuminate\Database\Eloquent\Builder;
use App\Services\Filter\AbstractFilter;
use App\Services\Filter\FilterInterface;


/**
 * Class PostFilter
 *
 * @package App\Services\Filter\AbstractFilter
 */
class PostFilter extends AbstractFilter implements FilterInterface
{
    /**
     * @param string $title
     *
     * @return Builder
     */
    public function title(string $title): Builder
    {
        return $this->query->when($title,fn(Builder $filter)=>$filter->where("title", "LIKE","%$title%"));
    }
    
     /**
     * @param string $author
     *
     * @return Builder
     */
    public function author(string $author): Builder
    {
        return $this->query->when($author,fn(Builder $filter)=>$filter->whereRelation("authors","name","LIKE", "%$author%"));
    }

}

 

شرح چند نکته

در کد های بالا، نام متد بر اساس نکته های زیر به دست آمده است: 

  • نام متد هایی که در کلاس فیلتر ایجاد می شود، باید کلید پارامترهای کوئری باشد که از URL دریافت شده است. 
  • نام متد باید به صورت camelCase نوشته شود. 

برای مثال، URL مربوط در مثال بالا به صورت

posts?title=learn&age=farshid/

 است و چون ما می خواستیم بر اساس title و author فیلتر کنیم، پس باید به ترتیب دو متد به نام های ()title و  ()author ایجاد می کردیم. 

3. حال یک scope محلی را در کلاس Model مورد نظر خود اضافه می کنیم. این متد نمونه ای از Eloquent Builder را به عنوان اولین پارامتر و مجموعه ای از فیلترها را به عنوان پارامتر دوم می پذیرد. 

برای راحتی و استفاده مجدد و جلوگیری از کد اضافه این اسکوپ را در تریت Filterable ایجاد میکنیم و در مدل مربوطه use میکنیم.

namespace App\Services\Filter;

use Illuminate\Database\Eloquent\Builder;

trait Filterable
{
    /**
     * @param Builder $query
     * @param array $filters
     *
     * @return Builder
     */
    public function scopeFilter(Builder $query, FilterInterface $filter): Builder
    {
        return $filter->apply($query);
    }
}

این کار به سادگی PostFilter را که ما در مرحله 2 تعریف کرده بودیم، نمونه سازی کرده و متد ()apply را فراخوانی می کند. 

use Illuminate\Database\Eloquent\Builder;
use App\Services\Filter\Filterable;

class Post extends Model
{
  //...
   use Filterable;
  //...
}

 

4. در آخر، ما می توانیم به سادگی scope با نا  filter از کلاس مدل خود را فراخوانی کنیم و تمام پارامترهای کوئری را به عنوان آرگومان های خود به آن منتقل کنیم. 

class PostController
{
    public function index(PostFilter $filter)
    {
        $limit = request()->get("limit") ?? 15;

        return Post::filter($filter)->latest()->paginate($limit);
    }
}

 

در پشت صحنه چه اتفاقی می افتد؟ 

  • ابتدا در کلاس کنترلر، scope محلی با نام filter را که در trait مربوطه ایجاد و در مدل Post استفاده کرده ایم، اعمال می کنیم. به عنوان یک پارامتر، ما یک نمونه از فیلتر مربوطه را پاس می دهیم. 
  • در مرحله ی بعد،  متد ()apply آن را فراخوانی می شود. 
  • کلاس فیلتری که ایجاد کرده بودیم، AbstractFilter را extends می کند. این کلاس شامل متدهای جداگانه ای است که در آن عملیات فیلتر، نوشته شده است. 
  • متد ()apply از کلاس AbstractFilter به گونه ای ساخته شده است که برای هر پارامتر کوئری از URL فعلی، تکرار می شود و متد فیلتر تعریف شده در زیر کلاس های آن را فراخوانی می کند. 
  • این متدها، مثلا مسئول اتصال (bind) عبارت where در Eloquent builder هستند. 

این راه کار، روشی تمیز جهت ایجاد یک API با فیلتر های زیاد است. اگر مجبورید فیلتر های مشابهی را برای موجودیت دیگری پیاده سازی کنید، کافی است یک کلاس Filter جدید ایجاد کنید که کلاس AbstractFilter را extends کند .

حال شما با توجه به شرایط مختلف میتوانید فیلتر های متنوع و پیچیده ای پیاده سازی کنید.

منبع 

https://ashishakya.medium.com/filters-in-laravel-with-oop-practices-9d7c646fa3fd

Comments

Submit Comment