#06 FilamentPHP応用

高度なテーブル——グループ化・集計・カスタムクエリ

テーブルをさらに使いこなす

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も実現できます。次のエピソードではマルチテナンシーの実装を解説します。