テーブルの高度なフィルタリング——カスタムFilterとQueryBuilderの組み合わせ
Filamentのフィルタリングシステム
Filamentは複数のフィルタタイプを提供しています。
| フィルタタイプ | 用途 |
|---|---|
Filter | カスタムフォームを持つ自由なフィルタ |
SelectFilter | ドロップダウンで値を選択 |
TernaryFilter | はい/いいえ/どちらでも |
DateRangeFilter | 日付範囲の絞り込み |
QueryBuilder | 複雑な条件を自由に組み合わせ |
SelectFilterの応用
use Filament\Tables\Filters\SelectFilter;
->filters([
SelectFilter::make('status')
->label('ステータス')
->options([
'draft' => '下書き',
'published' => '公開中',
'archived' => 'アーカイブ',
])
->multiple() // 複数選択可
->searchable(), // 選択肢を検索可能に
SelectFilter::make('category')
->label('カテゴリ')
->relationship('category', 'name') // リレーションから選択肢を生成
->multiple()
->preload(),
SelectFilter::make('author')
->label('著者')
->relationship('author', 'name')
->searchable()
->multiple()
->getOptionLabelFromRecordUsing(
fn (User $record): string => "{$record->name} ({$record->email})"
),
])
カスタムFilterの実装
Filter::make()で完全カスタムのフィルタを作ります。
use Filament\Tables\Filters\Filter;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\TextInput;
Filter::make('published_date_range')
->label('公開期間')
->form([
DatePicker::make('published_from')
->label('開始日'),
DatePicker::make('published_until')
->label('終了日'),
])
->query(function (Builder $query, array $data): Builder {
return $query
->when(
$data['published_from'],
fn ($q, $date) => $q->whereDate('published_at', '>=', $date)
)
->when(
$data['published_until'],
fn ($q, $date) => $q->whereDate('published_at', '<=', $date)
);
})
->indicateUsing(function (array $data): array {
// アクティブフィルタのバッジ表示
$indicators = [];
if ($data['published_from']) {
$indicators[] = Indicator::make('公開開始: ' . $data['published_from'])
->removeField('published_from');
}
if ($data['published_until']) {
$indicators[] = Indicator::make('公開終了: ' . $data['published_until'])
->removeField('published_until');
}
return $indicators;
}),
TernaryFilterの使い方
use Filament\Tables\Filters\TernaryFilter;
TernaryFilter::make('is_featured')
->label('おすすめ記事')
->nullable() // null = すべて、true = おすすめのみ、false = 通常のみ
->trueLabel('おすすめのみ')
->falseLabel('通常のみ')
->queries(
true: fn (Builder $query) => $query->where('is_featured', true),
false: fn (Builder $query) => $query->where('is_featured', false),
blank: fn (Builder $query) => $query, // nullの場合(すべて表示)
),
TernaryFilter::make('has_thumbnail')
->label('サムネイル')
->trueLabel('あり')
->falseLabel('なし')
->queries(
true: fn (Builder $query) => $query->whereNotNull('thumbnail'),
false: fn (Builder $query) => $query->whereNull('thumbnail'),
blank: fn (Builder $query) => $query,
),
QueryBuilderフィルタ
FilamentのQueryBuilderはエンドユーザーが自由に条件を組み合わせられる高機能フィルタです。
use Filament\Tables\Filters\QueryBuilder;
use Filament\Tables\Filters\QueryBuilder\Constraints\TextConstraint;
use Filament\Tables\Filters\QueryBuilder\Constraints\NumberConstraint;
use Filament\Tables\Filters\QueryBuilder\Constraints\DateConstraint;
use Filament\Tables\Filters\QueryBuilder\Constraints\SelectConstraint;
use Filament\Tables\Filters\QueryBuilder\Constraints\BooleanConstraint;
->filters([
QueryBuilder::make()
->constraints([
TextConstraint::make('title')
->label('タイトル'),
TextConstraint::make('body')
->label('本文')
->icon('heroicon-o-document-text'),
SelectConstraint::make('status')
->label('ステータス')
->options([
'draft' => '下書き',
'published' => '公開中',
'archived' => 'アーカイブ',
])
->multiple(),
NumberConstraint::make('views_count')
->label('閲覧数')
->icon('heroicon-o-eye'),
DateConstraint::make('published_at')
->label('公開日'),
BooleanConstraint::make('is_featured')
->label('おすすめ記事'),
])
->constraintPickerColumns(2), // 条件選択UIの列数
])
フィルタのレイアウト設定
use Filament\Tables\Enums\FiltersLayout;
->filtersLayout(FiltersLayout::AboveContent) // テーブル上部に表示
// FiltersLayout::BelowContent // テーブル下部
// FiltersLayout::AboveContentCollapsible // 折りたたみ可能
// FiltersLayout::Dropdown // ドロップダウン(デフォルト)
->filtersTriggerAction(
fn (Action $action) => $action
->label('絞り込み')
->icon('heroicon-o-funnel')
)
->filtersFormColumns(3) // フィルタフォームの列数
フィルタ状態のURLパラメータへの永続化
フィルタの状態をURLに含めることで、特定のフィルタ条件のURLを共有できます。
// ListRecordsページでURLフィルタを有効化
class ListOrders extends ListRecords
{
// フィルタの状態をURLクエリパラメータに保存
protected function getTableFiltersFormStateSessionKey(): ?string
{
return 'orders_table_filters'; // セッションキー
}
}
URLでフィルタを指定する例(フィルタ値をURLに含める場合):
// フィルタの初期値をURLパラメータから設定
protected function getDefaultTableFilters(): array
{
return [
'status' => request()->query('status', 'pending'),
];
}
アクティブフィルタの表示(Indicators)
適用されているフィルタを一目で分かるバッジとして表示し、個別にクリアできるようにします。
Filter::make('price_range')
->form([
TextInput::make('min_price')->numeric()->label('最低価格'),
TextInput::make('max_price')->numeric()->label('最高価格'),
])
->query(fn (Builder $query, array $data): Builder =>
$query
->when($data['min_price'], fn ($q, $v) => $q->where('price', '>=', $v))
->when($data['max_price'], fn ($q, $v) => $q->where('price', '<=', $v))
)
->indicateUsing(function (array $data): array {
$indicators = [];
if (! empty($data['min_price'])) {
$indicators[] = Indicator::make('最低価格: ¥' . number_format($data['min_price']))
->removeField('min_price');
}
if (! empty($data['max_price'])) {
$indicators[] = Indicator::make('最高価格: ¥' . number_format($data['max_price']))
->removeField('max_price');
}
return $indicators;
}),
コツ・注意点・ハマりポイント
コツ: QueryBuilderフィルタはエンドユーザーが自由に条件を組み合わせられる強力な機能です。ただし、技術的でないユーザーには複雑に見えることもあります。一般的な絞り込みはSelectFilterやTernaryFilterで提供し、高度な検索が必要なパワーユーザー向けにQueryBuilderをオプションで提供するのが理想的です。
注意点: カスタムFiltersで複雑なJOINを含むクエリを書く場合、他のフィルタや検索クエリと競合することがあります。->query()内では$queryのコピーを変更するため、基本的には安全ですが、グローバルスコープや既存のJOINと干渉しないか確認してください。
ハマりポイント: ->filtersLayout(FiltersLayout::AboveContent)を使うと、フィルタが常に表示された状態になります。この設定は省スペースが要求される画面には不向きです。デフォルトのDropdownかユーザーが開閉できるAboveContentCollapsibleの使い分けを検討してください。
まとめ
FilamentのフィルタリングはSelectFilter・TernaryFilter・カスタムFilter・QueryBuilderの4種類を用途に応じて使い分けることで、シンプルな絞り込みから複雑な検索条件まで対応できます。->indicateUsing()でアクティブフィルタを視覚的に表示すれば、ユーザーが現在の絞り込み状態を常に把握できます。