Концепция
Идея заключается в создании универсального класса QueryFilter для работы с фильтрами.
GET /posts?title=source&status=active
Использую данный пример, мы будем фильтровать посты (модель Post) по следующим критериям:
- Наличие слова «source» в поле title;
- Значение «publish» в поле status;
Пример приложения
Модель Post
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Post extends Model { /** * The attributes that are mass assignable. * * @var array */ protected $fillable = [ 'name', 'id', 'title', 'slug', 'status', 'type', 'published_at', 'updated_at', ]; }
Добавляем маршрут:
Route::get('/posts', 'PostController@index');
Создаем файл Resource\Post для вывода в формате JSON:
namespace App\Http\Resources; use Illuminate\Http\Resources\Json\JsonResource; class Post extends JsonResource { /** * Transform the resource into an array. * * @param \Illuminate\Http\Request $request * @return array */ public function toArray($request) { return [ 'id' => $this->ID, 'title' => $this->post_title, 'slug' => $this->post_name, 'status' => $this->post_status, 'type' => $this->post_type, 'published_at' => $this->post_date, 'updated_at' => $this->post_modified, ]; } }
И сам контроллер с одним единственным действием:
namespace App\Http\Controllers; use App\Http\Resources\Post as PostResource; use App\Post; class PostController extends Controller { /** * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection */ public function index() { $posts = Post::limit(10)->get(); return PostResource::collection($posts); } }
Стандартная фильтрация организуется следующим кодом:
/** * @param Request $request * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection */ public function index(Request $request) { $query = Post::limit(10); if ($request->filled('status')) { $query->where('post_status', $request->get('status')); } if ($request->filled('title')) { $title = $request->get('title'); $query->where('post_title', 'like', "%$title%"); } $posts = $query->get(); return PostResource::collection($posts); }
С таким подходом мы сталкиваемся с разрастанием контроллера, что нежелательно.
Внедрение QueryFilter
Смысл такой концепции заключается в том, чтобы использовать для каждой сущности отдельный класс, сопоставляющий методы с каждым полем, по которым предстоит фильтрация.
Фильтровать предстоит по запросу:
GET /posts?title=source&status=publish
Для фильтрации у нас будет класс PostFilter и методы title() и status(). PostFilter будет расширять абстрактный класс QueryFiler который отвечает за сопоставление методов класса с передаваемыми параметрами.
Метод apply()
Класс QueryFIlter имеет метод apply(), который отвечает за вызов фильтров, имеющихся в дочернем классе PostFilter.
namespace App\Http\Filters; use Illuminate\Database\Eloquent\Builder; use Illuminate\Http\Request; abstract class QueryFilter { /** * @var Request */ protected $request; /** * @var Builder */ protected $builder; /** * @param Request $request */ public function __construct(Request $request) { $this->request = $request; } /** * @param Builder $builder */ public function apply(Builder $builder) { $this->builder = $builder; foreach ($this->fields() as $field => $value) { $method = camel_case($field); if (method_exists($this, $method)) { call_user_func_array([$this, $method], (array)$value); } } } /** * @return array */ protected function fields(): array { return array_filter( array_map('trim', $this->request->all()) ); } }
Суть в том, что для каждого поля переданного через Request мы имеем отдельный метод в дочернем классе фильтра (класс PostFilter). Это позволяет нам настраивать логику для каждого поля фильтра.
Класс PostFilter
Теперь перейдем к созданию класса PostFilter расширяющего QueryFilter. Как говорилось ранее, этот класс должен содержать методы для каждого поля, по которым нам предстоит фильтрация. В нашем случае методы title($value) и status($value)
namespace App\Http\Filters; use Illuminate\Database\Eloquent\Builder; class PostFilter extends QueryFilter { /** * @param string $status */ public function status(string $status) { $this->builder->where('post_status', strtolower($status)); } /** * @param string $title */ public function title(string $title) { $words = array_filter(explode(' ', $title)); $this->builder->where(function (Builder $query) use ($words) { foreach ($words as $word) { $query->where('post_title', 'like', "%$word%"); } }); } }
Здесь я не вижу смысла для подробного разбора каждого из методов, вполне стандартные запросы. Смысл заключается в том что теперь мы имеет отдельный метод для каждого поля и можем применять какую угодно логику для формирования запроса.
Создаем scopeFilter()
Теперь нам необходимо связать модель и конструктор запросов
/** * @param Builder $builder * @param QueryFilter $filter */ public function scopeFilter(Builder $builder, QueryFilter $filter) { $filter->apply($builder); }
Для поиска нам необходимо вызвать метод filter() и передать экземпляр QueryFilter, в нашем случае PostFilter.
$filteredPosts = Post::filter($postFilter)->get();
Таким образом, вся логика фильтрации обрабатывается вызовом метода filter($postFilter), избавляя контроллер от лишней логики.
Для облегчения повторного использования можно поместить метод scopeFilter в трейт и использовать его для каждой модели, которую необходимо фильтровать.
namespace App\Http\Filters; use Illuminate\Database\Eloquent\Builder; trait Filterable { /** * @param Builder $builder * @param QueryFilter $filter */ public function scopeFilter(Builder $builder, QueryFilter $filter) { $filter->apply($builder); } }
В Post добавляем:
class Post extends CorcelPost { use Filterable;
Осталось добавить в качестве параметра PostFilter в метод контроллера index() и вызвать метод модели filter().
class PostController extends Controller { /** * @param PostFilter $filter * @return \Illuminate\Http\Resources\Json\ResourceCollection */ public function index(PostFilter $filter) { $posts = Post::filter($filter)->limit(10)->get(); return PostResource::collection($posts); } }
На этом все. Мы переместили всю логику фильтрации в соответствующий класс, соблюдая принцип единой ответственности (S в системе принципов SOLID)
Вывод
Такой подход к реализации фильтров позволяет придерживаться прицепа тонкий контроллер, а также облегчает написание тестов.
Здесь приведен пример с использование PHP и Laravel. Но как я и говорил, это концепция и может работать с любым языком или фреймворком.
Ссылки
- Блог Junior Grossi
- Готовый пакет от Killian Blais
ссылка на оригинал статьи https://habr.com/ru/post/485520/
Добавить комментарий