Пакет · mui-data-grid

Ментальная модель

Одна мысль объясняет всё остальное: библиотека не строит ваш запрос, она на него накладывается. Запрос остаётся вашим — со всеми JOIN, подзапросами и бизнес-логикой, — а грид добавляет к нему ровно три вещи: условия фильтров, порядок и границы страницы.

Идея: оверлей, а не генератор

Большинство «таблиц с сервера» устроены как генераторы: вы описываете модель, а библиотека сама решает, какой SQL получится. Пока задача типовая, это удобно; как только нужен хитрый JOIN, вычисляемая колонка или CTE — вы упираетесь в границы генератора и начинаете воевать с ним.

Здесь наоборот. Вы отдаёте готовый репозиторий — с SELECT, JOINами и базовым WHERE, — и библиотека дописывает поверх:

text
ваш репозиторий                    что добавляет MuiGrid::wrap()
─────────────────────              ────────────────────────────
SELECT a.id, a.title, au.name
FROM articles a
LEFT JOIN authors au ON …
WHERE a.is_published = true
                                 AND (a.title ILIKE :v0)      ← фильтры из схемы
                                 ORDER BY a.views DESC        ← сортировка из схемы
                                 LIMIT 25 OFFSET 50           ← страница
                                 + отдельный COUNT(*)         ← общее число строк

Три следствия, которые стоит держать в голове:

  • Всё, что выразимо в SQL, работает. Рекурсивные CTE, оконные функции, EXISTS, вычисляемые выражения — библиотека их не видит и не может им помешать.
  • Базовый WHERE неприкосновенен. Фильтры пользователя добавляются через andWhere(), то есть сужают ваш набор, а не заменяют его. Строку, которую вы отсекли бизнес-условием, через грид достать нельзя.
  • ORDER BY перезаписывается. Сортировка — это то, чем управляет пользователь, поэтому порядок ставится, а не дописывается. Если порядок важен для вашей логики, он должен быть в defaultOrder() схемы, а не в базовом запросе.

Следствие: схема — это дверь

Раз запрос ваш, встаёт вопрос: что из присланного браузером вообще имеет право попасть в этот запрос? Ответ — только то, что перечислено в схеме.

php
GridColumn::for('authorName', 'au.name')->filterable(FilterType::String)->sortable();
//               ↑ имя из браузера   ↑ SQL из вашего кода

Слева — строка, пришедшая от пользователя. Справа — выражение, написанное вами. Схема переводит одно в другое, и в SQL уходит только правая часть. Имя поля из запроса в текст запроса не попадает никогда.

Отсюда — граница доверия, которую полезно проговорить явно:

Источник Куда попадает Как обрабатывается
field из запроса никуда используется как ключ поиска в схеме
operator из запроса выбор ветки приводится к enum, неизвестный — ошибка
value из запроса в SQL всегда связанным параметром
SQL-выражение колонки в SQL подставляется как есть — это ваш код
выражение сортировки в SQL подставляется как есть — это ваш код

Практический вывод один: никогда не собирайте SQL-выражение колонки из данных запроса. Всё остальное библиотека сделает безопасно сама.

Следствие: белый список, а не чёрный

Свежесозданная колонка не умеет ничего — ни фильтроваться, ни сортироваться. Возможности включаются явно:

php
GridColumn::for('body', 'a.body');                            // объявлена, но бесполезна
GridColumn::for('body', 'a.body')->filterable(FilterType::String);  // можно фильтровать
GridColumn::for('body', 'a.body')->sortable();                      // можно сортировать

Это не мелочь стиля. При чёрном списке («запрещаем опасное») забытая колонка становится дырой; при белом — забытая колонка просто не работает, и вы узнаёте об этом от тестировщика, а не от злоумышленника.

Следствие: фильтр отклоняется, сортировка игнорируется

Реакция на неизвестное поле у этих двух операций разная — и это сделано намеренно.

Ситуация Фильтрация Сортировка
поле объявлено и включено применяется применяется
поле объявлено, но операция не включена 400 молча пропускается
поля нет в схеме 400 молча пропускается
оператор не разрешён для типа 400

Причина в том, кто эти запросы порождает. Фильтр пользователь ставит осознанно: если он не сработал, показать пустую таблицу вместо ошибки — значит соврать. А состояние сортировки таблица шлёт сама, в том числе для скрытых и переставленных колонок, — падать на этом было бы враждебно к интерфейсу. Когда пригодной сортировки не осталось, применяется defaultOrder(). Подробнее — в Белом списке и безопасности.

Следствие: два вида фильтров

Не всякий фильтр — колонка таблицы. Полезно с самого начала разделять:

  • Фильтры грида — то, что пользователь набирает в интерфейсе таблицы. Приходят в filterModel, проверяются схемой, накладываются библиотекой.
  • Доменные фильтры — область видимости запроса: «только мои», «в этой категории», строка глобального поиска. Это ваши поля в запросе, применяются к базовому запросу до вызова wrap(), в схеме не участвуют.
php
if ($request->authorId) {
  $repo->andWhere(Qb::eq('a.author_id', $request->authorId));   // доменный фильтр
}

return MuiGrid::wrap($repo, $request, $schema);                    // фильтры грида

Рецепты для второго вида — в Фильтрах вне грида.

Когда это подходит, а когда нет

Подходит, если набор данных больше, чем разумно отдавать в браузер, а пользователю нужны привычные пагинация, сортировка и фильтры по колонкам — то есть практически любой административный список.

Не подходит, если строк заведомо немного (сотни): тогда проще отдать всё разом и дать таблице фильтровать на клиенте — это дешевле и по коду, и по числу запросов. Также не подходит, если пагинация нужна бесконечной лентой: там уместнее курсор, а не OFFSET с COUNT.

Что дальше