Filament heeft import en export van CSV ingebouwd via ImportAction en ExportAction. Je definieert per model één importer- of exporterclass, Filament verwerkt het bestand in queue-jobs van standaard 100 rijen en levert een CSV met mislukte rijen terug. Export kan ook naar XLSX. Import van XLSX...
Filament heeft import en export van CSV ingebouwd via ImportAction en ExportAction. Je definieert per model één importer- of exporterclass, Filament verwerkt het bestand in queue-jobs van standaard 100 rijen en levert een CSV met mislukte rijen terug. Export kan ook naar XLSX. Import van XLSX zit er niet standaard in.
Dit artikel laat zien hoe het werkt, waar de grenzen liggen en wat je moet regelen voor grote productbestanden en prijslijsten. Alle API-details komen uit de officiële Filament 5.x-documentatie. Die API is gelijk aan v4.
Hoe werkt importeren in Filament?
Je maakt een importerclass, koppelt die aan een ImportAction en Filament regelt de rest: upload, kolommen mappen, validatie en verwerking op de achtergrond. De gebruiker uploadt een CSV, koppelt de kolommen aan velden en krijgt een notificatie als het klaar is.
De flow in vijf stappen:
- Gebruiker klikt op "Import" en uploadt een CSV.
- Filament leest de header en raadt welke kolom bij welk veld hoort.
- De gebruiker controleert de mapping en start de import.
- Filament splitst het bestand in chunks en zet die als job batch op de queue.
- Na afloop volgt een database-notificatie, met een download van mislukte rijen als er fouten waren.
Een importer genereer je met:
php artisan make:filament-importer Product --generate
Met --generate leest Filament de databasekolommen uit en zet een eerste set kolommen klaar. Daarna pas je ze aan.
Wat heb je nodig voordat het werkt?
Import en export draaien op job batches en database-notificaties. Die tabellen moet je eerst aanmaken:
php artisan make:queue-batches-table
php artisan make:notifications-table
php artisan vendor:publish --tag=filament-actions-migrations
php artisan migrate
De derde regel publiceert de tabellen imports, exports en failed_import_rows. Vergeet ook geen draaiende queue worker. Met de sync-driver werkt het lokaal, maar dan blokkeert een groot bestand het request. Zet in productie een echte queue neer, bijvoorbeeld Redis met Laravel Horizon voor monitoring.
Hoe ziet een importer voor productdata eruit?
Een importer bestaat uit drie onderdelen: getColumns() voor velden en validatie, resolveRecord() voor nieuw of bestaand record, en een notificatietekst. Meer is niet nodig voor een werkende prijslijstimport.
Een voorbeeld voor producten op basis van SKU:
use App\Models\Product;
use Filament\Actions\Imports\ImportColumn;
use Filament\Actions\Imports\Importer;
use Filament\Actions\Imports\Models\Import;
class ProductImporter extends Importer
{
protected static ?string $model = Product::class;
public static function getColumns(): array
{
return [
ImportColumn::make('sku')
->requiredMapping()
->rules(['required', 'max:64']),
ImportColumn::make('name')
->requiredMapping()
->rules(['required', 'max:255']),
ImportColumn::make('price')
->numeric(decimalPlaces: 2)
->rules(['numeric', 'min:0']),
ImportColumn::make('brand')
->relationship(resolveUsing: 'name'),
];
}
public function resolveRecord(): ?Product
{
return Product::firstOrNew([
'sku' => $this->data['sku'],
]);
}
public static function getCompletedNotificationBody(Import $import): string
{
return $import->successful_rows . ' producten geïmporteerd.';
}
}
Wat hier gebeurt:
requiredMapping()dwingt af dat de gebruiker deze kolom koppelt.rules()zijn gewone Laravel-validatieregels, per rij uitgevoerd.numeric(decimalPlaces: 2)cast de waarde naar een float en rondt af.relationship(resolveUsing: 'name')zoekt het merk op naam in plaats van op ID.firstOrNewop SKU maakt de import idempotent. Bestaande producten worden bijgewerkt, nieuwe aangemaakt.
Standaard geeft resolveRecord() altijd een nieuw model terug. Dat betekent: elke import maakt nieuwe records. Voor productdata wil je dat bijna nooit. Pas dit dus altijd aan.
Welke kolomopties zijn handig voor webshopdata?
Filament biedt meer castingopties dan de meeste teams gebruiken. Een paar die in de praktijk tijd besparen:
| Methode | Wat het doet | Typisch gebruik |
|---|---|---|
integer() / boolean() | Cast naar int of bool | Voorraad, "actief"-vlag |
castStateUsing(fn ($state) => ...) | Eigen casting | Prijs met komma als decimaalteken |
multiple(',') | Splitst een cel in een array | Tags, categorieën |
enum(Status::class) | Valideert tegen een backed enum | Productstatus |
ignoreBlankState() | Laat bestaande waarde staan als cel leeg is | Gedeeltelijke updates |
requiredMappingForNewRecordsOnly() | Verplicht alleen bij nieuwe records | Prijsupdate zonder productnaam |
guess(['artikelnummer', 'art.nr']) | Helpt automatische mapping | Leveranciersbestanden met eigen headers |
sensitive() | Niet opslaan in mislukte rijen | Persoonsgegevens |
Voor Nederlandse prijslijsten is castStateUsing vaak nodig. Excel exporteert "12,95" en dat is voor numeric geen getal. Vervang de komma eerst, dan pas valideren.
Hoe stel je de ImportAction in?
Je plaatst ImportAction in de header van een tabel of op een pagina en koppelt de importer. Op de action zelf stel je chunkgrootte, maximum aantal rijen, scheidingsteken en bestandsregels in.
use App\Filament\Imports\ProductImporter;
use Filament\Actions\ImportAction;
ImportAction::make()
->importer(ProductImporter::class)
->csvDelimiter(';')
->chunkSize(250)
->maxRows(100000)
De belangrijkste knoppen:
csvDelimiter(';'): standaard is dat een komma. Nederlandse Excel slaat CSV op met puntkomma's. Zonder deze regel staat alles in één kolom.chunkSize(): standaard 100 rijen per job. Hoger betekent minder jobs, maar meer geheugen per job.maxRows(): harde grens. Voorkomt dat iemand per ongeluk 2 miljoen rijen uploadt.headerOffset(): slaat rijen boven de header over. Handig bij leveranciersbestanden met een logo of datum bovenaan.fileRules(): extra validatie op het bestand, zoals een maximale grootte.options()engetOptionsFormComponents(): extra keuzes in de modal, zoals "bestaande producten bijwerken ja/nee".
De importer heeft ook lifecycle hooks: beforeValidate, beforeFill, beforeSave, afterSave, afterCreate en meer. Daarin heb je toegang tot $this->data, $this->originalData en $this->record. Daar hoort logica die per rij moet draaien, zoals een slug genereren of een cache-tag invalideren.
Wat gebeurt er met foutieve rijen?
Rijen die validatie niet doorstaan, worden niet opgeslagen en komen in een aparte CSV met de foutmelding erbij. De gebruiker downloadt dat bestand na afloop, past het aan en uploadt opnieuw.
Dit is de grootste winst ten opzichte van een eigen importscript. Je krijgt geen alles-of-niets import. Een prijslijst van 8.000 regels met 40 fouten zet 7.960 regels door en levert een bestand van 40 regels op om te corrigeren.
Twee kanttekeningen:
- Validatie is per rij. Regels als
uniquewerken, maar controleren niet op dubbelen binnen hetzelfde bestand die in verschillende chunks zitten. - Foutmeldingen zijn Engelstalig tenzij je ze vertaalt. Met
getValidationMessages()in de importer zet je eigen teksten neer. Voor gebruikers zonder technische achtergrond scheelt dat supportvragen.
Filament genereert ook automatisch een voorbeeld-CSV met alle importeerbare kolommen. Met example() of examples() op een kolom vul je die met realistische waarden. Geef dat bestand aan leveranciers, dan krijg je minder creatieve kolomnamen terug.
Hoe werkt exporteren naar CSV en XLSX?
Exporteren werkt spiegelbeeldig: een exporterclass met kolommen, een ExportAction of ExportBulkAction, en verwerking in queue-jobs. Na afloop krijgt de gebruiker een notificatie met downloadlinks voor CSV en XLSX.
php artisan make:filament-exporter Product --generate
use App\Models\Product;
use Filament\Actions\Exports\ExportColumn;
use Filament\Actions\Exports\Exporter;
use Filament\Actions\Exports\Models\Export;
class ProductExporter extends Exporter
{
protected static ?string $model = Product::class;
public static function getColumns(): array
{
return [
ExportColumn::make('sku')->label('SKU'),
ExportColumn::make('name'),
ExportColumn::make('price'),
ExportColumn::make('brand.name')->label('Merk'),
ExportColumn::make('cost_price')->enabledByDefault(false),
];
}
public static function getCompletedNotificationBody(Export $export): string
{
return $export->successful_rows . ' producten geëxporteerd.';
}
}
De gebruiker kiest in de modal welke kolommen mee moeten. Met enabledByDefault(false) staat een kolom standaard uit. Handig voor inkoopprijzen die niet in elke export horen.
Op de action beperk je formaten en query:
use App\Filament\Exports\ProductExporter;
use Filament\Actions\ExportAction;
use Filament\Actions\Exports\Enums\ExportFormat;
ExportAction::make()
->exporter(ProductExporter::class)
->formats([ExportFormat::Xlsx])
->modifyQueryUsing(fn ($query) => $query->where('is_active', true))
ExportBulkAction exporteert alleen geselecteerde rijen. Daarnaast heb je formatStateUsing(), prefix()/suffix() en aggregaties als counts('variants'). Voor XLSX kun je de opmaak sturen met getXlsxCellStyle() en getXlsxHeaderCellStyle().
Kan Filament ook XLSX importeren?
Nee, niet standaard. De ingebouwde importer accepteert CSV (en .txt). XLSX-export zit er wel in. Voor Excel-import heb je drie opties.
| Optie | Voordeel | Nadeel | Wanneer kiezen |
|---|---|---|---|
| Gebruiker slaat op als CSV | Geen extra code | Gebruiker moet het weten, scheidingsteken varieert | Interne gebruikers, af en toe een import |
| Plugin van derden | XLSX direct uploaden | Extra dependency, andere API | Veel externe bestanden, weinig maatwerk |
| Eigen conversiestap naar CSV vóór de import | Je houdt de Filament-importer met foutrapport | Wat extra code | Leveranciersbestanden, vaste processen |
Op filamentphp.com/plugins staan meerdere Excel-importplugins voor v4 en v5. Bekijk per plugin of hij actief onderhouden wordt en of hij de queue gebruikt. Een import die synchroon draait, gaat bij grote bestanden onderuit.
Wij kiezen meestal voor de eigen conversiestap. Dan blijft het foutrapport van Filament werken en is er één importpad voor alles.
Hoe ga je om met grote bestanden?
Filament schaalt prima naar honderdduizenden rijen, mits je queue, chunkgrootte en database op orde zijn. Het knelpunt zit zelden in Filament zelf, maar in wat er per rij gebeurt.
De rekenstap: een bestand van 200.000 rijen met de standaard chunkgrootte van 100 levert 2.000 jobs op. Duurt één job 2 seconden, dan is dat ruim een uur met één worker. Met vier workers ongeveer een kwartier. Verhoog je de chunk naar 500, dan heb je 400 jobs, maar wel meer geheugen per job.
Waar je op let:
- Eén worker is te weinig. Zet een aparte queue voor imports via
getJobQueue(), zodat orderverwerking niet wacht op een prijslijst. Hoe je queues voor e-commerce inricht, staat in Laravel queues voor orderverwerking. - Relationship-lookups per rij zijn duur.
relationship()doet per rij een query. Bij 200.000 rijen merk je dat. - Observers en events tellen mee. Een model-observer die per opslag een search-index bijwerkt, maakt van een import van minuten een import van uren.
- Retries. Filament probeert een job standaard 24 uur opnieuw, of tot 5 mislukte pogingen. Met
getJobRetryUntil()engetJobBackoff()pas je dat aan. - Indexen.
firstOrNewopskuzonder database-index is een full table scan per rij.
Meer over tabellen en queries bij grote volumes lees je in Filament performance bij grote datasets.
Wanneer is de ingebouwde importer niet genoeg?
Eerlijk: voor dagelijkse feeds van een leverancier of ERP is een handmatige upload de verkeerde oplossing. Dan bouw je een geautomatiseerde koppeling die het bestand zelf ophaalt en verwerkt. Filament gebruik je dan voor monitoring en uitzonderingen, niet voor de import zelf.
Ook bij miljoenen rijen met complexe transformaties kies je beter voor een eigen pipeline met bulk-inserts. Hetzelfde principe als bij productimports in Magento 2: rij-voor-rij opslaan via het ORM is de traagste route.
Waar moet je op letten qua beveiliging?
Exports bevatten vaak gevoelige data en Filament controleert geen rechten per record. Dat regel je zelf met een policy en een beperkte query.
- Download-rechten. Registreer een
ExportPolicymet eenview()-methode. Die bepaalt wie een exportbestand mag downloaden. - Scope de query. Gebruik
modifyQueryUsing()om alleen data te exporteren waar de gebruiker recht op heeft. - Bestandsopslag. Staat je standaard-disk op
publicen bestaat er eenlocaldisk, dan kiest Filament zelflocalvoor exports. Controleer dat metfileDisk()ofgetFileDisk(). - Opruimen. Filament verwijdert exportbestanden niet zelf. Zet een scheduled job neer die oude exports opruimt.
- Formula injection. Een cel die met
=begint, voert Excel uit als formule. ZetpreventFormulaInjection()aan op kolommen met data van klanten of leveranciers.
Toepassing: prijslijsten en productdata voor webshops
Voor webshops is de ingebouwde import ideaal als beheerlaag voor productdata. Inkopers uploaden een prijslijst, krijgen een foutrapport en corrigeren zelf. Geen developer nodig per prijswijziging.
Een typische opzet die wij bouwen:
- Product- en prijsdata staan in een Laravel-applicatie met Filament als beheerscherm.
- Inkopers importeren prijslijsten via
ImportAction, gematcht op SKU. - Een
afterSave-hook markeert gewijzigde producten. - Een aparte job pusht de wijzigingen naar Magento of Shopify via de API.
Dat is in feite een lichte PIM. Wil je verder gaan met attributen, varianten en kanalen, lees dan een PIM bouwen met Filament. Onze eigen backoffice draait ook op Filament. Meer over het framework zelf staat op onze pagina over Filament.
Samengevat
- Filament heeft CSV-import en CSV/XLSX-export ingebouwd. Geen plugin nodig.
- Import vereist job batches, database-notificaties en een draaiende queue worker.
- Pas
resolveRecord()aan naarfirstOrNewop SKU, anders maakt elke import nieuwe records. - Zet
csvDelimiter(';')voor Nederlandse Excel-bestanden. - Mislukte rijen komen terug als downloadbare CSV met foutmelding.
- XLSX importeren kan alleen via een plugin of eigen conversiestap.
- Bij grote bestanden zit de bottleneck in per-rij logica, niet in Filament.
- Beveilig exports met een policy, een gescopete query en formula injection-preventie.
Veelgestelde vragen
Heeft Filament een ingebouwde CSV-import?
Ja. ImportAction en ExportAction zitten in het actions-package van Filament, met dezelfde API in v4 en v5. Je maakt een importerclass met php artisan make:filament-importer en koppelt die aan de action. Validatie, chunking en een foutrapport zitten er standaard in.
Kan ik met Filament Excel-bestanden (XLSX) importeren?
Niet met de ingebouwde importer. Die accepteert CSV en .txt. Voor XLSX gebruik je een plugin van derden of zet je het bestand eerst om naar CSV. Exporteren naar XLSX kan wel standaard via ExportFormat::Xlsx.
Waarom staat mijn hele CSV in één kolom?
Je bestand gebruikt waarschijnlijk puntkomma's als scheidingsteken. Nederlandse Excel doet dat standaard. Zet ->csvDelimiter(';') op de ImportAction, of laat gebruikers met komma's opslaan.
Hoe update ik bestaande producten in plaats van nieuwe aan te maken?
Overschrijf resolveRecord() in je importer en gebruik Product::firstOrNew(['sku' => $this->data['sku']]). Dan wordt een bestaand product met dezelfde SKU bijgewerkt. Combineer dat met ignoreBlankState() als lege cellen bestaande waarden niet mogen overschrijven.
Hoe groot mag een importbestand zijn?
Filament legt zelf geen vaste grens op aantal rijen, tenzij je maxRows() instelt. De praktische grens zit in je PHP-uploadlimiet, je queue-capaciteit en de logica per rij. Stel altijd maxRows() en fileRules() in, zodat een verkeerd bestand je queue niet urenlang bezet houdt.
Waar worden exportbestanden opgeslagen?
Op de filesystem-disk die je instelt. Is de standaard public en bestaat er een local disk, dan gebruikt Filament local. Filament ruimt de bestanden niet op. Dat moet je zelf regelen met bijvoorbeeld een scheduled command.
Twijfel je of de ingebouwde import genoeg is voor jouw productdata, of heb je een echte koppeling nodig? Neem contact op, dan kijken we er samen naar.

Geschreven door Ruthger Idema
15+ jaar ervaring in e-commerce development. Gespecialiseerd in Magento, Shopify en Laravel maatwerk.
Meer over ons team →