高度なテーブル——グループ化・集計・カスタムクエリ
テーブルをさらに使いこなす
Filamentのテーブルには、基本的なCRUD表示にとどまらない高度な機能が多数用意されています。このエピソードでは、グループ化・集計・カスタムクエリ・仮想カラムなど、実務で差がつく機能を解説します。
->groups() でグループ化
->groups()を使うとレコードを特定のカラムでグループ化して表示できます。
use Filament\Tables\Grouping\Group;
public function table(Table $table): Table
{
return $table
->columns([...])
->groups([
Group::make('status')
->label('ステータス')
->collapsible(),
Group::make('category.name')
->label('カテゴリ')
->collapsible()
->orderQueryUsing(
fn (Builder $query, string $direction) =>
$query->orderBy('categories.name', $direction)
),
Group::make('published_at')
->label('公開月')
->date() // 日付でグループ化(年月ごと)
->collapsible(),
])
->defaultGroup('status') // デフォルトのグループ化
->collapsible()を付けるとグループの折りたたみ/展開ができます。ユーザーはUIのドロップダウンでグループ化の切り替えが可能です。
集計カラム:合計・平均・カウント
Summarizeを使うとテーブルフッターに集計値を表示できます。
use Filament\Tables\Columns\Summarizers\Sum;
use Filament\Tables\Columns\Summarizers\Average;
use Filament\Tables\Columns\Summarizers\Count;
use Filament\Tables\Columns\Summarizers\Range;
->columns([
TextColumn::make('title'),
TextColumn::make('price')
->money('JPY')
->summarize([
Sum::make()->label('合計'),
Average::make()->label('平均'),
]),
TextColumn::make('views_count')
->numeric()
->summarize([
Sum::make()->label('総閲覧数'),
Average::make()->label('平均閲覧数')
->numeric(decimalPlaces: 1),
]),
TextColumn::make('status')
->badge()
->summarize([
Count::make()
->label('公開件数')
->query(fn (Builder $query) => $query->where('status', 'published')),
]),
])
グループ化と組み合わせると、グループごとの小計も自動で表示されます。
->modifyQueryUsing(fn) でクエリカスタマイズ
カラムに対するクエリを細かく調整できます。
TextColumn::make('comments_count')
->label('承認済みコメント数')
->counts([
'comments' => fn (Builder $query) => $query->where('approved', true),
])
->sortable(),
TextColumn::make('category.name')
->label('カテゴリ')
->sortable()
->searchable(
query: fn (Builder $query, string $search): Builder =>
$query->whereHas('category', fn ($q) =>
$q->where('name', 'like', "%{$search}%")
)
),
->query(fn) でEloquentスコープ適用
テーブル全体のベースクエリを->query()でカスタマイズできます。
public function table(Table $table): Table
{
return $table
->query(
Post::query()
->withCount(['comments', 'views'])
->with(['category', 'author'])
->whereNotNull('published_at')
)
->columns([...]);
}
リソース側でもgetEloquentQuery()をオーバーライドできます。
// app/Filament/Resources/PostResource.php
public static function getEloquentQuery(): Builder
{
return parent::getEloquentQuery()
->withoutGlobalScopes([SoftDeletingScope::class])
->with(['category', 'tags'])
->withCount('comments');
}
仮想カラム ->state(fn)
データベースに存在しない計算値を表示するには->state()を使います。
TextColumn::make('engagement_score')
->label('エンゲージメントスコア')
->state(function (Post $record): string {
$score = ($record->views_count * 1)
+ ($record->comments_count * 5)
+ ($record->likes_count * 3);
return number_format($score);
})
->badge()
->color(fn (string $state): string => match(true) {
(int)str_replace(',', '', $state) >= 1000 => 'success',
(int)str_replace(',', '', $state) >= 100 => 'warning',
default => 'gray',
}),
TextColumn::make('reading_time')
->label('読了時間')
->state(fn (Post $record): string =>
ceil(mb_strlen(strip_tags($record->body)) / 400) . '分'
),
仮想カラムはデータベースに値がないため、デフォルトでは->sortable()・->searchable()は使えません。ソートが必要な場合は->sortable(query: fn)でカスタムクエリを渡します。
->tooltip(fn) でツールチップ
長いテキストや補足情報をツールチップで表示します。
TextColumn::make('title')
->limit(30)
->tooltip(fn (Post $record): string => $record->title), // 省略時にフル表示
TextColumn::make('status')
->badge()
->tooltip(fn (Post $record): string => match($record->status) {
'published' => "公開日: {$record->published_at?->format('Y/m/d')}",
'draft' => '未公開の下書き',
'archived' => "アーカイブ日: {$record->archived_at?->format('Y/m/d')}",
default => '',
}),
->description(fn) でサブテキスト
各セルにサブテキストを表示してコンテキストを補足できます。
TextColumn::make('title')
->description(fn (Post $record): string =>
$record->category->name . ' · ' . $record->published_at?->diffForHumans()
)
->wrap(),
->description()の第2引数で位置を変更できます。
->description('上に表示', position: 'above') // タイトルの上に表示
->description('下に表示', position: 'below') // デフォルト
テーブルのコンテンツ切り替え ->contentGrid()
テーブルビューをグリッドカード表示に切り替えられます。
public function table(Table $table): Table
{
return $table
->contentGrid([
'md' => 2, // mdサイズで2列
'xl' => 3, // xlサイズで3列
])
->columns([
ImageColumn::make('thumbnail')
->height(200)
->width('100%'),
TextColumn::make('title')
->weight(FontWeight::Bold),
TextColumn::make('category.name')
->badge(),
TextColumn::make('published_at')
->since(),
])
ユーザーがテーブルビューとグリッドビューを切り替えられるようにするには:
->defaultPaginationPageOption(12)
->contentGrid(['md' => 2, 'xl' => 3])
リピーターカラム ->listWithLineBreaks()
配列・JSON型カラムやリレーションの複数値を1セルに収めて表示します。
TextColumn::make('tags.name')
->label('タグ')
->badge()
->separator(','),
TextColumn::make('emails')
->label('メールアドレス')
->listWithLineBreaks()
->bulleted(), // 箇条書き形式
// JSONカラム
TextColumn::make('metadata')
->label('メタデータ')
->formatStateUsing(fn ($state) => collect($state)->map(
fn ($v, $k) => "{$k}: {$v}"
)->join("\n"))
->wrap(),
フィルターのカスタマイズ
高度なフィルタリングにはFilter::make()とカスタムフォームを組み合わせます。
use Filament\Tables\Filters\Filter;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Filters\TernaryFilter;
->filters([
SelectFilter::make('status')
->label('ステータス')
->options(['draft' => '下書き', 'published' => '公開中', 'archived' => 'アーカイブ'])
->multiple(),
Filter::make('published_at')
->label('公開期間')
->form([
DatePicker::make('from')->label('開始日'),
DatePicker::make('until')->label('終了日'),
])
->query(function (Builder $query, array $data): Builder {
return $query
->when($data['from'], fn ($q, $date) =>
$q->whereDate('published_at', '>=', $date)
)
->when($data['until'], fn ($q, $date) =>
$q->whereDate('published_at', '<=', $date)
);
}),
TernaryFilter::make('featured')
->label('特集記事')
->nullable(),
])
->filtersLayout(FiltersLayout::AboveContent) // フィルターをテーブル上部に表示
まとめ
Filamentのテーブルは標準のCRUD表示を大きく超えた機能を持っています。->groups()・集計・->state(fn)の仮想カラムを組み合わせることで、データ分析的な管理画面も作れます。->contentGrid()でカード表示に切り替えるとビジュアル的に豊かなUIも実現できます。次のエピソードではマルチテナンシーの実装を解説します。