filament/actions
Filament Actions adds reusable, customizable UI actions to Filament admin panels. Define buttons, modals, confirmations, and forms with a fluent API, then run callbacks, validations, and notifications consistently across tables, resources, and pages.
title: Import action
import Aside from "@components/Aside.astro" import AutoScreenshot from "@components/AutoScreenshot.astro" import UtilityInjection from "@components/UtilityInjection.astro"
Filament includes an action that is able to import rows from a CSV. When the trigger button is clicked, a modal asks the user for a file. Once they upload one, they are able to map each column in the CSV to a real column in the database. If any rows fail validation, they will be compiled into a downloadable CSV for the user to review after the rest of the rows have been imported. Users can also download an example CSV file containing all the columns that can be imported.
This feature uses job batches and database notifications, so you need to publish those migrations from Laravel. Also, you need to publish the migrations for tables that Filament uses to store information about imports:
php artisan make:queue-batches-table
php artisan make:notifications-table
php artisan vendor:publish --tag=filament-actions-migrations
php artisan migrate
If you'd like to receive import notifications in a panel, you can enable them in the panel configuration.
You may use the ImportAction like so:
use App\Filament\Imports\ProductImporter;
use Filament\Actions\ImportAction;
ImportAction::make()
->importer(ProductImporter::class)
If you want to add this action to the header of a table, you may do so like this:
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)
]);
}
The "importer" class needs to be created to tell Filament how to import each row of the CSV.
If you have more than one ImportAction in the same place, you should give each a unique name in the make() method:
use Filament\Actions\ImportAction;
ImportAction::make('importProducts')
->importer(ProductImporter::class)
ImportAction::make('importBrands')
->importer(BrandImporter::class)
To create an importer class for a model, you may use the make:filament-importer command, passing the name of a model:
php artisan make:filament-importer Product
This will create a new class in the app/Filament/Imports directory. You now need to define the columns that can be imported.
If you'd like to save time, Filament can automatically generate the columns for you, based on your model's database columns, using --generate:
php artisan make:filament-importer Product --generate
To define the columns that can be imported, you need to override the getColumns() method on your importer class, returning an array of ImportColumn objects:
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']),
];
}
The label for each column will be generated automatically from its name, but you can override it by calling the label() method:
use Filament\Actions\Imports\ImportColumn;
ImportColumn::make('sku')
->label('SKU')
You can call the requiredMapping() method to make a column required to be mapped to a column in the CSV. Columns that are required in the database should be required to be mapped:
use Filament\Actions\Imports\ImportColumn;
ImportColumn::make('sku')
->requiredMapping()
If you require a column in the database, you also need to make sure that it has a rules(['required']) validation rule.
If a column is not mapped, it will not be validated since there is no data to validate.
If you allow an import to create records as well as update existing ones, but only require a column to be mapped when creating records as it's a required field, you can use the requiredMappingForNewRecordsOnly() method instead of requiredMapping():
use Filament\Actions\Imports\ImportColumn;
ImportColumn::make('sku')
->requiredMappingForNewRecordsOnly()
If the resolveRecord() method returns a model instance that is not saved in the database yet, the column will be required to be mapped, just for that row. If the user does not map the column, and one of the rows in the import does not yet exist in the database, just that row will fail and a message will be added to the failed rows CSV after every row has been analyzed.
You can call the rules() method to add validation rules to a column. These rules will check the data in each row from the CSV before it is saved to the database:
use Filament\Actions\Imports\ImportColumn;
ImportColumn::make('sku')
->rules(['required', 'max:32'])
Any rows that do not pass validation will not be imported. Instead, they will be compiled into a new CSV of "failed rows", which the user can download after the import has finished. The user will be shown a list of validation errors for each row that failed.
<UtilityInjection set="importColumns" version="5.x">As well as allowing a static value, the rules() method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
Before validation, data from the CSV can be cast. This is useful for converting strings into the correct data type, otherwise validation may fail. For example, if you have a price column in your CSV, you may want to cast it to a 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);
})
<UtilityInjection set="importColumns" version="5.x" extras="State;;mixed;;$state;;The state to cast, after it has been processed by other casting methods.||Original state;;mixed;;$originalState;;The state to cast, before it was processed by other casting methods.">As well as $state, the castStateUsing() method allows you to inject various utilities into the function as parameters.</UtilityInjection>
In this example, we pass in a function that is used to cast the $state. This function removes any non-numeric characters from the string, casts it to a float, and rounds it to two decimal places.
Filament also ships with some built-in casting methods:
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.
If you're using a built-in casting method or array cast, you can mutate the state after it has been cast by passing a function to the castStateUsing() method:
use Filament\Actions\Imports\ImportColumn;
ImportColumn::make('price')
->numeric()
->castStateUsing(function (float $state): ?float {
if (blank($state)) {
return null;
}
return round($state * 100);
})
You can even access the original state before it was cast, by defining an $originalState argument in the function:
use Filament\Actions\Imports\ImportColumn;
ImportColumn::make('price')
->numeric()
->castStateUsing(function (float $state, mixed $originalState): ?float {
// ...
})
<UtilityInjection set="importColumns" version="5.x" extras="State;;mixed;;$state;;The state to cast, after it has been processed by other casting methods.||Original state;;mixed;;$originalState;;The state to cast, before it was processed by other casting methods.">As well as $state, the castStateUsing() method allows you to inject various utilities into the function as parameters.</UtilityInjection>
You may use the multiple() method to cast the values in a column to an array. It accepts a delimiter as its first argument, which is used to split the values in the column into an array. For example, if you have a documentation_urls column in your CSV, you may want to cast it to an array of URLs:
use Filament\Actions\Imports\ImportColumn;
ImportColumn::make('documentation_urls')
->multiple(',')
<UtilityInjection set="importColumns" version="5.x">As well as allowing a static value, the multiple() method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
In this example, we pass in a comma as the delimiter, so the values in the column will be split by commas, and cast to an array.
If you want to cast each item in the array to a different data type, you can chain the built-in casting methods:
use Filament\Actions\Imports\ImportColumn;
ImportColumn::make('customer_ratings')
->multiple(',')
->integer() // Casts each item in the array to an integer.
If you want to validate each item in the array, you can chain the nestedRecursiveRules() method:
use Filament\Actions\Imports\ImportColumn;
ImportColumn::make('customer_ratings')
->multiple(',')
->integer()
->rules(['array'])
->nestedRecursiveRules(['integer', 'min:1', 'max:5'])
<UtilityInjection set="importColumns" version="5.x">As well as allowing a static value, the nestedRecursiveRules() method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
You may use the relationship() method to import a relationship. At the moment, BelongsTo and BelongsToMany relationships are supported. For example, if you have a category column in your CSV, you may want to import the category BelongsTo relationship:
use Filament\Actions\Imports\ImportColumn;
ImportColumn::make('author')
->relationship()
In this example, the author column in the CSV will be mapped to the author_id column in the database. The CSV should contain the primary keys of authors, usually id.
If the column has a value, but the author cannot be found, the import will fail validation. Filament automatically adds validation to all relationship columns, to ensure that the relationship is not empty when it is required.
If you want to import a BelongsToMany relationship, make sure that the column is set to multiple(), with the correct separator between values:
use Filament\Actions\Imports\ImportColumn;
ImportColumn::make('authors')
->relationship()
->multiple(',')
If you want to find a related record using a different column, you can pass the column name as resolveUsing:
use Filament\Actions\Imports\ImportColumn;
ImportColumn::make('author')
->relationship(resolveUsing: 'email')
You can pass in multiple columns to resolveUsing, and they will be used to find the author, in an "or" fashion. For example, if you pass in ['email', 'username'], the record can be found by either their email or username:
use Filament\Actions\Imports\ImportColumn;
ImportColumn::make('author')
->relationship(resolveUsing: ['email', 'username'])
You can also customize the resolution process, by passing in a function to resolveUsing, which should return a record to associate with the relationship:
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();
})
<UtilityInjection set="importColumns" version="5.x" extras="State;;mixed;;$state;;The state to resolve into a record.">The function passed to resolveUsing allows you to inject various utilities into the function as parameters.</UtilityInjection>
If you are using a BelongsToMany relationship, the $state will be an array, and you should return a collection of records that you have resolved:
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();
})
You could even use this function to dynamically determine which columns to use to resolve the record:
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';
})
When import rows fail validation, they are logged to the database, ready for export when the import completes. You may want to exclude certain columns from this logging to avoid storing sensitive data in plain text. To achieve this, you can use the sensitive() method on the ImportColumn to prevent its data from being logged:
use Filament\Actions\Imports\ImportColumn;
ImportColumn::make('ssn')
->label('Social security number')
->sensitive()
->rules(['required', 'digits:9'])
If you want to customize how column state is filled into a record, you can pass a function to the fillRecordUsing() method:
use App\Models\Product;
use Filament\Actions\Imports\ImportColumn;
ImportColumn::make('sku')
->fillRecordUsing(function (Product $record, string $state): void {
$record->sku = strtoupper($state);
})
<UtilityInjection set="importColumns" version="5.x" extras="State;;mixed;;$state;;The state to fill into the record.">The function passed to the fillRecordUsing() method allows you to inject various utilities into the function as parameters.</UtilityInjection>
Sometimes, you may wish to provide extra information for the user before validation. You can do this by adding helperText() to a column, which gets displayed below the mapping select:
use Filament\Actions\Imports\ImportColumn;
ImportColumn::make('skus')
->multiple(',')
->helperText('A comma-separated list of SKUs.')
When generating an importer class, you will see this resolveRecord() method:
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();
}
This method is called for each row in the CSV, and is responsible for returning a model instance that will be filled with the data from the CSV, and saved to the database. By default, it will create a new record for each row. However, you can customize this behavior to update existing records instead. For example, you might want to update a product if it already exists, and create a new one if it doesn't. To do this, you can uncomment the firstOrNew() line, and pass the column name that you want to match on. For a product, we might want to match on the sku column:
use App\Models\Product;
public function resolveRecord(): ?Product
{
return Product::firstOrNew([
'sku' => $this->data['sku'],
]);
}
If you want to write an importer that only updates existing records, and does not create new ones, you can return null if no record is found:
use App\Models\Product;
public function resolveRecord(): ?Product
{
return Product::query()
->where('sku', $this->data['sku'])
->first();
}
If you'd like to fail the import row if no record is found, you can throw a RowImportFailedException with a message:
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;
}
When the import is completed, the user will be able to download a CSV of failed rows, which will contain the error messages.
By default, if a column in the CSV is blank, and mapped by the user, and it's not required by validation, the column will be imported as null in the database. If you'd like to ignore blank state, and use the existing value in the database instead, you can call the ignoreBlankState() method:
use Filament\Actions\Imports\ImportColumn;
ImportColumn::make('price')
->ignoreBlankState()
The import action can render extra form components that the user can interact with when importing a CSV. This can be useful to allow the user to customize the behavior of the importer. For instance, you might want a user to be able to choose whether to update existing records when importing, or only create new ones. To do this, you can return options form components from the getOptionsFormComponents() method on your importer class:
use Filament\Forms\Components\Checkbox;
public static function getOptionsFormComponents(): array
{
return [
Checkbox::make('updateExisting')
->label('Update existing records'),
];
}
Alternatively, you can pass a set of static options to the importer through the options() method on the action:
use App\Filament\Imports\ProductImporter;
use Filament\Actions\ImportAction;
ImportAction::make()
->importer(ProductImporter::class)
->options([
'updateExisting' => true,
])
Now, you can access the data from these options inside the importer class, by calling $this->options. For example, you might want to use it inside resolveRecord() to update an existing product:
use App\Models\Product;
public function resolveRecord(): ?Product
{
if ($this->options['updateExisting'] ?? false) {
return Product::firstOrNew([
'sku' => $this->data['sku'],
]);
}
return new Product();
}
By default, Filament will attempt to "guess" which columns in the CSV match which columns in the database, to save the user time. It does this by attempting to find different combinations of the column name, with spaces, -, _, all cases insensitively. However, if you'd like to improve the guesses, you can call the guess() method with more examples of the column name that could be present in the CSV:
use Filament\Actions\Imports\ImportColumn;
ImportColumn::make('sku')
->guess(['id', 'number', 'stock-keeping unit'])
Before the user uploads a CSV, they have an option to download an example CSV file, containing all the available columns that can be imported. This is useful, as it allows the user to import this file directly into their spreadsheet software, and fill it out.
You can also add an example row to the CSV, to show the user what the data should look like. To fill in this example row, you can pass in an example column value to the example() method:
use Filament\Actions\Imports\ImportColumn;
ImportColumn::make('sku')
->example('ABC123')
Or if you want to add more than one example row, y.....
How can I help you explore Laravel packages today?