Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Filament Excel Import Laravel Package

eightynine/filament-excel-import

Filament Excel Import adds a ready-made import action to your Filament resources, letting users upload Excel or CSV files with column mapping, validation, chunked processing, and progress feedback—no custom import code required.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install against the right major line

composer require eightynine/filament-excel-import:^4.0
php artisan vendor:publish --tag="filament-excel-import-config"   # tag name may differ; check README
php artisan filament:assets                                        # only if the plugin ships assets

v4.0.0 is the official Filament v4 support release — the ambiguity is gone. Use:

  • ^4.0 on a Filament v4 panel.
  • ^3.1 on a Filament v3 panel, which is still actively maintained (v3 fixes landed in the same release).

Because v4.0.0 is explicitly an initial v4 release, expect a .1 shortly. Pin exactly ("0.x.y" / "4.0.0") until the line settles, and read the diff before bumping.

2. Still check composer.json before writing code

Open vendor/eightynine/filament-excel-import/composer.json and confirm:

  • The Filament constraint matches your panel major — a mismatch here is a silent runtime failure, not a composer error, if you have both installed transitively.
  • Whether maatwebsite/excel is a direct dependency. It is, effectively, a thin Filament wrapper around Laravel Excel — importers, chunk reading and heading rows all behave exactly as you already know.

3. Register the plugin (if your version exposes one)

// app/Providers/Filament/AdminPanelProvider.php
public function panel(Panel $panel): Panel
{
    return $panel
        // ...
        ->plugin(\EightyNine\FilamentExcelImport\FilamentExcelImportPlugin::make());
}

Some releases are action-only. Check src/ for a class implementing Filament\Contracts\Plugin; if there isn't one, skip this step.

4. Where to look first

Location Why
README.md on GitHub The only docs; examples are the canonical usage
src/Actions/ The ImportAction class — its public methods are your real API surface
src/Importer.php (or similar) The base class / contract your importers must satisfy
composer.json Filament + Laravel Excel version constraints
CHANGELOG / release notes v4 changed modal + notification behaviour — see Gotchas

5. First use case

The 80% case: a ListRecords page gets an "Import" header button, the user uploads an .xlsx, rows become Eloquent records.

// app/Filament/Resources/UserResource/Pages/ListUsers.php
use App\Imports\UserImport;
use EightyNine\FilamentExcelImport\Actions\ImportAction; // confirm namespace in vendor

protected function getHeaderActions(): array
{
    return [
        ImportAction::make()
            ->importer(UserImport::class)
            ->label('Import users'),
        Actions\CreateAction::make(),
    ];
}

Then test two paths immediately, because v4 changed both:

  1. A clean 3-row file — you should get the new success notification without writing any notification code yourself.
  2. A file that makes your importer throw — the modal should stay open with the error, rather than closing and losing the user's upload.

If path 2 closes the modal anyway, your importer is swallowing the exception instead of letting it bubble. That distinction is the single most important behavioural change in this release.

Implementation Patterns

One importer class per entity, kept dumb. Treat the importer as an adapter, not a place for business logic.

namespace App\Imports;

use App\Actions\Users\UpsertUserFromRow;
use App\Models\User;
use Maatwebsite\Excel\Concerns\ToModel;
use Maatwebsite\Excel\Concerns\WithHeadingRow;

class UserImport implements ToModel, WithHeadingRow
{
    public function model(array $row): ?User
    {
        return app(UpsertUserFromRow::class)->execute($row);
    }
}

If the package ships its own base Importer with hooks (beforeImport, afterImport, model resolution), use it — but only for the wiring. Keep mapping in your own action so it is unit-testable without an Excel file.

Decide how failures surface — this is now a design decision, not an accident. With v4, "modal stays open" is driven by an exception escaping the import. That gives you two coherent strategies:

// A) Strict: fail the whole upload, keep the modal open, let the user fix the file.
//    Best for small, contract-driven imports (settings, price lists).
throw ValidationException::withMessages([
    'data' => ["Row {$index}: ".implode(' ', $errors)],
]);

// B) Tolerant: skip bad rows, import the rest, but notify explicitly —
//    the built-in success notification will fire regardless.
$this->failures[$index] = $errors;
return null; // skip this row only

With (B) you must add your own summary notification ("142 imported, 3 skipped") including row numbers, because the package's success notification will happily announce success on a partial import. Don't mix the two strategies in one importer; pick per resource and document which.

Upsert on a natural key, not on id. Imports get re-run. Match on email, sku, or an external reference and use updateOrCreate. If there is no natural key, add one to the spreadsheet contract before you ship the feature.

Resolve relationships by human-readable value. Spreadsheets contain "Acme Ltd", not company_id. Load a lookup map once per import, not per row:

$companies = Company::pluck('id', 'name'); // once
$row['company_id'] = $companies[$row['company']] ?? null;

Multi-tenancy: set the tenant explicitly. Filament scopes resources by tenant; a raw importer does not. Every created record needs team_id / tenant_id from Filament::getTenant(). Still the most common production bug with import features in Filament panels.

Queue anything that might exceed ~1,000 rows. WithChunkReading caps memory; ShouldQueue keeps the Livewire request from timing out. Combine both, accept the import is no longer transactional, and rely on idempotent upserts. Note the v4 modal/notification UX is a synchronous affordance — a queued import returns immediately, so you still own the "how did it go?" story (database notifications, a status column, or a Filament\Notifications push from the job).

Testing. UploadedFile::fake()->create('users.xlsx') produces a file with no valid XLSX internals. Keep a real fixture in tests/Fixtures/users.xlsx and feed it via createWithContent() or a real new UploadedFile(...). Test the importer class directly against a Collection — faster, no Livewire needed. Add one Livewire test asserting the modal stays open on a throwing import and closes on a clean one, so a future minor bump can't silently regress the UX you designed around.

Gotchas and Tips

Don't double-notify. v4.0.0 adds a success notification on completion. If you also flash your own "Import finished" toast you'll get two. Keep the built-in one and only add notifications for additional information (failure summaries, skipped rows, queued-job outcomes).

A swallowed exception looks like a successful import. Because the package treats "no throw" as success, try/catch blocks inside your importer will produce a green success toast and a closed modal on a partially failed file. Log and rethrow, or use the tolerant pattern plus your own summary — but never silently catch (\Throwable $e) {}.

The modal-stays-open behaviour is new and version-specific. If you support both v3 and v4 panels (multi-panel apps do exist), the UX diverges: v3 closes on error. Either normalise with your own notification layer, or pin a single major per deployment.

Heading-row slugging is silent and lossy. First Name, First name and first_name all normalise to first_name, but E-mail Address becomes e_mail_address. dd($rows->first()) on a real customer file before trusting key names, and validate the header row up front with an error listing missing columns.

Dates arrive as integers or floats, not Carbon. Excel stores dates as serials — you get 45870, not 2025-08-24. Convert with \PhpOffice\PhpSpreadsheet\Shared\Date::excelToDateTimeObject() or configure date columns in config/excel.php. Test a file mixing 2025-08-24, 24/08/2025 and a real datetime cell; locale is not inferred.

Formula cells return the formula string by default. Add WithCalculatedFormulas if users upload =SUM() or =VLOOKUP() columns, or you'll store "=A2*B2" in a numeric column.

Blank cells are null, trailing rows exist. Filter with collect($row)->filter(fn ($v) => filled($v))->isNotEmpty(), and trim() every string — non-breaking spaces from copy-paste are a recurring source of failed email validation.

Number localisation. 1.234,56 (EU) parses as a string; 1,234.56 parses as 1234.56 or 1.23456 depending on the reader. Normalise explicitly for monetary columns and document the expected format in the UI.

Temp file lifecycle. The uploaded file lives on the temporary upload disk. If you queue the import, copy it to permanent storage first and pass the path — otherwise the temp file may be cleaned before the worker reads it. More relevant now: the v4 modal stays open while the user sits on the page, so synchronous files are safe, but anything you dispatch is not.

Pin the version, and read both branches. Two maintained lines (v3.1.x, v4.0.x) means a fix you need may land on the other one first. Use an exact constraint, and check 3.x as well as 4.x before filing a bug. Wrapping ImportAction::make() in your own App\Filament\Actions\ImportAction costs ten minutes and insulates every resource from upstream API movement — still worth it on a package this size.

Authorisation is your job. Add ->visible(fn () => auth()->user()->can('import', User::class)) and enforce the same policy inside the importer. A hidden action is not authorisation.

Bypass $fillable deliberately. Bulk inserts hit mass-assignment guards. Either forceFill() inside the importer, or ensure $fillable covers every imported column — a MassAssignmentException mid-chunk leaves a half-imported file (and, in v4, an open modal over a partially mutated database).

When to walk away. If your import needs column-mapping UI, dry-run previews, rollback, or per-row review screens, this package gives you the entry point but not the workflow. v4's improved error affordance narrows that gap slightly — the modal now holds context on failure — but the wizard is still yours to build. At that point maatwebsite/excel directly, with a Filament custom page, is the more honest starting point.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle