导入操作
简介
Filament 引入了一个可以从 CSV 导入行数据的 Action。点击触发按钮后,模态框向用户请求文件。文件上传后,可以将 CSV 中的每一列映射到数据库的列字段中。如果有任何一行验证失败,这些行数据将会在其他行导入后,被编译到一个可下载的 CSV 中,供用户进行查阅。用户也可以下载包含可被导入的所有列信息的 CSV 示例文件。
该特性使用了队列批量操作及数据库通知,因此你需要发布这些迁移到 Laravel 中。同时,也需要发布 Filament 用于存储导入数据的迁移表:
# Laravel 11 and higher
php artisan make:queue-batches-table
php artisan make:notifications-table
# Laravel 10
php artisan queue:batches-table
php artisan notifications:table
# All apps
php artisan vendor:publish --tag=filament-actions-migrations
php artisan migrate
NOTE
如果你使用的是 PostgreSQL,请确保通知迁移中的 data 字段使用 json(): $table->json('data')。
NOTE
如果 User 模型使用了 UUID,请确保通知迁移的 notifiable 字段使用 uuidMorphs(): $table->uuidMorphs('notifiable')
ImportAction 可以像这样使用:
use App\Filament\Imports\ProductImporter;
use Filament\Actions\ImportAction;
ImportAction::make()
->importer(ProductImporter::class)
如果你想将该 Action 添加到表头,可以这样使用:
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() 方法中为每个 Action 都指定一个唯一的名称:
use Filament\Actions\ImportAction;
ImportAction::make('importProducts')
->importer(ProductImporter::class)
ImportAction::make('importBrands')
->importer(BrandImporter::class)
创建导入器
要为模型创建 importer 类,可以使用 make:filament-importer 命令,并传入模型名称:
php artisan make:filament-importer Product
该命令将在 app/Filament/Imports 目录下创建一个新类。你需要定义可被导入的列字段。
自动生成导入器字段
如果你想节省时间,Filament 可以使用 --generate,基于模型的数据库字段,为你自动生成列字段:
php artisan make:filament-importer Product --generate
定义导入器列字段
要定义可被导入的列,你需要在导入器类中重写 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')
要求导入器列映射到 CSV 字段
你可以调用 requiredMapping() 方法,使得列字段映射到 CSV 中的变成必需。数据库中必需的字段也应该是映射时必需的:
use Filament\Actions\Imports\ImportColumn;
ImportColumn::make('sku')
->requiredMapping()
如果字段在数据库中是必需项,你也需要确保它的验证规则有 ['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,用户可以在导入完成后下载。用户将看到验证失败的每一行的错误列表。
除了允许静态值之外,rules() 方法也可以接受函数动态计算其值。你可以将各种 utility 作为参数注入到函数中。
| Utility | Type | Parameter | Description |
|---|---|---|---|
| Import column | Filament\Actions\Imports\ImportColumn | $column | The current import column instance. |
| Data | array<string, mixed> | $data | The processed data for the record that is currently being imported. |
| Importer | ?Filament\Actions\Imports\Importer | $importer | The instance of the importer class that is currently being used for importing data. |
| Options | array<string, mixed> | $options | The options that were defined when the import started. |
| Original data | array<string, mixed> | $originalData | The original data for the record that is currently being imported, before it was processed. |
| Eloquent record | ?Illuminate\Database\Eloquent\Model | $record | The Eloquent record that is currently being imported. |
强制转换状态
在验证之前,CSV 数据可以被转换。这对于将字符串强制转换为正确的数据类型很有用,否则验证可能会失败。比如,如果你的 CSV 中有一个 price 字段,你可能想将其强制转换成浮点型:
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);
})
除了 $state之外,castStateUsing() 方法允许你将各种 utility 作为参数注入到函数中。
| Utility | Type | Parameter | Description |
|---|---|---|---|
| Import column | Filament\Actions\Imports\ImportColumn | $column | The current import column instance. |
| Data | array<string, mixed> | $data | The processed data for the record that is currently being imported. |
| Importer | ?Filament\Actions\Imports\Importer | $importer | The instance of the importer class that is currently being used for importing data. |
| Options | array<string, mixed> | $options | The options that were defined when the import started. |
| Original data | array<string, mixed> | $originalData | The original data for the record that is currently being imported, before it was processed. |
| Original state | mixed | $originalState | The state to cast, before it was processed by other casting methods. |
| Eloquent record | ?Illuminate\Database\Eloquent\Model | $record | The Eloquent record that is currently being imported. |
| State | mixed | $state | The state to cast, after it has been processed by other casting methods. |
本例中,我们传入一个用于强制转换 $state 的函数。此函数从字符串中删除任何非数字字符,将其强制转换为浮点值,并将其四舍五入到小数点后两位。
NOTE
请注意,如果字段在验证中不是必需的,且字段为空,它不会被转换。
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.
强制转换后修改状态
如果你使用了内置强制转换方法或者数组强制转换,你可以传入一个函数到 castStateUsing() 方法,在转换后改变其状态:
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 {
// ...
})
除了 $state之外,castStateUsing() 方法允许你将各种 utility 作为参数注入到函数中。
| Utility | Type | Parameter | Description |
|---|---|---|---|
| Import column | Filament\Actions\Imports\ImportColumn | $column | The current import column instance. |
| Data | array<string, mixed> | $data | The processed data for the record that is currently being imported. |
| Importer | ?Filament\Actions\Imports\Importer | $importer | The instance of the importer class that is currently being used for importing data. |
| Options | array<string, mixed> | $options | The options that were defined when the import started. |
| Original data | array<string, mixed> | $originalData | The original data for the record that is currently being imported, before it was processed. |
| Original state | mixed | $originalState | The state to cast, before it was processed by other casting methods. |
| Eloquent record | ?Illuminate\Database\Eloquent\Model | $record | The Eloquent record that is currently being imported. |
| State | mixed | $state | The state to cast, after it has been processed by other casting methods. |
在单列中处理多个值
你可以使用 multiple() 方法,将一个列的值强制转换成一个数组。它接受分隔符作为第一个参数,用于将列中的值拆分为数组。例如,如果你的 CSV 中有一个 documentation_urls 列,你可能希望将其强制转换为 URL 数组:
use Filament\Actions\Imports\ImportColumn;
ImportColumn::make('documentation_urls')
->multiple(',')
除了允许静态值之外,multiple() 方法也可以接受函数动态计算其值。你可以将各种 utility 作为参数注入到函数中。
| Utility | Type | Parameter | Description |
|---|---|---|---|
| Import column | Filament\Actions\Imports\ImportColumn | $column | The current import column instance. |
| Data | array<string, mixed> | $data | The processed data for the record that is currently being imported. |
| Importer | ?Filament\Actions\Imports\Importer | $importer | The instance of the importer class that is currently being used for importing data. |
| Options | array<string, mixed> | $options | The options that were defined when the import started. |
| Original data | array<string, mixed> | $originalData | The original data for the record that is currently being imported, before it was processed. |
| Eloquent record | ?Illuminate\Database\Eloquent\Model | $record | The Eloquent record that is currently being imported. |
本例中,我们传入一个逗号作为分隔符,因此列中的值将用逗号分隔,并强制转换为数组。
在数组中强制转换每一项
如果想在数组将每一项强制转换成不同的数据类型,你可以链式调用内置强制转换方法:
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'])
除了允许静态值之外,nestedRecursiveRules() 方法也可以接受函数动态计算其值。你可以将各种 utility 作为参数注入到函数中。
| Utility | Type | Parameter | Description |
|---|---|---|---|
| Import column | Filament\Actions\Imports\ImportColumn | $column | The current import column instance. |
| Data | array<string, mixed> | $data | The processed data for the record that is currently being imported. |
| Importer | ?Filament\Actions\Imports\Importer | $importer | The instance of the importer class that is currently being used for importing data. |
| Options | array<string, mixed> | $options | The options that were defined when the import started. |
| Original data | array<string, mixed> | $originalData | The original data for the record that is currently being imported, before it was processed. |
| Eloquent record | ?Illuminate\Database\Eloquent\Model | $record | The Eloquent record that is currently being imported. |
导入关联
你可以使用 relationship() 方法导入关联。目前只支持 BelongsTo 和 BelongsToMany 关联。比如,你的 CSV 中有一个 category 字段,你可能希望导入 category 的 BelongsTo 关联:
use Filament\Actions\Imports\ImportColumn;
ImportColumn::make('author')
->relationship()
本例中,CSV 中的 author 列映射到数据库中的 author_id 字段。该 CSV 中应该包含 author 的主键,通常为 id。
如果李忠有值,但找不到作者(author),则导入将无法通过验证。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 中,这些列使用 "or" 方式查找作者。比如,如果你传入 ['email', 'username'],将通过邮箱(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();
})
The function passed to resolveUsing allows you to inject various utilities into the function as parameters.
| Utility | Type | Parameter | Description |
|---|---|---|---|
| Import column | Filament\Actions\Imports\ImportColumn | $column | The current import column instance. |
| Data | array<string, mixed> | $data | The processed data for the record that is currently being imported. |
| Importer | ?Filament\Actions\Imports\Importer | $importer | The instance of the importer class that is currently being used for importing data. |
| Options | array<string, mixed> | $options | The options that were defined when the import started. |
| Original data | array<string, mixed> | $originalData | The original data for the record that is currently being imported, before it was processed. |
| Eloquent record | ?Illuminate\Database\Eloquent\Model | $record | The Eloquent record that is currently being imported. |
| State | mixed | $state | The state to resolve into a record. |
如果你使用了 BelongsToMany 关联, $state 将为一个数组,这种情况下,你应该返回已经解析的记录集合:
use App\Models\Author;
use Filament\Actions\Imports\ImportColumn;
use Illuminate\Database\Eloquent\Collection;
ImportColumn::make('authors')
->relationship(resolveUsing: function (array $states): Collection {
return Author::query()
->whereIn('email', $states)
->orWhereIn('username', $states)
->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);
})
The function passed to the fillRecordUsing() 方法允许你将各种 utility 作为参数注入到函数中。
| Utility | Type | Parameter | Description |
|---|---|---|---|
| Import column | Filament\Actions\Imports\ImportColumn | $column | The current import column instance. |
| Data | array<string, mixed> | $data | The processed data for the record that is currently being imported. |
| Importer | ?Filament\Actions\Imports\Importer | $importer | The instance of the importer class that is currently being used for importing data. |
| Options | array<string, mixed> | $options | The options that were defined when the import started. |
| Original data | array<string, mixed> | $originalData | The original data for the record that is currently being imported, before it was processed. |
| Eloquent record | ?Illuminate\Database\Eloquent\Model | $record | The Eloquent record that is currently being imported. |
| State | mixed | $state | The state to fill into the record. |
在导入列下面添加帮助文本
有时,你可能希望在验证之前提供额外的信息给用户。你可以使用 helperText() 方法添加帮助文本,这些文本将在映射的下拉列表下方显示
use Filament\Actions\Imports\ImportColumn;
ImportColumn::make('skus')
->multiple(',')
->helperText('A comma-separated list of SKUs.')
导入时更新现有记录
生成导入类时,你将会看到其中有一个 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'],
]);
}