#26 FilamentPHP応用

テーブルのエクスポート——CSV/Excel出力の実装

エクスポートの概要

Filamentのエクスポート機能(エピソード5でも触れましたが、このエピソードではより深く解説)は非同期Jobを使って大量データを効率的にエクスポートします。ユーザーがエクスポートを開始すると、バックグラウンドでJobが実行され、完了したら通知でダウンロードリンクが届きます。

Exporterクラスの詳細設計

php artisan make:filament-exporter Order
namespace App\Filament\Exports;

use App\Models\Order;
use Filament\Actions\Exports\ExportColumn;
use Filament\Actions\Exports\Exporter;
use Filament\Actions\Exports\Models\Export;
use Illuminate\Support\Carbon;

class OrderExporter extends Exporter
{
    protected static ?string $model = Order::class;

    public static function getColumns(): array
    {
        return [
            ExportColumn::make('id')
                ->label('注文ID'),

            ExportColumn::make('order_number')
                ->label('注文番号'),

            ExportColumn::make('customer.name')
                ->label('顧客名'),

            ExportColumn::make('customer.email')
                ->label('メールアドレス'),

            ExportColumn::make('status')
                ->label('ステータス')
                ->formatStateUsing(fn (string $state): string => match($state) {
                    'pending'    => '処理待ち',
                    'processing' => '処理中',
                    'shipped'    => '発送済み',
                    'delivered'  => '配達完了',
                    'cancelled'  => 'キャンセル',
                    default      => $state,
                }),

            ExportColumn::make('total_amount')
                ->label('合計金額')
                ->formatStateUsing(fn (int $state): string =>
                    '¥' . number_format($state)
                ),

            ExportColumn::make('items_count')
                ->label('商品点数')
                ->counts('orderItems'),

            ExportColumn::make('created_at')
                ->label('注文日時')
                ->formatStateUsing(fn (?Carbon $state): string =>
                    $state?->format('Y/m/d H:i:s') ?? ''
                ),

            ExportColumn::make('shipped_at')
                ->label('発送日時')
                ->formatStateUsing(fn (?Carbon $state): string =>
                    $state?->format('Y/m/d H:i:s') ?? ''
                ),

            // 計算値カラム
            ExportColumn::make('processing_days')
                ->label('処理日数')
                ->state(fn (Order $record): string => $record->shipped_at
                    ? $record->created_at->diffInDays($record->shipped_at) . '日'
                    : '-'
                ),
        ];
    }

    public static function getCompletedNotificationBody(Export $export): string
    {
        $body = "注文データのエクスポートが完了しました。\n";
        $body .= "{$export->successful_rows}件エクスポートしました。";

        if ($failedRowsCount = $export->getFailedRowsCount()) {
            $body .= "\n({$failedRowsCount}件の処理に失敗しました)";
        }

        return $body;
    }
}

ユーザーが選択できるカラム

->enabledByDefault(false)でデフォルトでは選択されない(オプション)カラムを定義できます。

public static function getColumns(): array
{
    return [
        ExportColumn::make('id')->label('ID'),
        ExportColumn::make('order_number')->label('注文番号'),
        ExportColumn::make('customer.name')->label('顧客名'),

        // デフォルトでは非表示(ユーザーが選択した場合のみ出力)
        ExportColumn::make('customer.phone')
            ->label('顧客電話番号')
            ->enabledByDefault(false),

        ExportColumn::make('shipping_address')
            ->label('配送先住所')
            ->enabledByDefault(false),

        ExportColumn::make('internal_notes')
            ->label('社内メモ')
            ->enabledByDefault(false),
    ];
}

ExportActionのカスタマイズ

use Filament\Actions\ExportAction;
use Filament\Actions\Exports\Enums\ExportFormat;

protected function getHeaderActions(): array
{
    return [
        ExportAction::make()
            ->exporter(OrderExporter::class)
            ->label('エクスポート')
            ->icon('heroicon-o-arrow-down-tray')
            ->formats([
                ExportFormat::Csv,
                ExportFormat::Xlsx,
            ])
            ->fileName(fn (): string =>
                'orders-' . now()->format('Y-m-d_H-i-s')
            )
            ->chunkSize(200)          // 1Jobあたりの処理件数
            ->maxRows(50000)          // エクスポート上限
    ];
}

フィルタされたデータのエクスポート

テーブルに適用されたフィルタをエクスポートにも反映させる場合、テーブルのヘッダーアクションとしてExportActionを追加します。

public function table(Table $table): Table
{
    return $table
        ->filters([
            SelectFilter::make('status')
                ->options([
                    'pending' => '処理待ち',
                    'shipped' => '発送済み',
                ]),
            Filter::make('date_range')
                ->form([
                    DatePicker::make('from')->label('開始日'),
                    DatePicker::make('until')->label('終了日'),
                ])
                ->query(fn (Builder $query, array $data): Builder =>
                    $query
                        ->when($data['from'], fn ($q, $v) => $q->whereDate('created_at', '>=', $v))
                        ->when($data['until'], fn ($q, $v) => $q->whereDate('created_at', '<=', $v))
                ),
        ])
        ->headerActions([
            ExportAction::make()
                ->exporter(OrderExporter::class)
                // テーブルのフィルタが自動的に適用される
        ]);
}

テーブルの->headerActions()ExportActionを置くと、現在のフィルタ・検索条件が自動的にエクスポートに適用されます。

エクスポート用の専用クエリ

class OrderExporter extends Exporter
{
    public static function modifyQuery(Builder $query): Builder
    {
        // Eager Loadingを追加(N+1問題防止)
        return $query->with(['customer', 'orderItems', 'shippingAddress'])
                     ->withCount('orderItems');
    }
}

XLSXのセルスタイル設定

XLSXエクスポートでは、maatwebsite/excelパッケージを使ったスタイル設定が可能です。

// 数値カラムの書式設定
ExportColumn::make('total_amount')
    ->label('合計金額')
    ->formatStateUsing(fn (int $state): int => $state)
    // XLSXでは数値として出力(Excelで集計可能)

より高度なXLSXカスタマイズ(セルの背景色・ヘッダーのスタイルなど)が必要な場合は、Exporterのafter処理でOpenSpoutなどを使ったカスタム実装が必要になります。

エクスポートジョブのキュー設定

本番環境では専用のキューを設定してエクスポートJobが管理画面のレスポンスに影響しないようにします。

# .env
FILAMENT_EXPORT_QUEUE=exports
// app/Filament/Exports/OrderExporter.php

public static function getJobQueue(): ?string
{
    return 'exports';  // 専用キュー
}
# キューワーカーの起動
php artisan queue:work --queue=exports

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

コツ: ExportColumn::make()->counts('relation')でリレーションのカウントをエクスポートできます。ただしN+1を防ぐため、modifyQuery()withCount()を追加することをセットで行ってください。

注意点: エクスポートファイルはstorage/app/filament-exports/に保存されます。本番環境では定期的なクリーンアップが必要です。Filamentが提供するfilament:prune-exportsコマンドをCronで実行してください。

# 7日以上前のエクスポートファイルを削除
php artisan filament:prune-exports --days=7

ハマりポイント: XLSXエクスポートで日本語(マルチバイト文字)が文字化けする場合は、ファイルのエンコーディング設定を確認してください。CSVの場合はBOM付きUTF-8にすることでExcelでの文字化けを防げます。カスタムエクスポータで--bomオプションを付けることで対処できます。

まとめ

FilamentのExportActionとExporterクラスを使えば、フォーマット変換・非同期処理・ユーザーによるカラム選択まで含めた本格的なエクスポート機能が実装できます。テーブルフィルタとの連携でフィルタされたデータのみエクスポートする設計が現場では最も使いやすいパターンです。