导入操作
简介
Filament 包含一个能够从 CSV 导入行的操作。点击触发按钮后,模态框会要求用户选择文件。上传后,用户可以将 CSV 中的每一列映射到数据库中的真实列。若有行未通过验证,会在其余行导入完成后编译成可下载的 CSV,供用户审阅。用户还可以下载包含全部可导入列的示例 CSV 文件。
php artisan make:queue-batches-table
php artisan make:notifications-table
php artisan vendor:publish --tag=filament-actions-migrations
php artisan migrate若希望在面板中接收导入通知,可以在 面板配置 中启用。
INFO
若使用 PostgreSQL,请确保通知迁移中的 data 列使用 json():$table->json('data')。
INFO
若 User 模型使用 UUID,请确保通知迁移中的 notifiable 列使用 uuidMorphs():$table->uuidMorphs('notifiable')。
你可以这样使用 ImportAction:
use App\Filament\Imports\ProductImporter;
use Filament\Actions\ImportAction;
ImportAction::make()
->importer(ProductImporter::class)

若要将其添加到表格页眉,可以这样做:
use App\Filament\Imports\ProductImporter;
use Filament\Actions\ImportAction;
use Filament\Tables\Table;
public function table(Table $table): Table
{
return $table
->headerActions([
ImportAction::make()
->importer(ProductImporter::class)
]);
}需要 创建「importer」类 来告诉 Filament 如何导入 CSV 的每一行。
若在同一处有多个 ImportAction,应在 make() 方法中为每个指定唯一名称:
use Filament\Actions\ImportAction;
ImportAction::make('importProducts')
->importer(ProductImporter::class)
ImportAction::make('importBrands')
->importer(BrandImporter::class)创建 importer
若要为模型创建 importer 类,可使用 make:filament-importer 命令并传入模型名称:
php artisan make:filament-importer Product这会在 app/Filament/Imports 目录中创建新类。接下来需要定义可导入的 列。
自动生成 importer 列
若想节省时间,Filament 可以根据模型的数据库列,使用 --generate 自动生成 列:
php artisan make:filament-importer Product --generate定义 importer 列
若要定义可导入的列,需要在 importer 类上覆盖 getColumns() 方法,返回 ImportColumn 对象数组:
use Filament\Actions\Imports\ImportColumn;
public static function getColumns(): array
{
return [
ImportColumn::make('name')
->requiredMapping()
->rules(['required', 'max:255']),
ImportColumn::make('sku')
->label('SKU')
->requiredMapping()
->rules(['required', 'max:32']),
ImportColumn::make('price')
->numeric()
->rules(['numeric', 'min:0']),
];
}自定义导入列的标签
每列的标签会根据其名称自动生成,但你可以通过调用 label() 方法覆盖:
use Filament\Actions\Imports\ImportColumn;
ImportColumn::make('sku')
->label('SKU')要求 importer 列必须映射到 CSV 列
你可以调用 requiredMapping() 方法,要求某列必须映射到 CSV 中的列。数据库中必填的列也应要求映射:
use Filament\Actions\Imports\ImportColumn;
ImportColumn::make('sku')
->requiredMapping()若数据库中该列为必填,还需要确保它有 rules(['required']) 验证规则。
若某列未映射,则不会进行验证,因为没有可验证的数据。
若导入既允许创建记录也允许 更新已有记录,但仅在创建记录时要求映射某列(因为该字段必填),可使用 requiredMappingForNewRecordsOnly() 方法代替 requiredMapping():
use Filament\Actions\Imports\ImportColumn;
ImportColumn::make('sku')
->requiredMappingForNewRecordsOnly()若 resolveRecord() 方法返回尚未保存到数据库的模型实例,则仅对该行要求映射该列。若用户未映射该列,且导入中有一行在数据库中尚不存在,则仅该行会失败,并在分析完所有行后将消息加入失败行 CSV。
验证 CSV 数据
你可以调用 rules() 方法为列添加验证规则。这些规则会在保存到数据库前检查 CSV 中每一行的数据:
use Filament\Actions\Imports\ImportColumn;
ImportColumn::make('sku')
->rules(['required', 'max:32'])未通过验证的行不会被导入。相反,它们会被编译成「失败行」的新 CSV,用户可在导入完成后下载。用户会看到每个失败行的验证错误列表。
TIP
除了允许静态值外,rules() 方法也接受一个函数来动态计算值。你可以将各种实用工具作为参数注入该函数。
转换状态
在 验证 之前,可以对 CSV 数据进行类型转换。这有助于将字符串转换为正确的数据类型,否则验证可能失败。例如,若 CSV 中有 price 列,你可能希望将其转换为 float:
use Filament\Actions\Imports\ImportColumn;
ImportColumn::make('price')
->castStateUsing(function (string $state): ?float {
if (blank($state)) {
return null;
}
$state = preg_replace('/[^0-9.]/', '', $state);
$state = floatval($state);
return round($state, precision: 2);
})TIP
除了 $state 之外,castStateUsing() 方法还允许你将各种实用工具作为参数注入该函数。
在此示例中,我们传入用于转换 $state 的函数。该函数会移除字符串中的非数字字符,转换为 float,并四舍五入到两位小数。
INFO
若某列并非 验证必填,且为空,则不会进行转换。
Filament 还附带一些内置转换方法:
use Filament\Actions\Imports\ImportColumn;
ImportColumn::make('price')
->numeric() // Casts the state to a float.
ImportColumn::make('price')
->numeric(decimalPlaces: 2) // Casts the state to a float, and rounds it to 2 decimal places.
ImportColumn::make('quantity')
->integer() // Casts the state to an integer.
ImportColumn::make('is_visible')
->boolean() // Casts the state to a boolean.在转换后变更状态
use Filament\Actions\Imports\ImportColumn;
ImportColumn::make('price')
->numeric()
->castStateUsing(function (float $state): ?float {
if (blank($state)) {
return null;
}
return round($state * 100);
})你甚至可以通过在函数中定义 $originalState 参数,访问转换前的原始状态:
use Filament\Actions\Imports\ImportColumn;
ImportColumn::make('price')
->numeric()
->castStateUsing(function (float $state, mixed $originalState): ?float {
// ...
})TIP
除了 $state 之外,castStateUsing() 方法还允许你将各种实用工具作为参数注入该函数。
处理单列中的多个值
你可以使用 multiple() 方法将列中的值转换为数组。它接受分隔符作为第一个参数,用于将该列中的值拆分为数组。例如,若 CSV 中有 documentation_urls 列,你可能希望将其转换为 URL 数组:
use Filament\Actions\Imports\ImportColumn;
ImportColumn::make('documentation_urls')
->multiple(',')TIP
除了允许静态值外,multiple() 方法也接受一个函数来动态计算值。你可以将各种实用工具作为参数注入该函数。
在此示例中,我们传入逗号作为分隔符,因此列中的值会按逗号拆分并转换为数组。
转换数组中的每一项
若要将数组中的每一项转换为不同的数据类型,可以链式调用 内置转换方法:
use Filament\Actions\Imports\ImportColumn;
ImportColumn::make('customer_ratings')
->multiple(',')
->integer() // Casts each item in the array to an integer.验证数组中的每一项
若要验证数组中的每一项,可以链式调用 nestedRecursiveRules() 方法:
use Filament\Actions\Imports\ImportColumn;
ImportColumn::make('customer_ratings')
->multiple(',')
->integer()
->rules(['array'])
->nestedRecursiveRules(['integer', 'min:1', 'max:5'])TIP
除了允许静态值外,nestedRecursiveRules() 方法也接受一个函数来动态计算值。你可以将各种实用工具作为参数注入该函数。
导入关联关系
你可以使用 relationship() 方法导入关联关系。目前支持 BelongsTo 和 BelongsToMany 关系。例如,若 CSV 中有 category 列,你可能希望导入 category 的 BelongsTo 关系:
use Filament\Actions\Imports\ImportColumn;
ImportColumn::make('author')
->relationship()在此示例中,CSV 中的 author 列会映射到数据库中的 author_id 列。CSV 应包含作者的主键,通常是 id。
若该列有值但找不到作者,导入将验证失败。Filament 会自动为所有关联列添加验证,以确保在必填时关联不为空。
若要导入 BelongsToMany 关系,请确保该列设置了 multiple(),并使用正确的值分隔符:
use Filament\Actions\Imports\ImportColumn;
ImportColumn::make('authors')
->relationship()
->multiple(',')自定义关联导入解析
若要通过其他列查找关联记录,可以将列名作为 resolveUsing 传入:
use Filament\Actions\Imports\ImportColumn;
ImportColumn::make('author')
->relationship(resolveUsing: 'email')你可以向 resolveUsing 传入多个列,它们会以「或」的方式用于查找作者。例如,若传入 ['email', 'username'],则可通过邮箱或用户名找到记录:
use Filament\Actions\Imports\ImportColumn;
ImportColumn::make('author')
->relationship(resolveUsing: ['email', 'username'])你还可以通过向 resolveUsing 传入函数来自定义解析过程,该函数应返回要与关联关联的记录:
use App\Models\Author;
use Filament\Actions\Imports\ImportColumn;
ImportColumn::make('author')
->relationship(resolveUsing: function (string $state): ?Author {
return Author::query()
->where('email', $state)
->orWhere('username', $state)
->first();
})TIP
传给 resolveUsing 的函数允许你将各种实用工具作为参数注入。
若使用 BelongsToMany 关系,$state 将是数组,你应返回已解析的记录集合:
use App\Models\Author;
use Filament\Actions\Imports\ImportColumn;
use Illuminate\Database\Eloquent\Collection;
ImportColumn::make('authors')
->relationship(resolveUsing: function (array $state): Collection {
return Author::query()
->whereIn('email', $state)
->orWhereIn('username', $state)
->get();
})你甚至可以用此函数动态决定用哪些列来解析记录:
use App\Models\Author;
use Filament\Actions\Imports\ImportColumn;
ImportColumn::make('author')
->relationship(resolveUsing: function (string $state): ?Author {
if (filter_var($state, FILTER_VALIDATE_EMAIL)) {
return 'email';
}
return 'username';
})将列数据标记为敏感
当导入行验证失败时,它们会被记录到数据库,以便在导入完成后导出。你可能希望从该日志中排除某些列,以避免以纯文本存储敏感数据。为此,可以在 ImportColumn 上使用 sensitive() 方法,阻止其数据被记录:
use Filament\Actions\Imports\ImportColumn;
ImportColumn::make('ssn')
->label('Social security number')
->sensitive()
->rules(['required', 'digits:9'])自定义如何将列填充到记录
若要自定义如何将列状态填充到记录中,可以向 fillRecordUsing() 方法传入函数:
use App\Models\Product;
use Filament\Actions\Imports\ImportColumn;
ImportColumn::make('sku')
->fillRecordUsing(function (Product $record, string $state): void {
$record->sku = strtoupper($state);
})TIP
传给 fillRecordUsing() 方法的函数允许你将各种实用工具作为参数注入。
在导入列下方添加帮助文本
有时,你可能希望在验证前为用户提供额外信息。可以通过向列添加 helperText() 实现,它会显示在映射选择框下方:
use Filament\Actions\Imports\ImportColumn;
ImportColumn::make('skus')
->multiple(',')
->helperText('A comma-separated list of SKUs.')导入时更新已有记录
生成 importer 类时,你会看到此 resolveRecord() 方法:
use App\Models\Product;
public function resolveRecord(): ?Product
{
// return Product::firstOrNew([
// // Update existing records, matching them by `$this->data['column_name']`
// 'email' => $this->data['email'],
// ]);
return new Product();
}该方法会对 CSV 中的每一行调用,负责返回一个模型实例,该实例会用 CSV 数据填充并保存到数据库。默认情况下,它会为每一行创建新记录。不过,你可以自定义此行为以改为更新已有记录。例如,若产品已存在则更新,否则创建新记录。为此,可以取消注释 firstOrNew() 行,并传入要匹配的列名。对于产品,我们可能希望按 sku 列匹配:
use App\Models\Product;
public function resolveRecord(): ?Product
{
return Product::firstOrNew([
'sku' => $this->data['sku'],
]);
}仅更新已有记录的导入
若要编写仅更新已有记录、不创建新记录的 importer,可以在找不到记录时返回 null:
use App\Models\Product;
public function resolveRecord(): ?Product
{
return Product::query()
->where('sku', $this->data['sku'])
->first();
}若希望在找不到记录时使该导入行失败,可以抛出带消息的 RowImportFailedException:
use App\Models\Product;
use Filament\Actions\Imports\Exceptions\RowImportFailedException;
public function resolveRecord(): ?Product
{
$product = Product::query()
->where('sku', $this->data['sku'])
->first();
if (! $product) {
throw new RowImportFailedException("No product found with SKU [{$this->data['sku']}].");
}
return $product;
}导入完成后,用户可以下载包含错误消息的失败行 CSV。
忽略导入列的空白状态
默认情况下,若 CSV 中某列为空、且已被用户映射、且验证不要求必填,则该列会以 null 导入数据库。若希望忽略空白状态并改用数据库中的现有值,可以调用 ignoreBlankState() 方法:
use Filament\Actions\Imports\ImportColumn;
ImportColumn::make('price')
->ignoreBlankState()使用导入选项
导入操作可以渲染用户在导入 CSV 时可交互的额外表单组件。这有助于让用户自定义 importer 的行为。例如,你可能希望用户选择导入时是更新已有记录,还是仅创建新记录。为此,可以在 importer 类的 getOptionsFormComponents() 方法中返回选项表单组件:
use Filament\Forms\Components\Checkbox;
public static function getOptionsFormComponents(): array
{
return [
Checkbox::make('updateExisting')
->label('Update existing records'),
];
}或者,你可以通过操作上的 options() 方法向 importer 传入一组静态选项:
use App\Filament\Imports\ProductImporter;
use Filament\Actions\ImportAction;
ImportAction::make()
->importer(ProductImporter::class)
->options([
'updateExisting' => true,
])现在,你可以在 importer 类中通过调用 $this->options 访问这些选项的数据。例如,你可能希望在 resolveRecord() 中用它来 更新已有产品:
use App\Models\Product;
public function resolveRecord(): ?Product
{
if ($this->options['updateExisting'] ?? false) {
return Product::firstOrNew([
'sku' => $this->data['sku'],
]);
}
return new Product();
}改进导入列映射猜测
默认情况下,Filament 会尝试「猜测」CSV 中哪些列对应数据库中的哪些列,以节省用户时间。做法是尝试匹配列名的不同组合(含空格、-、_),且不区分大小写。若希望改进猜测,可以调用 guess() 方法,并传入 CSV 中可能出现的更多列名示例:
use Filament\Actions\Imports\ImportColumn;
ImportColumn::make('sku')
->guess(['id', 'number', 'stock-keeping unit'])提供示例 CSV 数据
在用户上传 CSV 之前,他们可以选择下载示例 CSV 文件,其中包含全部可导入列。这很有用,因为用户可以直接将该文件导入电子表格软件并填写。
你还可以向 CSV 添加示例行,向用户展示数据应是什么样子。要填充该示例行,可以向 example() 方法传入示例列值:
use Filament\Actions\Imports\ImportColumn;
ImportColumn::make('sku')
->example('ABC123')若要添加多行示例,可以向 examples() 方法传入数组:
use Filament\Actions\Imports\ImportColumn;
ImportColumn::make('sku')
->examples(['ABC123', 'DEF456'])默认情况下,示例 CSV 的表头使用列名。你可以使用 exampleHeader() 按列自定义表头:
use Filament\Actions\Imports\ImportColumn;
ImportColumn::make('sku')
->exampleHeader('SKU')使用自定义用户模型
默认情况下,imports 表有一个 user_id 列,该列约束到 users 表:
$table->foreignId('user_id')->constrained()->cascadeOnDelete();在 Import 模型中,user() 关系定义为到 App\Models\User 模型的 BelongsTo 关系。若 App\Models\User 模型不存在,或你想使用其他模型,可以在服务提供者的 register() 方法中将新的 Authenticatable 模型绑定到容器:
use App\Models\Admin;
use Illuminate\Contracts\Auth\Authenticatable;
$this->app->bind(Authenticatable::class, Admin::class);若你的可认证模型使用的不是 users 表,应将表名传给 constrained():
$table->foreignId('user_id')->constrained('admins')->cascadeOnDelete();使用多态用户关系
若要将导入与多个用户模型关联,可以改用多态 MorphTo 关系。为此,需要替换 imports 表中的 user_id 列:
$table->morphs('user');然后,在服务提供者的 boot() 方法中,应调用 Import::polymorphicUserRelationship(),将 Import 模型上的 user() 关系切换为 MorphTo 关系:
use Filament\Actions\Imports\Models\Import;
Import::polymorphicUserRelationship();限制可导入的最大行数
为防止服务器过载,你可能希望限制单个 CSV 文件可导入的最大行数。可以在操作上调用 maxRows() 方法:
use App\Filament\Imports\ProductImporter;
use Filament\Actions\ImportAction;
ImportAction::make()
->importer(ProductImporter::class)
->maxRows(100000)TIP
除了允许静态值外,maxRows() 方法也接受一个函数来动态计算值。你可以将各种实用工具作为参数注入该函数。
更改导入分块大小
Filament 会将 CSV 分块,并在不同的队列任务中处理每个分块。默认每次分块为 100 行。你可以在操作上调用 chunkSize() 方法更改:
use App\Filament\Imports\ProductImporter;
use Filament\Actions\ImportAction;
ImportAction::make()
->importer(ProductImporter::class)
->chunkSize(250)TIP
除了允许静态值外,chunkSize() 方法也接受一个函数来动态计算值。你可以将各种实用工具作为参数注入该函数。
TIP
若在导入大型 CSV 文件时遇到内存或超时问题,可以考虑减小分块大小。
更改 CSV 分隔符
CSV 的默认分隔符是逗号(,)。若导入使用其他分隔符,可以在操作上调用 csvDelimiter() 方法并传入新分隔符:
use App\Filament\Imports\ProductImporter;
use Filament\Actions\ImportAction;
ImportAction::make()
->importer(ProductImporter::class)
->csvDelimiter(';')TIP
除了允许静态值外,csvDelimiter() 方法也接受一个函数来动态计算值。你可以将各种实用工具作为参数注入该函数。
只能指定单个字符,否则会抛出异常。
更改列标题偏移
若列标题不在 CSV 的第一行,可以在操作上调用 headerOffset() 方法,并传入要跳过的行数:
use App\Filament\Imports\ProductImporter;
use Filament\Actions\ImportAction;
ImportAction::make()
->importer(ProductImporter::class)
->headerOffset(5)TIP
除了允许静态值外,headerOffset() 方法也接受一个函数来动态计算值。你可以将各种实用工具作为参数注入该函数。
自定义完成通知
导入完成后,Filament 会向启动导入的用户发送通知。你可以通过在 importer 上覆盖 getCompletedNotificationTitle() 和 getCompletedNotificationBody() 来自定义通知的标题和正文:
use Filament\Actions\Imports\Models\Import;
public static function getCompletedNotificationTitle(Import $import): string
{
return 'Your product import has finished';
}
public static function getCompletedNotificationBody(Import $import): string
{
return $import->successful_rows . ' products were imported.';
}若要自定义标题和正文以外的内容——例如更改通知颜色、添加额外操作或替换图标——请覆盖 modifyCompletedNotification()。你可以修改传入的 Notification 并返回它,也可以构建并返回一个全新的通知:
use Filament\Actions\Action;
use Filament\Actions\Imports\Models\Import;
use Filament\Notifications\Notification;
public static function modifyCompletedNotification(Notification $notification, Import $import): Notification
{
$notification->icon('heroicon-o-shopping-bag');
if ($import->getOptions()['sendWelcomeEmails'] ?? false) {
$notification->actions([
...$notification->getActions(),
Action::make('viewWelcomeEmails')
->url(route('emails.sent')),
]);
}
return $notification;
}Import 模型通过 $import->getColumnMap() 和 $import->getOptions() 暴露用户选择的列映射和选项,因此你可以根据用户导入的内容定制通知。
自定义失败行的下载方式
默认情况下,失败行会编译成 CSV 并以流式响应返回。你可以通过覆盖 getFailedRowsDownloader() 方法,自定义 importer 的下载方式:
use App\Filament\Imports\Downloaders\CustomFailedRowsDownloader;
use Filament\Actions\Imports\Downloaders\Contracts\Downloader;
public static function getFailedRowsDownloader(): Downloader
{
return app(CustomFailedRowsDownloader::class);
}Downloader 是一个可调用类,接受 Import 模型并返回 Symfony Response。该响应可以流式下载、返回文件,或将用户重定向到远程文件系统上的临时 URL。
若自定义 downloader 仅更改生成内容的交付方式,可以使用 CsvImportFailureContentGenerator 将失败行写入 League CSV Writer。Filament 会从容器解析该类,以便你复用内置内容生成而无需重复实现。
自定义导入任务
处理导入的默认任务是 Filament\Actions\Imports\Jobs\ImportCsv。若要扩展该类并覆盖其任何方法,可以在服务提供者的 register() 方法中替换原始类:
use App\Jobs\ImportCsv;
use Filament\Actions\Imports\Jobs\ImportCsv as BaseImportCsv;
$this->app->bind(BaseImportCsv::class, ImportCsv::class);或者,你可以将新的任务类传给操作上的 job() 方法,以自定义特定导入的任务:
use App\Filament\Imports\ProductImporter;
use App\Jobs\ImportCsv;
use Filament\Actions\ImportAction;
ImportAction::make()
->importer(ProductImporter::class)
->job(ImportCsv::class)自定义导入队列与连接
默认情况下,导入系统使用默认队列和连接。若要自定义某个 importer 的任务所用队列,可以在 importer 类中覆盖 getJobQueue() 方法:
public function getJobQueue(): ?string
{
return 'imports';
}你也可以通过在 importer 类中覆盖 getJobConnection() 方法,自定义某个 importer 的任务所用连接:
public function getJobConnection(): ?string
{
return 'sqs';
}自定义导入任务中间件
默认情况下,导入系统对每次导入一次只处理一个任务。这是为了防止服务器过载,以及避免其他任务被大型导入拖慢。该功能在 importer 类上的 WithoutOverlapping 中间件中定义:
use Illuminate\Queue\Middleware\WithoutOverlapping;
public function getJobMiddleware(): array
{
return [
(new WithoutOverlapping("import{$this->import->getKey()}"))->expireAfter(600),
];
}若要自定义某个 importer 任务所应用的中间件,可以在 importer 类中覆盖此方法。关于任务中间件的更多信息,请参阅 Laravel 文档。
自定义导入任务重试
默认情况下,导入系统会在 24 小时内重试任务,或直到因未处理异常失败 5 次(以先发生者为准)。这是为了让数据库不可用等临时问题有时间解决。你可以更改任务重试的时间段,该时间段在 importer 类的 getJobRetryUntil() 方法中定义:
use Carbon\CarbonInterface;
public function getJobRetryUntil(): ?CarbonInterface
{
return now()->addHours(12);
}关于任务重试的更多信息,请参阅 Laravel 文档。
自定义导入任务退避策略
默认情况下,导入系统会在重试任务前等待 1 分钟、然后 2 分钟、然后 5 分钟、之后每次 10 分钟。这是为了防止反复失败的任务过载服务器。该功能在 importer 类的 getJobBackoff() 方法中定义:
/**
* @return int | array<int> | null
*/
public function getJobBackoff(): int | array | null
{
return [60, 120, 300, 600];
}关于任务退避(包括如何配置指数退避)的更多信息,请参阅 Laravel 文档。
自定义导入任务标签
默认情况下,导入系统会用导入 ID 标记每个任务。这便于你轻松找到与某次导入相关的所有任务。该功能在 importer 类的 getJobTags() 方法中定义:
public function getJobTags(): array
{
return ["import{$this->import->getKey()}"];
}若要自定义某个 importer 任务所应用的标签,可以在 importer 类中覆盖此方法。
自定义导入任务批处理名称
默认情况下,导入系统不会为任务批处理定义任何名称。若要自定义某个 importer 的任务批处理名称,可以在 importer 类中覆盖 getJobBatchName() 方法:
public function getJobBatchName(): ?string
{
return 'product-import';
}自定义导入验证消息
导入系统会在导入前自动验证 CSV 文件。若有任何错误,会向用户显示错误列表,且不会处理导入。若要覆盖任何默认验证消息,可以在 importer 类上覆盖 getValidationMessages() 方法:
public function getValidationMessages(): array
{
return [
'name.required' => 'The name column must not be empty.',
];
}要了解有关自定义验证消息的更多信息,请阅读 Laravel 文档。
自定义导入验证属性
当列验证失败时,错误消息会使用其标签。若要自定义字段错误消息中使用的标签,请使用 validationAttribute() 方法:
use Filament\Actions\Imports\ImportColumn;
ImportColumn::make('name')
->validationAttribute('full name')自定义导入文件验证
你可以使用 fileRules() 方法为导入文件添加新的 Laravel 验证规则:
use Filament\Actions\ImportAction;
use Illuminate\Validation\Rules\File;
ImportAction::make()
->importer(ProductImporter::class)
->fileRules([
'max:1024',
// or
File::types(['csv', 'txt'])->max(1024),
]),TIP
除了允许静态值外,fileRules() 方法也接受一个函数来动态计算值。你可以将各种实用工具作为参数注入该函数。
生命周期钩子
可以使用钩子在 importer 生命周期的不同节点执行代码,例如在记录保存之前。要设置钩子,请在 importer 类上创建与钩子同名的受保护方法:
protected function beforeSave(): void
{
// ...
}在此示例中,beforeSave() 方法中的代码会在将已验证的 CSV 数据保存到数据库之前调用。
Importer 有若干可用钩子:
use Filament\Actions\Imports\Importer;
class ProductImporter extends Importer
{
// ...
protected function beforeValidate(): void
{
// Runs before the CSV data for a row is validated.
}
protected function afterValidate(): void
{
// Runs after the CSV data for a row is validated.
}
protected function beforeFill(): void
{
// Runs before the validated CSV data for a row is filled into a model instance.
}
protected function afterFill(): void
{
// Runs after the validated CSV data for a row is filled into a model instance.
}
protected function beforeSave(): void
{
// Runs before a record is saved to the database.
}
protected function beforeCreate(): void
{
// Similar to `beforeSave()`, but only runs when creating a new record.
}
protected function beforeUpdate(): void
{
// Similar to `beforeSave()`, but only runs when updating an existing record.
}
protected function afterSave(): void
{
// Runs after a record is saved to the database.
}
protected function afterCreate(): void
{
// Similar to `afterSave()`, but only runs when creating a new record.
}
protected function afterUpdate(): void
{
// Similar to `afterSave()`, but only runs when updating an existing record.
}
}在 trait 中定义生命周期钩子
若要在 trait 中定义生命周期钩子,请在钩子名称后加上 trait 名称作为后缀。这遵循 Eloquent 使用的 boot{TraitName}() 约定以及 Livewire 使用的 mount{TraitName}() 约定,使可复用的 trait 能接入 importer 生命周期,而不会与 importer 自身定义的钩子冲突:
use Filament\Actions\Imports\Importer;
trait LogsImports
{
protected function afterSaveLogsImports(): void
{
// Runs after a record is saved to the database, in addition to the
// hook on the importer.
}
}
class ProductImporter extends Importer
{
use LogsImports;
protected function afterSave(): void
{
// Both lifecycle hooks are called.
}
}先调用 importer 自身的钩子,然后是每个 trait 钩子。被其他 trait 使用的 trait 中的钩子也会被调用。Trait 钩子会自动调用,因此你不应再从 importer 自身的钩子中调用它们。
在这些钩子中,你可以使用 $this->data 访问当前行的数据。还可以使用 $this->originalData 访问 CSV 中在 转换 或映射之前的原始行数据。
当前记录(若已存在)可在 $this->record 中访问,导入表单选项 可通过 $this->options 访问。
授权
默认情况下,仅启动导入的用户可以访问导入部分失败时生成的失败 CSV 文件。若要自定义授权逻辑,可以创建 ImportPolicy 类,并 在 AuthServiceProvider 中注册:
use App\Policies\ImportPolicy;
use Filament\Actions\Imports\Models\Import;
protected $policies = [
Import::class => ImportPolicy::class,
];策略的 view() 方法将用于授权访问失败 CSV 文件。
请注意,若你定义了策略,原先「仅启动导入的用户可访问失败 CSV」的逻辑将被移除。若要保留该逻辑,需要将其加入你的策略:
use App\Models\User;
use Filament\Actions\Imports\Models\Import;
public function view(User $user, Import $import): bool
{
return $import->user()->is($user);
}安全性
按记录授权
导入系统在创建或更新记录时不会执行按记录的授权检查。CSV 中的每一行都会由 importer 的 resolveRecord()、fillRecord() 和 saveRecord() 方法处理,而不会咨询应用的 Laravel 策略。这意味着若用户被允许触发导入,他们就可以创建或更新 importer 支持的任何记录,无论他们是否通常有权通过应用 UI 这样做。
若导入时需要按记录授权,应在 importer 的 生命周期钩子(例如 beforeCreate() 或 beforeUpdate())中添加检查,以根据记录授权当前用户。
DANGER
若应用允许不受信任的用户触发导入,你应实现按记录的授权检查,以防止未经授权的记录创建或修改。
CSV 公式注入
导入时行验证失败后,Filament 会将它们编译成可下载的 CSV 供用户审阅。该失败 CSV 包含上传文件中的原始数据,完全按提交时的样子,不做任何转换。若上传的 CSV 包含以 =、+、- 或 @ 等字符开头的值,它们会原样出现在失败 CSV 中。在 Microsoft Excel 或 Google Sheets 等电子表格软件中打开时,这些值可能被解释为公式;若原始 CSV 来自不受信任的来源,这可能构成安全风险。你应确保用户在审阅失败 CSV 时了解此风险,或在 importer 的生命周期钩子中实现清理,在将潜在危险值存储为失败行之前将其中和。
或者,你可以选择启用 Filament 对失败 CSV 的内置保护。启用后,任何以公式触发字符(=、+、-、@、制表符或回车符)开头的单元格都会加上单引号(')前缀,使电子表格软件将其视为纯文本。由于失败 CSV 包含上传文件的每一列——即使未映射到 ImportColumn 的列——此保护会应用于整个文件,而不是单个列。
若要为单个 importer 启用,请在 importer 类上设置 $shouldPreventFormulaInjection 属性:
use Filament\Actions\Imports\Importer;
class ProductImporter extends Importer
{
protected static bool $shouldPreventFormulaInjection = true;
}若要为应用中的每个 importer 启用,请在服务提供者的 boot() 方法中调用 preventFormulaInjection() 方法:
use Filament\Actions\Imports\Importer;
Importer::preventFormulaInjection();WARNING
此保护为 选择性启用,默认关闭,因为失败 CSV 本意是供修正后重新上传。加上单引号前缀会改变合法数据——例如 -5 或电话号码 +44 1234 567890 都是有效的公式触发值,会被改写为 '-5 和 '+44 1234 567890,随后导入时会保留前导引号。仅当上传文件可能来自不受信任的来源时再启用。