فیلترها در لاراول با روش های 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 کردن آن ها در کلاس های جداگانه، پیاده سازی کنیم.
- ابتدا یک کلاس
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