インポートとエクスポート——CSVデータ連携
Filamentのインポート・エクスポート機能
Filament v5以降、ImportActionとExportActionが標準機能として追加されました。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.iniのmemory_limitとキューワーカーのタイムアウトも調整してください。
php artisan queue:work --timeout=3600 --memory=512
インポート用マイグレーション
インポート・エクスポートの進捗管理テーブルが必要です。
php artisan vendor:publish --tag=filament-actions-migrations
php artisan migrate
これでimports・exports・failed_import_rowsテーブルが作成されます。
まとめ
FilamentのImporter/Exporterは、ファイルアップロード→カラムマッピング→バリデーション→非同期処理→完了通知という一連のフローを標準で実装しています。resolveRecord()のカスタマイズで更新/新規作成のロジックを柔軟に制御でき、エラー行のダウンロード機能でユーザーが自分でデータを修正できる体験を提供できます。次のエピソードでは高度なテーブル機能を解説します。