Главная/Статьи/Фильтры и сортировка в Orchid

Фильтры и сортировка в Orchid

Добавляем фильтрацию в таблицы Orchid: текстовые фильтры, select, даты, кастомные фильтры. HTTP-фильтры и Selection-фильтры.

ДМ
Дмитрий Мещеряков
📅 19 июня 2026 г.📖 6 мин чтения

Фильтры в Orchid устроены удобно: добавил трейт в модель, перечислил разрешённые поля — и таблица фильтруется параметрами из адресной строки. Ровно из-за этого удобства здесь есть две вещи, о которых нужно знать: allowedFilters — это не список для галочки, а граница безопасности, а фильтр по подстроке на большой таблице работает так же плохо, как везде.

Типы фильтров в Orchid

Orchid предоставляет два подхода к фильтрации:

  1. Встроенные фильтры TD — прямо в колонках таблицы
  2. HTTP Filters — отдельные классы фильтров
  3. Selection — панель фильтров над таблицей
💡 Совет

Когда что использовать:

  • TD-фильтры — быстрая фильтрация по одному полю
  • HTTP Filters — сложная логика, переиспользование
  • Selection — визуальная панель с несколькими фильтрами

TD-фильтры в таблицах

Текстовый фильтр

php
use Orchid\Screen\TD;
Layout::table('users', [
    TD::make('name', 'Имя')
        ->filter(Input::make())  // Простой текстовый фильтр
        ->sort(),
    TD::make('email', 'Email')
        ->filter()  // Короткая запись — текстовый фильтр
        ->sort(),
]);

Select-фильтр

php
TD::make('status', 'Статус')
    ->filter(
        Select::make()
            ->options([
                '' => 'Все',
                'active' => 'Активные',
                'blocked' => 'Заблокированные',
            ])
    )
    ->render(fn($user) => $user->status === 'active' 
        ? '<span class="badge bg-success">Активен</span>'
        : '<span class="badge bg-danger">Заблокирован</span>'
    ),

DateRange-фильтр

php
use Orchid\Screen\Fields\DateRange;
TD::make('created_at', 'Дата регистрации')
    ->filter(DateRange::make())
    ->sort()
    ->render(fn($user) => $user->created_at->format('d.m.Y')),
⚠️ Важно

allowedFilters и allowedSorts — белые списки, и это их основная функция. Условия фильтрации приходят из адресной строки, то есть от пользователя. Перечисляя поля, вы определяете, по чему вообще можно фильтровать и сортировать таблицу.

Отсюда практическое следствие: не добавляйте туда поля, которых оператор не должен видеть даже косвенно. Возможность отфильтровать список пользователей по служебному полю или отсортировать по нему позволяет извлечь информацию о значениях, даже если сама колонка в таблице не выводится. Перечисляйте ровно те поля, по которым фильтрация нужна.

Модель с Filterable

php
// app/Models/User.php
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Orchid\Filters\Filterable;
use Orchid\Screen\AsSource;
class User extends Authenticatable
{
    use Filterable, AsSource;
    /**
     * Разрешённые поля для сортировки
     */
    protected $allowedSorts = [
        'id',
        'name',
        'email',
        'created_at',
    ];
    /**
     * Разрешённые поля для фильтрации
     */
    protected $allowedFilters = [
        'name',
        'email',
        'status',
        'created_at',
    ];
}

HTTP Filters

Создание фильтра

bash
php artisan orchid:filter StatusFilter
php
// app/Orchid/Filters/StatusFilter.php
namespace App\Orchid\Filters;
use Orchid\Filters\Filter;
use Illuminate\Database\Eloquent\Builder;
use Orchid\Screen\Fields\Select;
class StatusFilter extends Filter
{
    /**
     * Название фильтра для URL
     */
    public function name(): string
    {
        return 'status';
    }
    /**
     * Применение фильтра
     */
    public function run(Builder $builder): Builder
    {
        return $builder->where('status', $this->request->get('status'));
    }
    /**
     * Отображение фильтра
     */
    public function display(): iterable
    {
        return [
            Select::make('status')
                ->options([
                    'active' => 'Активные',
                    'blocked' => 'Заблокированные',
                    'pending' => 'Ожидающие',
                ])
                ->empty('Все статусы')
                ->value($this->request->get('status'))
                ->title('Статус'),
        ];
    }
}

Фильтр по дате

php
// app/Orchid/Filters/DateRangeFilter.php
namespace App\Orchid\Filters;
use Orchid\Filters\Filter;
use Illuminate\Database\Eloquent\Builder;
use Orchid\Screen\Fields\DateRange;
class DateRangeFilter extends Filter
{
    public function name(): string
    {
        return 'created_at';
    }
    public function run(Builder $builder): Builder
    {
        $value = $this->request->get('created_at');
        if (isset($value['start'])) {
            $builder->whereDate('created_at', '>=', $value['start']);
        }
        if (isset($value['end'])) {
            $builder->whereDate('created_at', '<=', $value['end']);
        }
        return $builder;
    }
    public function display(): iterable
    {
        return [
            DateRange::make('created_at')
                ->title('Дата регистрации')
                ->value($this->request->get('created_at')),
        ];
    }
}

Фильтр по связи

php
// app/Orchid/Filters/RoleFilter.php
namespace App\Orchid\Filters;
use App\Models\Role;
use Orchid\Filters\Filter;
use Illuminate\Database\Eloquent\Builder;
use Orchid\Screen\Fields\Relation;
class RoleFilter extends Filter
{
    public function name(): string
    {
        return 'role';
    }
    public function run(Builder $builder): Builder
    {
        $roleId = $this->request->get('role');
        return $builder->whereHas('roles', function ($query) use ($roleId) {
            $query->where('id', $roleId);
        });
    }
    public function display(): iterable
    {
        return [
            Relation::make('role')
                ->fromModel(Role::class, 'name')
                ->empty('Все роли')
                ->value($this->request->get('role'))
                ->title('Роль'),
        ];
    }
}

Selection — панель фильтров

Создание Selection

php
// app/Orchid/Layouts/UserFiltersLayout.php
namespace App\Orchid\Layouts;
use App\Orchid\Filters\StatusFilter;
use App\Orchid\Filters\RoleFilter;
use App\Orchid\Filters\DateRangeFilter;
use Orchid\Filters\Filter;
use Orchid\Screen\Layouts\Selection;
class UserFiltersLayout extends Selection
{
    /**
     * Список фильтров
     */
    public function filters(): iterable
    {
        return [
            StatusFilter::class,
            RoleFilter::class,
            DateRangeFilter::class,
        ];
    }
}

Использование в Screen

php
// app/Orchid/Screens/UserListScreen.php
use App\Orchid\Layouts\UserFiltersLayout;
use App\Orchid\Filters\StatusFilter;
use App\Orchid\Filters\RoleFilter;
class UserListScreen extends Screen
{
    public function query(): iterable
    {
        return [
            'users' => User::filters([
                    StatusFilter::class,
                    RoleFilter::class,
                ])
                ->defaultSort('created_at', 'desc')
                ->paginate(),
        ];
    }
    public function layout(): iterable
    {
        return [
            // Панель фильтров
            UserFiltersLayout::class,
            // Таблица
            Layout::table('users', [
                TD::make('id', 'ID')->sort(),
                TD::make('name', 'Имя')->sort()->filter(),
                TD::make('email', 'Email')->sort(),
                TD::make('status', 'Статус'),
                TD::make('created_at', 'Регистрация')->sort(),
            ]),
        ];
    }
}

Сложный фильтр с логикой

php
// app/Orchid/Filters/ActivityFilter.php
namespace App\Orchid\Filters;
use Orchid\Filters\Filter;
use Illuminate\Database\Eloquent\Builder;
use Orchid\Screen\Fields\Select;
class ActivityFilter extends Filter
{
    public function name(): string
    {
        return 'activity';
    }
    public function run(Builder $builder): Builder
    {
        $activity = $this->request->get('activity');
        return match ($activity) {
            'active_today' => $builder->whereDate('last_activity_at', today()),
            'active_week' => $builder->where('last_activity_at', '>=', now()->subWeek()),
            'active_month' => $builder->where('last_activity_at', '>=', now()->subMonth()),
            'inactive' => $builder->where('last_activity_at', '<', now()->subMonth()),
            'never' => $builder->whereNull('last_activity_at'),
            default => $builder,
        };
    }
    public function display(): iterable
    {
        return [
            Select::make('activity')
                ->options([
                    'active_today' => 'Активны сегодня',
                    'active_week' => 'Активны за неделю',
                    'active_month' => 'Активны за месяц',
                    'inactive' => 'Неактивны > месяца',
                    'never' => 'Никогда не заходили',
                ])
                ->empty('Любая активность')
                ->value($this->request->get('activity'))
                ->title('Активность'),
        ];
    }
}

Сортировка по умолчанию

php
public function query(): iterable
{
    return [
        'orders' => Order::filters()
            ->defaultSort('created_at', 'desc')  // По умолчанию
            ->paginate(),
    ];
}

Сортировка по связанному полю

php
// В модели
protected $allowedSorts = [
    'id',
    'total',
    'created_at',
    'user.name',  // Связанное поле
];
// В Screen
TD::make('user.name', 'Клиент')
    ->sort()
    ->render(fn($order) => $order->user->name),
⚠️ Важно

Производительность: При фильтрации по связям используйте whereHas с осторожностью — на больших таблицах это медленно. Рассмотрите денормализацию или индексы.

Сохранение фильтров в URL

Orchid автоматически сохраняет состояние фильтров в URL:

text
/admin/users?filter[status]=active&filter[created_at][start]=2026-01-01&sort=name

Это позволяет:

  • Делиться ссылками с фильтрами
  • Сохранять в закладки
  • Работает с кнопкой "Назад"

Итоги

СпособКогда использовать
TD::filter()Быстрый фильтр в колонке
HTTP FilterСложная логика, переиспользование
SelectionПанель с несколькими фильтрами

Правила, к которым сводится работа с фильтрами.

allowedFilters и allowedSorts — минимально необходимые. Это не формальность, а определение того, что пользователь может делать с вашей таблицей.

Индексы обязательны на всех полях, по которым фильтруете и сортируете. Без них каждое применение фильтра — полное сканирование таблицы, и на сотне тысяч записей админка начинает «думать» по несколько секунд на каждый клик.

Помните про ограничение поиска по подстроке. Условие вида LIKE '%текст%' индекс использовать не может в принципе — ведущий символ подстановки исключает это. На больших таблицах такой фильтр остаётся медленным, сколько индексов ни добавляй; если поиск по тексту действительно нужен, это задача для полнотекстового индекса или отдельного поискового движка, а не для фильтра в таблице.

Фильтр по связанной таблице — отдельная история. Он порождает JOIN или подзапрос, и здесь индекс нужен уже на связанной таблице. Это самая частая причина, по которой «один фильтр почему-то тормозит, а остальные нет».

Группируйте фильтры вместо того, чтобы плодить их по одному на поле. Панель из пятнадцати фильтров бесполезна: оператор не найдёт нужный. Три-четыре, закрывающие реальные сценарии работы, полезнее полного покрытия полей.

💡 Совет

Полезная привычка при разработке экранов со списками: включите логирование запросов в локальном окружении и посмотрите, что реально уходит в базу при применении фильтров. Обычно обнаруживаются две вещи — отсутствие нужного индекса и лишние запросы к связанным таблицам, которые решаются добавлением with() в запрос экрана.

🚀

Хотите такое же решение?

Настрою окружение под ваш проект, учту специфику инфраструктуры и обучу команду.

Обсудить проект →
Бесплатная консультация · Ответ в течение дня

Комментарии

Система комментариев скоро будет подключена. А пока вы можете написать мне в Telegram или на email.