Студия разработки сайтов и приложений

Netspark.ru

Платформа для ботов в Telegram

Ботопотамы

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

Спешно прикручиваю к своей туристической 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() в объявлении колонки надо оставить, чтобы сортировка происходила по клику на название колонки, и чтобы иконка сортировки появлялась.

Результат с включенным фильтром и сортировкой:

json_filtered.png

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

Обсуждение

Чтобы обсудить заметку, написать комментарий, или просто связаться, заходите в Телеграм-канал. У нас весело и всем рады!

Также меня можно найти в Хвиттере, VC.ru, Дзене, или Тенчате.