---
title: 'Sorting by a JSON field in Laravel Orchid tables'
url: 'https://graker.ru/en/news/2025/11/22/laravel-orchid-sort-table-by-json-field'
language: en
type: post
author: 'Roman Grachev'
published: '2025-11-22T01:30:00+03:00'
updated: '2026-11-07T18:54:35+03:00'
categories:
  - Programming
  - Laravel
translations:
  ru: 'https://graker.ru/news/2025/11/22/laravel-orchid-sort-table-by-json-field.md'
---

# Sorting by a JSON field in Laravel Orchid tables

I'm hastily bolting a proper admin panel (the one for admins) onto my travel CRM. Previously it was a custom-built one with a Vue frontend, and because of that, adding any new page or feature took way too much time. I gathered my thoughts and finally decided to replace it with [Laravel Orchid](https://orchid.software/). Things went much faster, and overall I can state the obvious: no matter how fun it is to play with a Vue frontend, no matter how small the admin panel is — it's of course better to take a ready-made panel right away. And play with the frontend in the user-facing part.

During the migration I ran into an unexpected difficulty. Orchid can output models in tables, which are quite easy and convenient to put together, and the table columns can be made filterable and sortable — also easily. Here, for example, we've declared a "Name" column with sorting and a filter (by text fragment):

```php
    TD::make('name', 'Title')
        ->sort()
        ->filter(Input::make()),
```

And everything's great, but one of these tables lists tags (countries, to be exact). The tags are built with the [spatie/laravel-tags](https://github.com/spatie/laravel-tags) library, and their names are JSON fields, which is done to support localization. That is, the name field will contain `{"ru": "Россия", "en": "Russia"}` and so on.

I only have Russian, but this structure has been used heavily for a long time, and I don't want to abandon it now. But, of course, it didn't come without problems. The thing is, the filter for searching tags by name works right away, out of the box (which makes sense). But sorting is a mess: without adding `->ru` it doesn't work. And if you use `TD::name('name->ru'...)` (and render the field value manually), it's the other way around. Sorting works, but the filter doesn't.

But we want everything to work. Below are two solutions: a hack and a more elegant option.

## The hack

First, I dug into the code of the class that defines table columns and found the sorting code. I wrote my own subclass where `->ru` is forcibly added to sorting, and also passed a separate `sort_column` variable to the header template — so the template knows when to draw the sort icon for this column.

```php
<?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') // <- suffix added
        )->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', // <- suffix added
            'title'        => $this->title,
            'filter'       => $this->buildFilter(),
            'filterString' => $this->buildFilterString(),
            'slug'         => $this->sluggable(),
            'popover'      => $this->popover,
        ]);
    }

}
```

I overrode the Orchid template `platform/partials/layouts/th.blade.php` to use the new suffixed variable if it's set.

```markup
@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
```

And instead of TD in the table layout I specified our new class:

```php
JSONSortedTD::make('name', 'Title')
    ->sort()
    ->filter(Input::make()),
```

Not much of a solution, but it works.

## Solution using a filter

I asked around in the [Orchid channel](https://t.me/orchid_russian_community) (in Russian), and they advised writing a filter instead of the hack above (see [Eloquent Filters](https://orchid.software/en/docs/filters/)). I tried it, and it really turned out much better.

You'd think a filter is for filtering data, but it can be rigged up to handle sorting too. Here's the filter class I ended up with:

```php
<?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
    {
        // react to sort part of the query
        return ['sort'];
    }

    public function run(Builder $builder): Builder
    {
        $sort = $this->request->get('sort');
        // if we have our parameter in sort — use it to sort the query
        if ($sort == 'name') {
            return $builder->orderBy('name->ru', 'asc');
        } else if ($sort == '-name') {
            return $builder->orderBy('name->ru', 'desc');
        }
        // if it's something else, do nothing
        return $builder;
    }

    public function display(): iterable
    {
        // our filter doesn't need visual fields
        return [];
    }
}
```

And now the new filter can be added to the `query()` of the tag list screen:

```php
    $tags = DirectionTag::filters([JsonSortFilter::class])->defaultSort('name->ru')->paginate(40);
```

Nothing else needs to be added to `TD::...`. It already works. But the `->sort()` call in the column declaration must stay, so that sorting happens when you click the column name, and so that the sort icon appears.

The result with the filter and sorting enabled:

![json_filtered.png](https://graker.ru/sites/default/files/styles/max_1300x1300/public/uploads/2025-11/json_filtered.png.webp?itok=D9mnUIkm)

P.S. I explicitly add `->ru` everywhere, since there's only Russian. But parameterizing it is, of course, no problem.
