Сортировка по JSON-полю в таблицах Laravel Orchid

Автор: | 22 ноября, 2025 | 10
json поле

Спешно прикручивал к своей туристической 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 сдуваются до пары строк кода.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *