Фильтры в Orchid устроены удобно: добавил трейт в модель, перечислил разрешённые поля — и таблица фильтруется параметрами из адресной строки. Ровно из-за этого удобства здесь есть две вещи, о которых нужно знать: allowedFilters — это не список для галочки, а граница безопасности, а фильтр по подстроке на большой таблице работает так же плохо, как везде.
Типы фильтров в Orchid
Orchid предоставляет два подхода к фильтрации:
- Встроенные фильтры TD — прямо в колонках таблицы
- HTTP Filters — отдельные классы фильтров
- Selection — панель фильтров над таблицей
Когда что использовать:
- TD-фильтры — быстрая фильтрация по одному полю
- HTTP Filters — сложная логика, переиспользование
- Selection — визуальная панель с несколькими фильтрами
TD-фильтры в таблицах
Текстовый фильтр
use Orchid\Screen\TD;
Layout::table('users', [
TD::make('name', 'Имя')
->filter(Input::make()) // Простой текстовый фильтр
->sort(),
TD::make('email', 'Email')
->filter() // Короткая запись — текстовый фильтр
->sort(),
]);Select-фильтр
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-фильтр
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
// 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
Создание фильтра
php artisan orchid:filter StatusFilter// 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('Статус'),
];
}
}Фильтр по дате
// 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')),
];
}
}Фильтр по связи
// 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
// 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
// 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(),
]),
];
}
}Сложный фильтр с логикой
// 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('Активность'),
];
}
}Сортировка по умолчанию
public function query(): iterable
{
return [
'orders' => Order::filters()
->defaultSort('created_at', 'desc') // По умолчанию
->paginate(),
];
}Сортировка по связанному полю
// В модели
protected $allowedSorts = [
'id',
'total',
'created_at',
'user.name', // Связанное поле
];
// В Screen
TD::make('user.name', 'Клиент')
->sort()
->render(fn($order) => $order->user->name),Производительность: При фильтрации по связям используйте whereHas с осторожностью — на больших таблицах это медленно. Рассмотрите денормализацию или индексы.
Сохранение фильтров в URL
Orchid автоматически сохраняет состояние фильтров в URL:
/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.