#27 FilamentPHP応用

テーブルの集計とサマリー——Summaries APIで行の集計を表示

Summaries APIとは

FilamentのSummaries APIはテーブルの各列フッターに集計値(合計・平均・件数・範囲)を表示する機能です。グループ化と組み合わせると、グループごとの小計も自動的に表示されます。

基本的な集計の設定

use Filament\Tables\Columns\Summarizers\Sum;
use Filament\Tables\Columns\Summarizers\Average;
use Filament\Tables\Columns\Summarizers\Count;
use Filament\Tables\Columns\Summarizers\Range;

public function table(Table $table): Table
{
    return $table
        ->columns([
            TextColumn::make('product_name')
                ->label('商品名')
                ->searchable(),

            TextColumn::make('quantity')
                ->label('数量')
                ->numeric()
                ->summarize([
                    Sum::make()->label('合計数量'),
                    Average::make()
                        ->label('平均数量')
                        ->numeric(decimalPlaces: 1),
                ]),

            TextColumn::make('unit_price')
                ->label('単価')
                ->money('JPY')
                ->summarize([
                    Average::make()
                        ->label('平均単価')
                        ->money('JPY'),
                    Range::make()
                        ->label('価格範囲'),
                ]),

            TextColumn::make('total_amount')
                ->label('小計')
                ->money('JPY')
                ->summarize([
                    Sum::make()
                        ->label('合計金額')
                        ->money('JPY'),
                ]),

            TextColumn::make('status')
                ->label('ステータス')
                ->badge()
                ->summarize([
                    Count::make()
                        ->label('処理済み件数')
                        ->query(fn (Builder $query) =>
                            $query->where('status', 'processed')
                        ),
                ]),
        ]);
}

通貨・数値フォーマットの設定

Sum::make()
    ->label('合計')
    ->money('JPY')  // ¥ フォーマット

Average::make()
    ->label('平均')
    ->numeric(
        decimalPlaces: 2,
        decimalSeparator: '.',
        thousandsSeparator: ',',
    )

// カスタムフォーマット
Sum::make()
    ->label('合計数量')
    ->formatStateUsing(fn ($state): string =>
        number_format($state) . '個'
    )

グループ化との組み合わせ

グループ化を設定すると、グループごとの小計が自動的に表示されます。

public function table(Table $table): Table
{
    return $table
        ->groups([
            Group::make('category.name')
                ->label('カテゴリ')
                ->collapsible(),
        ])
        ->defaultGroup('category.name')
        ->columns([
            TextColumn::make('name')
                ->label('商品名'),

            TextColumn::make('sales_count')
                ->label('販売数')
                ->numeric()
                ->summarize([
                    Sum::make()->label('カテゴリ計'),
                ]),

            TextColumn::make('revenue')
                ->label('売上')
                ->money('JPY')
                ->summarize([
                    Sum::make()->label('カテゴリ売上'),
                ]),
        ]);
}

グループごとの小計がグループヘッダーに表示され、テーブルフッターには全体の合計が表示されます。

カスタムSummarizerの作成

標準の集計では対応できない場合はカスタムSummarizerを作成します。

use Filament\Tables\Columns\Summarizers\Summarizer;
use Illuminate\Database\Query\Builder;

TextColumn::make('status')
    ->label('ステータス')
    ->summarize([
        Summarizer::make()
            ->label('ステータス内訳')
            ->using(fn (Builder $query): string => implode(' / ', [
                '未処理: '  . $query->where('status', 'pending')->count(),
                '処理中: '  . $query->where('status', 'processing')->count(),
                '完了: '    . $query->where('status', 'completed')->count(),
            ]))
    ]),

フィルタ適用時の集計

フィルタを適用すると集計値も自動的に絞り込まれます。これはSummarizeAllではなくフィルタ後のデータに対して集計が実行されるためです。

特定の条件で集計したい場合は->query()でクエリを追加します。

TextColumn::make('amount')
    ->label('金額')
    ->money('JPY')
    ->summarize([
        // フィルタ後の全データの合計
        Sum::make()->label('表示中の合計'),

        // 特定の条件(refunded=falseのもの)の合計
        Sum::make()
            ->label('未返金合計')
            ->query(fn (Builder $query) => $query->where('refunded', false)),
    ])

集計の表示位置

集計はデフォルトでテーブルフッターに表示されますが、グループ化と組み合わせるとグループフッターにも表示されます。

->groups([
    Group::make('department')
        ->label('部署')
        ->collapsible()
        ->getTitleFromRecordUsing(fn (Employee $record): string =>
            $record->department->name
        ),
])

Placeholderを使った計算値の表示

テーブルではなく、フォームやカスタムページで集計を表示する場合はPlaceholderを使います。

// フォーム内での集計表示
use Filament\Forms\Components\Placeholder;

Placeholder::make('total')
    ->label('合計金額')
    ->content(function (Get $get): string {
        $items = $get('order_items') ?? [];
        $total = collect($items)->sum(fn ($item) =>
            ($item['unit_price'] ?? 0) * ($item['quantity'] ?? 0)
        );
        return '¥' . number_format($total);
    })

ウィジェットとの組み合わせ

テーブルのSummariesとStatsOverviewウィジェットを組み合わせると、より詳細なデータ分析画面が作れます。

// テーブルページのヘッダーウィジェット
protected function getHeaderWidgets(): array
{
    return [
        SalesStatsWidget::class,
    ];
}

// ウィジェット内でもSQLの集計を表示
protected function getStats(): array
{
    $period = now()->startOfMonth();

    return [
        Stat::make('今月の売上', '¥' . number_format(
            Order::where('created_at', '>=', $period)->sum('total_amount')
        ))->color('success'),

        Stat::make('今月の注文数', Order::where('created_at', '>=', $period)->count())
            ->color('info'),

        Stat::make('平均注文額', '¥' . number_format(
            Order::where('created_at', '>=', $period)->avg('total_amount') ?? 0
        ))->color('warning'),
    ];
}

コツ・注意点・ハマりポイント

コツ: 集計はテーブルに表示されている全ページのデータに対して実行されます(ページネーションを超えて)。大量データの場合はDBインデックスが効いているか確認し、重い集計クエリはウィジェットでキャッシュして対処してください。

注意点: Count::make()はそのカラムのNULL以外の行数をカウントします。全行数をカウントしたい場合は->query(fn ($query) => $query)でクエリを渡してください。

ハマりポイント: グループ化と複数のSummarizeを組み合わせると、グループフッターとテーブルフッターの両方に集計が表示されます。意図しない二重表示になる場合は、どちらか一方を->visibleInColumn()で制御してください。

まとめ

FilamentのSummaries APIはSum・Average・Count・Rangeの4種類の標準Summarizerに加え、カスタムSummarizerで任意の集計ロジックが実装できます。グループ化と組み合わせることで、カテゴリ別・担当者別などの小計付きテーブルを簡単に構築できます。