У вас когда-нибудь было приложение Laravel, и вам нужны фильтры поверх вашего API? Возможно, вы пробовали GraphQL, однако вы могли бы написать простой и не нуждаться в каких-либо интеграциях.
В этой статье я собираюсь реализовать простой сервис, который мог бы работать с Laravel eloquent ORM, он будет обрабатывать отношения, а также основные операторы, которые используются чаще всего.
Идея
Вы разрабатываете RESTful API и хотите, чтобы ваши пользователи фильтровали индексы по разным полям или даже по отношениям вашей модели.
Например, простая модель статьи, в которой так много тегов и комментариев, а также пользователь, которому она принадлежит. Теперь у вас есть API, в котором перечислены все статьи/api/статьи. Как вы могли бы позволить пользователю фильтровать теги или комментарии? Как вы могли бы отфильтровать все статьи конкретного пользователя? Более того, как вы могли бы объединить их все вместе?
Давайте добавим фильтр в наш API, чтобы найти все статьи пользователя.
/api/статьи?фильтры[]=идентификатор пользователя=1
Кажется крутым? Теперь я хочу, чтобы статьи пользователя 1 имели определенный тег.
/api/статьи?фильтры[]идентификатор пользователя=1 и фильтры[]=tags.name = ipsa
На следующем шаге мы настроим базовый проект, а затем начнем добавлять в него эти динамические фильтры.
Установка
Я искал образец проекта в реальном мире и обнаружил, что существует полное приложение Laravel, которое можно было бы использовать. Взгляните на их репозиторий github и пройдите через * README *, чтобы настроить его.
Взгляните на их репозиторий github и пройдите через * README *, чтобы настроить его.
Расширьте идею
Мы знаем, что существует параметр запроса, называемый фильтрами, который содержит отношение/поле, оператор и значение.
Здесь мы собираемся реализовать основные логические операторы, которые являются = , != , > , < , >= и <= . Также я добавил 2 пользовательских оператора ~ и () .
~ будет работать так же, как в запросах с % вокруг него.
e.x. /api/статьи?фильтры[]=пуля~ реальный мир
SQL: ВЫБЕРИТЕ * ИЗ статей, где пуля похожа на "%реальный мир%";
() будет работать так же, как В запросах.
e.x. /api/статьи?фильтры[]user.id ()1,2
SQL: ВЫБЕРИТЕ * ИЗ статей, где идентификатор пользователя В(1,2);
Погрузитесь в кодирование
служба
Сначала нам нужно создать сервис, который мог бы принять запрос и создать для нас соответствующие запросы по данной модели.
Создайте каталог служб в папке приложения, затем создайте FilterQueryBuilder.php файл внутри него.
# app/Services/FilterQueryBuilder.php request = $request; } }
Теперь давайте добавим код шаг за шагом.
Нам нужен общедоступный метод в этом классе, который принимал бы красноречивый конструктор, анализировал фильтры и добавлял к нему соответствующие запросы. Теперь давайте добавим приведенный ниже метод в наш сервис.
# app/Services/FilterQueryBuilder.php
...
public function buildQuery($query) {
$filters = $this->request->query('filters');
$filters = $this->parseFilters($filters);
foreach($filters as $filter) {
$query = $this->addFiltersToQuery($query, $filter);
}
return $query;
}
...
Анализ фильтров
Для каждого заданного фильтра в параметре запроса нам нужно найти отношения/поле, оператор и значение, после чего мы сможем добавить их в наш запрос.
Давайте добавим метод getOperator, который принимает строковое значение фильтра и возвращает оператор для нас.
# app/Services/FilterQueryBuilder.php
...
private function getOperator($filter) {
$operatorsPattern = '/=|!=|\(\)|>=|<=|~|>|Благодаря регулярным выражениям очень легко найти оператора. Если вы не знакомы с регулярным выражением, узнайте об этом подробнее здесь . Также вы можете практиковать эти методы онлайн с помощью веб-сайта PHPLiveRegex .
Теперь, когда у нас есть оператор, мы могли бы разделить строку фильтра на отношения/поле и часть значения. Давайте добавим код для этого.
# app/Services/FilterQueryBuilder.php
...
private function getFilterValue($filter, $operator) {
$result = explode($operator, $filter);
if(count($result) == 2) {
return $result[1];
}
}
private function getFilterRelations($filter, $operator) {
$result = explode($operator, $filter);
if(count($result) == 2) {
return $result[0];
}
}
...
Дерево отношений
До сих пор мы находили отношения, оператора и значение в нашем фильтре. Как мы могли бы добавить эти отношения к нашему запросу?
Например, для нашего первого примера API нам нужно создать вложенный массив, подобный этому.
# /api/articles?filters[]user.id()1,2
Array
(
[user] => Array
(
[field] => id
[operator] => ()
[value] => 1,2
)
)
Для создания дерева отношений нам нужны ключи отношений, оператор и значение, которые уже были созданы.
# app/Services/FilterQueryBuilder.php
...
private function createRelationTree(string $relationKeys, $operator, $value) {
$relations = explode('.', $relationKeys);
$lastKey = array_key_last($relations);
$field = $relations[$lastKey];
unset($relations[$lastKey]);
$result = [];
if(count($relations) > 0 ) {
$result[$relations[count($relations)-1]] = [
'field' => $field,
'operator' => $operator,
'value' => $value
];
for($i=count($relations)-2; $i>-1; $i--)
{
$result[$relations[$i]] = $result;
unset($result[$relations[$i+1]]);
}
} else {
$result = [
'field' => $field,
'operator' => $operator,
'value' => $value
];
}
return $result;
}
...
Этот код разделит все отношения и преобразует их во вложенный массив. Если у нас есть фильтр типа model1.model2.model3.id=100 , то результат будет примерно таким.
Array
(
[model1] => Array
(
[model2] => Array
(
[model3] => Array
(
[field] => id
[operator] => =
[value] => 100
)
)
)
)
Теперь давайте добавим эти методы в один метод и объединим все фильтры вместе.
# app/Services/FilterQueryBuilder.php
...
private function parseFilters($filters) {
if(empty($filters)) {
return [];
}
$result = [];
foreach($filters as $filter) {
if(empty($filter)) {
continue;
}
$operator = $this->getOperator($filter);
$value = $this->getFilterValue($filter, $operator);
$keys = $this->getFilterRelations($filter, $operator);
$relationTree = $this->createRelationTree($keys, $operator, $value);
array_push($result, $relationTree);
}
return $result;
}
...
Добавление фильтра в конструктор запросов
Теперь, когда у нас есть все фильтры в массиве, нам нужно добавить эти отношения в наш конструктор запросов.
# app/Services/FilterQueryBuilder.php
...
private function addFiltersToQuery($query, $filters) {
if(count($filters) === 3) {
switch($filters['operator']) {
case '()':
return $query->whereIn($filters['field'], explode(',', $filters['value']));
case '~':
return $query->where($filters['field'], 'LIKE', '%' . $filters['value'] . '%');
default:
return $query->where($filters['field'], $filters['operator'], $filters['value']);
}
}
$relation = array_key_first($filters);
return $query->whereHas($relation, function(Builder $query) use($relation, $filters) {
$this->addFiltersToQuery($query, $filters[$relation]);
});
}
...
Вот рекурсивный метод, который будет перебирать массив фильтров, если это отношение, то он будет использовать где есть , который говорит красноречивому конструктору объединять таблицы на основе определения модели.
Более того, когда он дойдет до конца массива, он добавит в него поле и значение с заданным оператором. Вы могли видеть, что мы также справились с пользовательскими операторами.
Вы также можете прочитать больше о Красноречивом и построителе запросов laravel .
Поставщик услуг
Поставщики услуг являются центральным местом загрузки всех приложений Laravel. Ваше собственное приложение, а также все основные сервисы Laravel загружаются через поставщиков услуг.
Вы могли бы прочитать больше об идее поставщика услуг здесь .
Поставщик услуг поможет вам внедрить ваш сервис с помощью контейнера служб laravel в ваше приложение. Он позаботится о создании экземпляра и передаст его вашим классам и методам.
Вам необходимо выполнить приведенную ниже команду, чтобы создать поставщика услуг под названием |/Поставщик услуг фильтрации .
$ php artisan make:provider FilterServiceProvider
Затем взгляните на файл ниже.
# app/Providers/FilterServiceProvider.php
Теперь давайте скажем, как мы создаем экземпляр из нашего сервиса FilterQueryBuilder внутри метода register.
# app/Providers/FilterServiceProvider.php
...
public function register()
{
$this->app->bind(\App\Services\FilterQueryBuilder::class, function () {
$request = app(\Illuminate\Http\Request::class);
return new FilterQueryBuilder($request);
});
}
...
Поэтому всякий раз, когда в приложении вы вводите класс Filter Query Builder , он передает объект \Illuminate\Http\Request в конструктор и предоставляет вам экземпляр.
После того, как вы добавили своего поставщика услуг, вам необходимо зарегистрировать его, чтобы laravel его понял. Теперь откройте config/app.php файл, а в разделе “Поставщики” также добавьте своего поставщика.
# config/app.php
...
'providers' => [
...
App\Providers\FilterServiceProvider::class,
...
]
...
Как использовать его сейчас?
Пока все идет хорошо. Теперь вы можете взглянуть на код API индекса статей в разделе app/Http/Controllers/Api/ArticleController.php
# app/Http/Controllers/Api/ArticleController.php
...
public function index(ArticleFilter $filter)
{
$articles = new Paginate(Article::loadRelations()->filter($filter));
return $this->respondWithPagination($articles);
}
...
Однако они создали класс Article Filter , но если вы посмотрите на код, он просто обработает некоторое заданное отношение. Мы собираемся заменить его нашим Построителем запросов фильтра , который будет принимать любой фильтр в нашей модели на основе определения модели. Поэтому в будущем нам просто нужно обновить нашу модель, и фильтры будут работать и на них.
# app/Http/Controllers/Api/ArticleController.php...
public function index(\App\Services\FilterQueryBuilder $filters)
{
$articles = $filters->buildQuery(Article::loadRelations());
$result = new Paginate($articles);return $this->respondWithPagination($result);
}
...
Будь Осторожен!
Вы должны знать, что это была всего лишь практика, и она не оптимизирована для крупномасштабных приложений, а также для решения многих других задач. Не стесняйтесь общаться и расширять его, если хотите.
Оригинал: “https://dev.to/azolf/dynamic-filters-with-laravel-eloquent-4opg”