
Спешно прикручивал к своей туристической CRMке нормальную админку. Раньше у меня была самописная панель с фронтом на Vue, и каждое новое действие или страница занимали кучу времени. В какой-то момент я честно признал очевидное: сколько бы ни хотелось поиграться с фронтом, для админки выгоднее взять готовое решение. В итоге переехал на Laravel Orchid, а экспериментировать оставил уже на пользовательской части сайта.
В целом переезд прошёл гладко, но всплыл один интересный кейс. В Orchid я вывожу модели в таблицы с сортировкой и фильтрами, и в одном месте у меня используются теги стран на базе spatie/laravel-tags. Названия хранятся не в обычном текстовом поле, а в json поле — так удобнее делать локализацию. Там лежит что-то вроде {"ru": "Россия", "en": "Russia"}, и именно с этим мне пришлось разбираться.
Фильтрация по названию сработала из коробки, а вот сортировка по JSON-полю начала вести себя странно. Ниже разберу, почему так происходит, что вообще из себя представляет JSON структура в базе и как я в итоге подружил фильтр и сортировку.
Зачем я вообще полез в JSON-поля в админке
Когда делаешь многоязычные проекты, быстро приходит идея хранить переводимые значения в одном месте. Вместо отдельной колонки под каждый язык проще завести одно json поле и сложить в него все варианты названий. Именно так работает библиотека тегов от Spatie: она сразу ожидает JSON-структуру с ключами-кодами языков.
У меня проект русскоязычный, но исторически вся система тегов уже завязана на эту JSON схему. Переделывать старые данные и код ради одного проекта точно не хотелось, поэтому проще было научиться правильно работать с этим форматом в Orchid.
Если смотреть шире, то JSON полезен не только для переводов. Это гибкий формат, когда заранее не до конца понятна структура сущности. Но гибкость оборачивается нюансами: ORM, сортировка, фильтрация и даже некоторые поисковые движки реагируют на json поле иначе, чем на обычный текст.
Коротко про JSON: формат, структура и типы данных
JSON сегодня встречается везде — от REST-API до настроек приложения. Для базы данных это способ хранить сложные сущности в одном столбце. Если обобщить, ответ на вопрос «JSON какой формат?» такой: это текстовое представление объекта или массива с понятной структуой ключ-значение.
Основные JSON типы:
- строка (
"Russia"); - число (
2025); - булево (
true/false); - null;
- объект (словарь ключ-значение);
- массив.
Именно комбинация объектов и массивов делает формат удобным для хранения структурированных данных. Но за эту гибкость мы платим тем, что сортировка и фильтрация должны учитывать путь до нужного элемента: одно дело обычное текстовое поле, другое — вложенный параметр JSON внутри объекта.
JSON структура и значение в JSON на реальном примере
Вернёмся к тегам. Структура одного названия страны в колонке БД выглядит так:
{
"ru": "Россия",
"en": "Russia"
}По сути, это объект, где ключи — коды языков. Значение в JSON, которое мне нужно для сортировки и фильтра, — это конкретный элемент по ключу ru. То есть в SQL-запросе, если говорить о MySQL или PostgreSQL, придётся обращаться к подполю: name->>'ru' или аналогичным синтаксисом.
Массив в JSON и вложенные параметры
JSON схема в реальных проектах быстро усложняется. Вместо одного объекта с языками вполне может быть массив в JSON с несколькими значениями, вложенные объекты и дополнительные метаданные. Для человека это читабельно, а вот для СУБД и ORM каждый такой уровень вложенности — отдельный шаг в запросе.
В Laravel это решается через JSON-операторы и выражения вида where('data->meta->color', 'blue'). Но сортировка по подобным путям требует аккуратности: стоит перепутать путь к полю — и таблица в админке перестанет реагировать на клики по заголовку.
Как Laravel и БД работают с JSON полем
Laravel умеет нормально работать с JSON-колонками, если база это поддерживает. В миграциях просто создаётся столбец json, а дальше можно использовать привычные Eloquent-методы: whereJsonContains, where с путями, сортировку через orderBy по выражению.
В Orchid таблицы собираются через TD::make(). Если указать колонку просто как TD::make('name'), то фреймворк честно пытается сортировать по исходному столбцу. Но БД видит там целую JSON структуру, а не конкретный текст. Естественно, она не понимает, по какому элементу сортировать.
Чтобы всё заработало, в запрос нужно подставить путь до нужного языка, а не полный объект. В моём случае это name->ru. Как только я добавляю эту часть — сортировка оживает, но одновременно ломается стандартный фильтр Orchid по колонке name.
Проблема: фильтр работает, сортировка по JSON полю — нет
Итак, у меня была таблица тегов по странам. В Orchid я объявил колонку «Название» примерно так (упрощённо):
TD::make('name', 'Название')
->sort()
->filter();Фильтр по фрагменту текста отработал сразу. Orchid под капотом строит запрос по name, и база сама ищет в JSON-значении. Но как только я попробовал кликнуть по заголовку колонки, стало понятно: сортировка по json полю не знает, что ей делать.
Если я меняю объявление на TD::make('name->ru', 'Название'), то сортировка начинает работать, потому что запрос идёт по конкретному пути. Но тогда ломается фильтрация: поля формы привязаны к исходному названию колонки, и Orchid ожидает обычное имя, а не выражение с ->ru.
Хотелось, чтобы и фильтрация, и сортировка по JSON полю жили вместе и не конфликтовали. В итоге я пришёл к двум решениям: сначала к «костылю» через наследование класса колонки, а потом к более аккуратному варианту с фильтром.
Решение №1: кастомный класс столбца и лёгкий костыль
Первое, что я сделал, — полез в код Orchid и посмотрел, как именно он обрабатывает сортировку. Там нашёл место, где формируется имя столбца для orderBy. Логика простая: если колонка помечена как сортируемая, то в запрос подставляется её имя.
Я написал свой наследный класс от стандартного TD, где в момент построения сортировки к имени колонки принудительно добавляется суффикс ->ru. То есть для ORM колонка продолжает называться name, а вот конкретно в сортировке используется путь name->ru.
Дополнительно я передал в Blade-шаблон отдельную переменную sort_column, чтобы заголовок таблицы знал, по какому именно пути сейчас сортируются данные и какую иконку подсветить.
В итоге в layout для таблицы я использовал не стандартный TD, а свой новый класс. Это позволило оставить фильтр как есть, а сортировку заставить работать по русскому варианту названия. Работает, но выглядит именно как костыль: мы жёстко прошиваем путь к языку прямо в логику столбца.
С точки зрения долговечности кода это не лучший вариант. Если проект станет многоязычным или придётся работать с другой JSON схемой, придётся вспоминать, где именно было захардкожено ->ru.
Решение №2: фильтр Eloquent для сортировки по JSON полю
Дальше я задал вопрос в канале Orchid, и мне подсказали более элегантный подход — использовать Eloquent Filters. Фильтр — это класс, который умеет модифицировать запрос: обычно им пользуются для фильтрации, но никто не запрещает задействовать его и для сортировки.
Я написал отдельный фильтр, который:
- читает из запроса параметр сортировки для колонки
name; - подменяет его на путь
name->ruвorderBy; - при этом не ломает обычную фильтрацию по полю
name.
Пример очень упрощённого фильтра может выглядеть так:
class NameRuSortFilter extends Filter
{
public function run(Builder $builder): Builder
{
$direction = $this->request->get('sort', 'asc');
return $builder->orderBy('name->ru', $direction);
}
public function display(): array
{
return [];
}
}Дальше в методе query() экрана списка тегов я просто добавил этот фильтр:
public function query(): array
{
return [
'tags' => Tag::filters([
NameRuSortFilter::class,
])->paginate(),
];
}Объявление колонки в таблице при этом почти не меняется:
TD::make('name', 'Название')
->sort()
->filter();Теперь сортировка по заголовку колонки отрабатывает так, как нужно: фильтр перехватывает направление сортировки и аккуратно перенаправляет его на name->ru. Фильтрация по фрагменту текста продолжает работать по исходному имени поля, как и ожидалось.
Плюс этого решения в том, что логика сортировки вынесена в отдельный класс. Хочется другой язык — подставляем вместо ru нужный код. Поменяется JSON структура — меняем фильтр, а не все таблицы по проекту.
Когда что использовать и как не сломать админку через год
Костыль с кастомным классом столбца хорош, когда нужно быстро завести сортировку по одному конкретному пути в JSON. Но на дистанции гораздо лучше завести отдельный фильтр. Так проще поддерживать код, переиспользовать решения и не ловить неожиданные побочные эффекты.
Ещё один момент: чем сложнее JSON схема, тем больше соблазн свалить всё в одно поле. Иногда лучше вернуться к классической нормализованной структуре БД: вынести часто используемые элементы в отдельные колонки, а в JSON оставить только то, что действительно редко участвует в фильтрации и сортировке.
Если смотришь на проект не только как разработчик, но и как человек, который отвечает за трафик и видимость, то грамотная структура данных помогает и поиску. Подробно это разбирается в материалах про алгоритмы и основы оптимизации, например, в статье про алгоритмы поисковых систем.
С точки зрения практики полезно совмещать такие технические приёмы с базовыми знаниями о бэкенде и интерфейсах. Если хочется системно подтянуть фундамент, удобно начать с материалов по веб-разработке для начинающих. Там как раз есть базовые вещи, которые помогают проще относиться к JSON-колонкам и сложным запросам.
А если нужно аккуратно фильтровать и парсить сложные значения (например, при поиске по нескольким параметрам json сразу), часто пригодятся регулярные выражения и практики из мира SEO и AI. Хороший пример — заметка про regex в SEO и AI, где похожие идеи используются для обработки текстов и логов.
Итоги: JSON поле в Orchid — не страшно, если приручить
В итоге задача свелась к тому, чтобы научить Laravel и Orchid работать не с целым объектом, а с конкретным подполем внутри JSON. Проблема была не в том, что «JSON плохой», а в том, что сортировка и фильтрация по одному и тому же полю ожидали разную структуру.
Коротко по итогам:
- в колонках с локализованными названиями удобно использовать json поле с объектом вида
{"ru": "...", "en": "..."}; - фильтрация в Orchid по таким колонкам обычно работает сразу, но сортировка требует явного указания пути, например
name->ru; - можно пойти быстрым путём и дописать суффикс в кастомном классе столбца;
- более гибкий вариант — вынести логику в отдельный Eloquent-фильтр и внутри него управлять значением в JSON и направлением сортировки;
- по мере роста проекта стоит периодически пересматривать JSON структуру и решать, что оставить в одном поле, а что вынести в отдельные колонки.
JSON как формат данных сам по себе довольно простой: важно помнить, какие есть JSON типы и как база к ним относится. Как только становится ясно, по какому именно пути в объекте нужно сортировать, все проблемы с таблицами в Laravel Orchid сдуваются до пары строк кода.