Паттерн "Фильтр" в Laravel
Паттерн “Фильтр” (Filter Pattern) — это архитектурный шаблон, который помогает отделить логику фильтрации данных от контроллеров и моделей. В Laravel этот паттерн часто используется для построения динамических запросов к базе данных на основе входящих данных из HTTP-запросов. Это позволяет сделать код более чистым, поддерживаемым и легко расширяемым.
Пример использования
Рассмотрим пример использования паттерна “Фильтр” на модели Product. Мы создадим фильтр для поиска продуктов по различным критериям, таким как название, категория, цена и наличие на складе.
Шаг 1: Создание фильтра
Создадим базовый класс Filter и конкретный фильтр для продуктов.
<?php
declare(strict_types=1);
namespace App\Core\Filters;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Str;
/**
* Class Filter
*/
abstract class Filter
{
public const KEYS_TO_BOOL = [];
public const KEYS_TO_INT = [];
public const KEYS_TO_DATE = [];
public const KEYS_STRING_TO_ARRAY = [];
public const KEYS_TO_ARRAY = [];
protected Builder $builder;
/**
* @param FormRequest $request
*/
public function __construct(protected readonly FormRequest $request)
{
}
/**
* Применение фильтров к запросу
*
* @param Builder $builder
* @return Builder
*/
public function apply(Builder $builder): Builder
{
$this->builder = builder;
foreach ($this->request->input() as $method => $value) {
$methodName = Str::camel($method);
if (null === $value) {
continue;
}
if (method_exists($this, $methodName)) {
if (in_array($method, static::KEYS_TO_BOOL, true)) {
$value = (bool)$value;
}
if (in_array($method, static::KEYS_TO_INT, true)) {
$value = (int)$value;
}
if (in_array($method, static::KEYS_TO_DATE, true)) {
$value = CarbonImmutable::parse($value);
}
if (in_array($method, static::KEYS_TO_ARRAY, true)) {
$value = is_array($value) ? $value : [$value];
}
if (in_array($method, static::KEYS_STRING_TO_ARRAY, true)) {
$value = explode(',', $value);
}
$this->builder = $this->{$methodName}($value);
}
}
return $this->builder;
}
}
Создадим конкретный фильтр для продуктов.
<?php
namespace App\Filters;
use Illuminate\Database\Eloquent\Builder;
/**
* Class ProductFilter
*/
class ProductFilter extends Filter
{
/**
* Фильтрация по названию
*
* @param string $value
* @return Builder
*/
protected function name(string $value): Builder
{
return $this->builder->where('name', 'like', '%' . $value . '%');
}
/**
* Фильтрация по категории
*
* @param string $value
* @return Builder
*/
protected function category(string $value): Builder
{
return $this->builder->where('category', $value);
}
/**
* Фильтрация по цене
*
* @param array $value
* @return Builder
*/
protected function price(array $value): Builder
{
return $this->builder->whereBetween('price', [$value['min'], $value['max']]);
}
/**
* Фильтрация по наличию на складе
*
* @param bool $value
* @return Builder
*/
protected function inStock(bool $value): Builder
{
return $this->builder->where('in_stock', $value);
}
}
Шаг 2: Создание запроса
Создадим FormRequest, который будет использоваться для валидации и передачи данных в фильтр.
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
/**
* Class ProductFilterRequest
*/
class ProductFilterRequest extends FormRequest
{
/**
* Правила валидации
*
* @return array
*/
public function rules(): array
{
return [
'name' => 'nullable|string|max:255',
'category' => 'nullable|string|max:255',
'price' => 'nullable|array',
'price.min'=> 'nullable|numeric|min:0',
'price.max'=> 'nullable|numeric|min:0',
'in_stock' => 'nullable|boolean',
];
}
}
Шаг 3: Использование трейт для применения фильтра
Создадим трейт HasFilter, который позволит легко применять фильтры к моделям.
<?php
declare(strict_types=1);
namespace App\Core\Traits;
use App\Core\Filters\Filter;
use Illuminate\Database\Eloquent\Builder;
/**
* Trait HasFilter
*
* @method static Builder filter(Filter $filter)
*/
trait HasFilter
{
/**
* @param Builder $builder
* @param Filter $filter
* @return Builder
*/
public function scopeFilter(Builder $builder, Filter $filter): Builder
{
return $filter->apply($builder);
}
}
Шаг 4: Применение фильтра в контроллере
Теперь используем наш фильтр в контроллере для получения продуктов.
<?php
namespace App\Http\Controllers;
use App\Filters\ProductFilter;
use App\Http\Requests\ProductFilterRequest;
use App\Models\Product;
use Illuminate\Http\JsonResponse;
/**
* Class ProductController
*/
class ProductController extends Controller
{
/**
* Получение списка продуктов с фильтрацией
*
* @param ProductFilterRequest $request
* @param ProductFilter $filter
* @return JsonResponse
*/
public function index(ProductFilterRequest $request, ProductFilter $filter): JsonResponse
{
$products = Product::filter($filter)->get();
return response()->json($products);
}
}
Заключение
Паттерн “Фильтр” позволяет легко и элегантно управлять сложными запросами к базе данных, отделяя логику фильтрации от контроллеров и моделей. Это улучшает читаемость кода и упрощает его поддержку. Такой подход также облегчает тестирование и расширение логики фильтрации без необходимости изменения основного кода. Использование трейт HasFilter дополнительно упрощает применение фильтров к моделям, делая код еще более модульным и переиспользуемым.