Свои фильтры
По умолчанию каждый оператор превращается в SQL одинаково для всех колонок: contains —
это совпадение по шаблону, >= — сравнение, isEmpty — проверка на NULL. Когда
конкретной колонке нужен другой SQL, filterUsing() перехватывает нужные операторы,
оставляя остальные нетронутыми.
Как это работает
Резолвер получает элемент фильтра и возвращает условие — либо null, что значит
«обрабатывай как обычно».
GridColumn::for('authorName', 'au.name')
->filterable(FilterType::String)
->filterUsing(fn (MGFilterItem $i): ?Qb => match ($i->operator) {
MGOperator::IS_EMPTY => Qb::isNull('a.author_id'),
default => null,
});Порядок обработки одного элемента фильтра фиксирован:
1. поле ищется в схеме → нет → 400
2. оператор сверяется с FilterType → не разрешён → 400
3. вызывается ваш резолвер → вернул Qb → используется он
4. вернул null → стандартное сопоставление оператораИз этого следует важное: шлюз по типу срабатывает раньше резолвера. Оператор, не
входящий в набор FilterType колонки, до вашего кода не дойдёт — запрос будет отклонён.
Если вы хотите обработать > на строковой колонке, объявляйте её типом, который этот
оператор допускает. Таблицы наборов — в Операторах.
Частичное переопределение
Самый частый случай — переопределить один-два оператора. Ветка default => null
возвращает всё остальное к стандартному поведению:
use Flytachi\Winter\Cdo\Qb;
use Flytachi\Winter\MuiDataGrid\Entity\MGFilterItem;
use Flytachi\Winter\MuiDataGrid\Entity\MGOperator;
GridColumn::for('authorName', 'au.name')
->filterable(FilterType::String)
->sortable()
->filterUsing(fn (MGFilterItem $i): ?Qb => match ($i->operator) {
// «без автора» — это NULL внешнего ключа, а не NULL присоединённого имени
MGOperator::IS_EMPTY => Qb::isNull('a.author_id'),
MGOperator::IS_NOT_EMPTY => Qb::isNotNull('a.author_id'),
default => null,
});Пишите только исключения. Резолвер, который перечисляет все двадцать три оператора, —
верный признак того, что колонке нужен другой FilterType или другое выражение, а не
переопределение.
Логическое поле с тремя состояниями
Классический случай, где стандартного сопоставления не хватает: в базе колонка
is_archived может быть true, false или NULL, а пользователь ожидает, что «нет»
включает и то, и другое.
GridColumn::for('archived', 'a.is_archived')
->filterable(FilterType::Boolean)
->sortable()
->filterUsing(function (MGFilterItem $i): ?Qb {
if ($i->operator !== MGOperator::IS && $i->operator !== MGOperator::EQUALS) {
return null;
}
return $i->value
? Qb::eq('a.is_archived', true)
: Qb::clip(Qb::or(
Qb::eq('a.is_archived', false),
Qb::isNull('a.is_archived'),
));
});Qb::clip() оборачивает условие в скобки — без него OR внутри общего AND изменил бы
смысл всего фильтра.
Поиск с нормализацией
Телефон в базе хранится с разделителями, пользователь вводит цифры подряд. Приведём обе стороны к одному виду:
GridColumn::for('phone', 'c.phone')
->filterable(FilterType::String)
->sortable()
->filterUsing(function (MGFilterItem $i, TextMatch $m): ?Qb {
if ($i->operator !== MGOperator::CONTAINS) {
return null;
}
$digits = preg_replace('/\D+/', '', (string) $i->value);
return $m->match("regexp_replace(c.phone, '\\D', '', 'g')", "%$digits%");
});Здесь видно второй аргумент резолвера — режим совпадения
TextMatch, уже разрешённый под драйвер вашей
базы. Метод match() пишет ILIKE, LIKE или lower() — то же, что использовало бы
стандартное поведение, только по вашему выражению.
Второй параметр объявлять необязательно
Резолверы, написанные с одним параметром (fn (MGFilterItem $i) => …), продолжают работать:
лишний аргумент PHP просто игнорирует. Объявляйте TextMatch, только когда он вам нужен.
Повторное использование стандартного поведения
Иногда нужно не изменить логику оператора, а применить её к другому выражению. Для этого у оператора есть собственный метод, тот самый, которым пользуется библиотека:
GridColumn::for('name', 'c.name')
->filterable(FilterType::String)
->filterUsing(fn (MGFilterItem $i, TextMatch $m): ?Qb => $i->operator === MGOperator::CONTAINS
? $i->operator->toQb("coalesce(c.nick, c.name)", $i->value, $m)
: null);Так вы не переписываете сопоставление руками и не рискуете разойтись с ним при обновлении библиотеки.
Значение фильтра
Резолвер получает значение таким, каким его прислал браузер, — без приведения типов.
| Поле | Тип | Что содержит |
|---|---|---|
$item->field |
string |
имя поля из запроса (уже сопоставленное со схемой) |
$item->operator |
MGOperator |
оператор в виде enum |
$item->value |
mixed |
значение как пришло: строка, число, null, массив |
null приходит для операторов isEmpty и isNotEmpty — значение им не нужно. Массив
приходит для isAnyOf. Для остальных операторов интерфейс шлёт скаляр, но полагаться на
это не стоит: приводите тип сами, если он важен.
->filterUsing(function (MGFilterItem $i): ?Qb {
if ($i->operator !== MGOperator::IS_ANY_OF) {
return null;
}
$ids = array_map('intval', (array) $i->value);
return $ids === [] ? Qb::empty() : Qb::in('a.status_id', $ids);
})Qb::empty() — пустое условие: оно не добавит в запрос ничего. Возвращать его корректно,
когда фильтр оказался бессмысленным (пустой набор), но отклонять запрос не за что.
Отказ из резолвера
Если значение не годится, бросьте MuiGridException — роутер ответит 400 с вашим
текстом:
use Flytachi\Winter\MuiDataGrid\MuiGridException;
->filterUsing(function (MGFilterItem $i): ?Qb {
if ($i->operator === MGOperator::EQUALS && !is_numeric($i->value)) {
throw new MuiGridException("Поле 'code' принимает только числовое значение");
}
return null;
})Что дальше
- Операторы — полный список и наборы по типам.
- Режимы совпадения — что такое
TextMatch. - Фильтры вне грида — когда фильтр вообще не должен быть колонкой.