Спешно прикручиваю к своей туристической CRMке нормальную админку (которая для админов). Раньше была самописная с фронтом на Vue, из-за этого добавление любой новой страницы или функции требовало слишком много времени. Собрался с мыслями, решил наконец заменить её на Laravel Orchid. Дело пошло значительно быстрее, и в целом могу констатировать очевидное: как бы ни было прикольно играться с фронтом на Vue, какая бы небольшая ни была админка — лучше конечно сразу брать готовую панельку. А с фронтом играться в пользовательской части.
В процессе переноса столкнулся с неожиданной трудностью. В Orchid есть вывод моделей в таблицы, который довольно легко и удобно собирается, при этом столбцы таблицы можно сделать фильтруемыми и сортируемыми — тоже легко. Вот, например, объявили колонку «Название» с сортировкой и фильтром (по фрагменту текста):
TD::make('name', 'Название')
->sort()
->filter(Input::make()),
И всё отлично, но в одной из таких таблиц у меня выводятся теги (а именно — страны). Теги сделаны на библиотеке spatie/laravel-tags, и в них названия (name) — это поля json, так сделано для поддержки локализации. То есть внутри поля названия будет {"ru": "Россия", "en": "Russia"} и т.д.
У меня язык только русский, но эта структура плотно и давно используется, отказываться от неё сейчас не хочется. Но, разумеется, без проблем с ней не обошлось. Дело в том, что фильтр для поиска тегов по названию работает сразу, из коробки (что логично). А вот с сортировкой получается ерунда, без подстановки ->ru она не работает. Если же использовать TD::name('name->ru'...) (и отрендерить значение поля вручную) — получается наоборот. Сортировка работает, а фильтр нет.
Но мы-то хотим, чтобы работало всё. Далее два решения: костыль и более элегантный вариант.
Костыль
Первым делом поковырялся в коде класса, который размечает колонки таблицы, и нашел код сортировки. Написал свой наследный класс, где в сортировку принудительно добавляется ->ru, а также передал в шаблон построения заголовка отдельную переменную sort_column — чтобы шаблон знал, когда надо рисовать иконку сортировки по данному столбцу.
<?php
namespace App\Orchid\Screen;
use Illuminate\View\View;
use Orchid\Screen\TD;
class JSONSortedTD extends TD
{
public function buildSortUrl(): string
{
$query = request()->collect()->put(
'sort',
revert_sort($this->column . '->ru') // <- добавили суффикс
)->toArray();
return url()->current() . '?' . http_build_query($query);
}
public function buildTh()
{
return view('platform::partials.layouts.th', [
'width' => is_numeric($this->width) ? $this->width . 'px' : $this->width,
'align' => $this->align,
'sort' => $this->sort,
'sortUrl' => $this->buildSortUrl(),
'column' => $this->column,
'sort_column' => $this->column . '->ru', // <- добавили суффикс
'title' => $this->title,
'filter' => $this->buildFilter(),
'filterString' => $this->buildFilterString(),
'slug' => $this->sluggable(),
'popover' => $this->popover,
]);
}
}
Переопределил шаблон Orchid platform/partials/layouts/th.blade.php, чтобы использовать новую переменную с суффиксом, если она задана.
@if(is_sort($sort_column ?? $column))
@php $sortIcon = get_sort($sort_column ?? $column) === 'desc' ? 'bs.sort-down' : 'bs.sort-up' @endphp
<x-orchid-icon :path="$sortIcon"/>
@endif
И вместо TD в табличном лейауте указал наш новый класс:
JSONSortedTD::make('name', 'Название')
->sort()
->filter(Input::make()),
Такое себе решение, но работает.
Решение с помощью фильтра
Поспрашивал в канале Orchid, присоветовали вместо костыля выше написать фильтр (см. Eloquent Filters). Попробовал, и действительно получилось гораздо лучше.
Казалось бы, фильтр — для фильтрации данных, но его можно примонстрячить и для обработки сортировки. Вот класс фильтра, который у меня получился:
<?php
namespace App\Orchid\Filters;
use Illuminate\Database\Eloquent\Builder;
use Orchid\Filters\Filter;
use Orchid\Screen\Field;
class JsonSortFilter extends Filter
{
public function name(): string
{
return '';
}
public function parameters(): ?array
{
// реагируем на sort в query
return ['sort'];
}
public function run(Builder $builder): Builder
{
$sort = $this->request->get('sort');
// если в sort — наш параметр, используем его для сортировки запроса
if ($sort == 'name') {
return $builder->orderBy('name->ru', 'asc');
} else if ($sort == '-name') {
return $builder->orderBy('name->ru', 'desc');
}
// а если что-то другое, то ничего не делаем
return $builder;
}
public function display(): iterable
{
// никаких визуальных полей нашему фильтру не нужно
return [];
}
}
И теперь в query() экрана со списком тегов можно добавить новый фильтр:
$tags = DirectionTag::filters([JsonSortFilter::class])->defaultSort('name->ru')->paginate(40);
Прописывать в TD::... дополнительно ничего не надо. Уже работает. Но вызов ->sort() в объявлении колонки надо оставить, чтобы сортировка происходила по клику на название колонки, и чтобы иконка сортировки появлялась.
Результат с включенным фильтром и сортировкой:

P.S. У меня везде добавляется явно ->ru, поскольку язык только русский. Но параметризовать это, конечно же, не проблема.

