فیلترها در لاراول با روش های OOP

فیلترها در لاراول با روش های OOP

فیلترها در لاراول با روش های OOP 

به احتمال زیاد با موقعیتی رو به رو شده اید که نیاز به فیلتر کردن پرس و جو بر اساس پارامترهای خاص داشته باشید. در این مقاله قصد داریم به صورت مختصر، فیلتر کردن با روش های OOP را برایتان شرح دهیم. 

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

معمول ترین روشی که برای اعمال شرط ها و ساختن کوئری متناسب با فیلترهای درخواستی کاربر می توان در پیش گرفت، به صورت زیر است. در کد زیر، مقادیر username و age، پارامتر هایی هستند که می خواهیم کاربران را بر اساس آن فیلتر کنیم. 

 class UserController

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

        if (request()->has("username")) {
            $username = request()->get("username");

            $userQuery = $userQuery->where("username", $username);
        }
        if (request()->has("age")) {
            $age = request()->get("age");

            $userQuery = $userQuery->where("age", $age);
        }
        $limit = request()->get("limit") ?? 15;

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

}

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

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

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

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

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

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

    /**
     * @return Builder
     */
    public function apply(): Builder
    {
        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 (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 این کلاس 2 پارامتر تعریف شده است. پارامتر اول کوئری و پارامتر دوم، آرایه ای از مقادیر فیلتر می باشد. در متد apply، یک حلقه بر اساس آرایه مقادیر فیلتر ایجاد شده که عملیاتی را روی تک تک پارامتر های فیلتر انجام می دهد. این آرایه، یک آرایه ی key-value است که key نشان دهنده ی نوع فیلتر و value، مقدار مورد نظر است و بر اساس پردازش های این کلاس، نام متدی که به آن ارجاع داده می شود، به دست خواهد آمد برای مثال، خالی بودن مقدار آن را بررسی کرده و رشته را به camelCase تبدیل می کند. 

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

namespace App\CriteriaFilters;

use Illuminate\Database\Eloquent\Builder;

/**
 * Class UserFilter
 *
 * @package App\CriteriaFilters
 */
class UserFilter extends BaseFilter
{
    /**
     * @param string $username
     *
     * @return Builder
     */
    public function username(string $username): Builder
    {
        return $this->query->where("username", $username);
    }

    /**
     * @param int $age
     *
     * @return Builder
     */
    public function age(int $age): Builder
    {
        return $this->query->where("age", $age);
    }
}

شرح چند نکته

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

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

برای مثال، URL مربوط به مثال بالا به صورت some-url?username=20&age=2 است و چون ما می خواستیم بر اساس username و age فیلتر کنیم، پس باید به ترتیب دو متد به نام های ()username و  ()age ایجاد می کردیم. 

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

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

use Illuminate\Database\Eloquent\Builder;

class User extends Model
{
    /**
     * @param Builder $query
     * @param array $filters
     *
     * @return Builder
     */
    public function scopeFilterBy(Builder $query, array $filters): Builder
    {
        return (new UserFilter($query, $filters))->apply();
    }
}

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

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

        return User::filterBy(request()->all())->latest()->paginate($limit);
    }
}

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

  • ابتدا در کلاس کنترلر، scope محلی با نام filterBy را که در مدل user ایجاد کرده ایم، اعمال می کنیم. به عنوان یک پارامتر، ما آرایه پارامترهای کوئری را با استفاده از ()request()->all، پاس می دهیم. 
  • در مرحله ی بعد،  scope محلی filterBy، یک نمونه از کلاس UserFilter می سازد و متد ()apply آن را فراخوانی می کند. به خاطر دااشته باشید که، وقتی UserFilter را نمونه سازی می کنیم، باید نمونه ایجاد کننده کوئری و آرایه قبلی پارامترهای پرس و جو را برای فیلترها منتقل کنیم. 
  • کلاس فیلتری که ایجاد کرده بودیم، BaseFilter را extends می کند. این کلاس شامل متدهای جداگانه ای است که در آن عملیات فیلتر، نوشته شده است. 
  • متد ()apply از کلاس BaseFilter به گونه ای ساخته شده است که برای هر پارامتر کوئری از URL فعلی، تکرار می شود و متد فیلتر تعریف شده در زیر کلاس های آن را فراخوانی می کند. 
  • این متدها، مسئول اتصال (bind) عبارت where در Eloquent builder هستند. 

این راه کار، روشی تمیز جهت ایجاد یک API با فیلتر های زیاد است. اگر مجبورید فیلتر های مشابهی را برای موجودیت دیگری پیاده سازی کنید، کافی است یک کلاس Filter جدید ایجاد کنید که کلاس BaseFilter را extends کند و یک scope محلی به کلاس Model مربوط به آن اضافه کنید.

منبع 

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

از بهترین نوشته‌های کاربران سکان آکادمی در سکان پلاس