#05 FilamentPHP応用

インポートとエクスポート——CSVデータ連携

Filamentのインポート・エクスポート機能

Filament v5以降、ImportActionExportActionが標準機能として追加されました。CSVファイルのアップロードによるデータインポート、テーブルデータのCSV/XLSXエクスポートが、ほぼコードを書かずに実装できます。

大量データは自動的にJobキューで非同期処理されるため、タイムアウトの心配もありません。

インポーターの作成

php artisan make:filament-importer Post

生成ファイル: app/Filament/Imports/PostImporter.php

namespace App\Filament\Imports;

use App\Models\Post;
use Filament\Actions\Imports\ImportColumn;
use Filament\Actions\Imports\Importer;
use Filament\Actions\Imports\Models\Import;

class PostImporter extends Importer
{
    protected static ?string $model = Post::class;

    public static function getColumns(): array
    {
        return [
            ImportColumn::make('title')
                ->label('タイトル')
                ->requiredMapping()
                ->rules(['required', 'max:255']),

            ImportColumn::make('body')
                ->label('本文'),

            ImportColumn::make('status')
                ->label('ステータス')
                ->rules(['in:draft,published,archived']),

            ImportColumn::make('published_at')
                ->label('公開日時')
                ->castStateUsing(function (string $state): ?Carbon {
                    if (empty($state)) return null;
                    return Carbon::parse($state);
                }),

            ImportColumn::make('category')
                ->label('カテゴリ名')
                ->relationship(resolveUsing: 'name'),
        ];
    }

    public function resolveRecord(): ?Post
    {
        // titleが一致する既存レコードを更新、なければ新規作成
        return Post::firstOrNew([
            'title' => $this->data['title'],
        ]);
    }

    public static function getCompletedNotificationBody(Import $import): string
    {
        $body = "インポート完了: {$import->successful_rows}件成功";

        if ($failedRowsCount = $import->getFailedRowsCount()) {
            $body .= "、{$failedRowsCount}件失敗";
        }

        return $body;
    }
}

resolveRecord() で既存レコードの更新vs新規作成

resolveRecord()はCSVの1行ごとに呼ばれ、対応するEloquentモデルのインスタンスを返します。

// パターン1: 常に新規作成
public function resolveRecord(): ?Post
{
    return new Post();
}

// パターン2: IDで既存レコードを更新、なければ新規作成
public function resolveRecord(): ?Post
{
    return Post::firstOrNew(['id' => $this->data['id'] ?? null]);
}

// パターン3: ユニークキーで重複チェック
public function resolveRecord(): ?Post
{
    return Post::firstOrNew([
        'slug' => Str::slug($this->data['title']),
    ]);
}

// パターン4: nullを返すと行をスキップ
public function resolveRecord(): ?Post
{
    if (empty($this->data['title'])) {
        return null;  // この行はスキップされる
    }
    return new Post();
}

getColumns() でマッピング定義

ImportColumnでCSVカラムとモデル属性のマッピングを定義します。

ImportColumn::make('email')
    ->label('メールアドレス')
    ->requiredMapping()        // UIでのマッピングを必須に
    ->rules(['required', 'email', 'max:255'])
    ->example('user@example.com'),  // サンプルCSVに表示されるサンプル値

ImportColumn::make('price')
    ->label('価格')
    ->numeric(decimalPlaces: 0)
    ->rules(['numeric', 'min:0']),

ImportColumn::make('is_featured')
    ->label('特集')
    ->boolean()                // "true"/"false"/"1"/"0"を自動変換
    ->rules(['boolean']),

ImportAction::make() をテーブルヘッダーに追加

use Filament\Actions\ImportAction;

public function table(Table $table): Table
{
    return $table
        ->headerActions([
            ImportAction::make()
                ->importer(PostImporter::class)
                ->label('CSVインポート')
                ->icon('heroicon-o-arrow-up-tray'),
        ]);
}

または、ページのヘッダーアクションとして追加することもできます。

// PostResource/Pages/ListPosts.php

protected function getHeaderActions(): array
{
    return [
        ImportAction::make()
            ->importer(PostImporter::class),
        CreateAction::make(),
    ];
}

エラー行のダウンロード

インポートが完了すると、Filamentは失敗した行の詳細を記録します。ユーザーはUIからエラー行のみをCSVとしてダウンロードし、修正して再インポートできます。この機能は追加設定なしで自動的に有効です。

失敗したレコードのファイルはstorage/app/filament-importsに保存されます。

エクスポーターの作成

php artisan make:filament-exporter Post

生成ファイル: app/Filament/Exports/PostExporter.php

namespace App\Filament\Exports;

use App\Models\Post;
use Filament\Actions\Exports\ExportColumn;
use Filament\Actions\Exports\Exporter;
use Filament\Actions\Exports\Models\Export;

class PostExporter extends Exporter
{
    protected static ?string $model = Post::class;

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

            ExportColumn::make('title')
                ->label('タイトル'),

            ExportColumn::make('status')
                ->label('ステータス')
                ->formatStateUsing(fn (string $state) => match($state) {
                    'draft' => '下書き',
                    'published' => '公開中',
                    'archived' => 'アーカイブ',
                    default => $state,
                }),

            ExportColumn::make('category.name')
                ->label('カテゴリ'),

            ExportColumn::make('published_at')
                ->label('公開日時')
                ->formatStateUsing(
                    fn (?Carbon $state) => $state?->format('Y/m/d H:i')
                ),

            ExportColumn::make('views_count')
                ->label('閲覧数')
                ->counts('views'),  // リレーションのカウント
        ];
    }

    public static function getCompletedNotificationBody(Export $export): string
    {
        return "エクスポート完了: {$export->successful_rows}件";
    }
}

ExportAction::make() + フォーマット選択(CSV/XLSX)

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

->headerActions([
    ExportAction::make()
        ->exporter(PostExporter::class)
        ->label('エクスポート')
        ->icon('heroicon-o-arrow-down-tray')
        ->formats([
            ExportFormat::Csv,
            ExportFormat::Xlsx,
        ])
        ->fileName(fn () => 'posts-' . now()->format('YmdHis')),
])

ユーザーはモーダルでCSVかXLSXを選んでダウンロードできます。XLSXサポートにはmaatwebsite/excelパッケージが必要です。

composer require maatwebsite/excel

Jobキューを使った非同期処理

インポート・エクスポートは内部的にLaravelのJobとして処理されます。本番環境では必ずキューワーカーを起動してください。

php artisan queue:work --queue=default

大量データ(数万行)のインポートでは、1ジョブあたりの処理行数(チャンクサイズ)を調整できます。

class PostImporter extends Importer
{
    public static function getOptionsFormComponents(): array
    {
        return [
            TextInput::make('chunkSize')
                ->label('チャンクサイズ')
                ->integer()
                ->default(100)
                ->minValue(1)
                ->maxValue(1000),
        ];
    }
}

また、インポート処理にメモリが多く必要な場合はphp.inimemory_limitとキューワーカーのタイムアウトも調整してください。

php artisan queue:work --timeout=3600 --memory=512

インポート用マイグレーション

インポート・エクスポートの進捗管理テーブルが必要です。

php artisan vendor:publish --tag=filament-actions-migrations
php artisan migrate

これでimportsexportsfailed_import_rowsテーブルが作成されます。

まとめ

FilamentのImporter/Exporterは、ファイルアップロード→カラムマッピング→バリデーション→非同期処理→完了通知という一連のフローを標準で実装しています。resolveRecord()のカスタマイズで更新/新規作成のロジックを柔軟に制御でき、エラー行のダウンロード機能でユーザーが自分でデータを修正できる体験を提供できます。次のエピソードでは高度なテーブル機能を解説します。