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

Livewire Csv Laravel Package

askdkc/livewire-csv

Livewire CSV is a Laravel Livewire package for handling CSV imports with a simple API, using database UPSERT to create or update records. It includes a setup command, importer and button components, queue support, and TALL stack integration.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install and scaffold

composer require askdkc/livewire-csv
php artisan livecsv-setup

livecsv-setup publishes the config file and migrations, then interactively asks whether it should run php artisan migrate for you (and, per the README, asks you to star the repo — answer freely). If you'd rather stay in control:

php artisan vendor:publish --tag="livewire-csv-migrations"
php artisan vendor:publish --tag="livewire-csv-config"
php artisan migrate

2. Check the queue tables exist — imports run through queued jobs, so you need the jobs/batches tables:

php artisan queue:table
php artisan queue:batches-table
php artisan migrate

3. Add the trait to your User model (yes, User specifically — the trait is a gate for the importer, not a per-model concern):

use Askdkc\LivewireCsv\Concerns\HasCsvImports;

class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable, HasCsvImports;
}

4. Drop the component into a Blade view — this is the whole "UI" of the package:

<livewire:csv-importer
    :model="App\Models\Contact::class"
    :columns-to-map="['id', 'name', 'email']"
    :required-columns="['name', 'email']"
    :column-labels="['id' => 'ID', 'name' => 'Name', 'email' => 'Email Address']"
    :upsert-columns="['email']"
/>

<x-csv-button class="rounded py-2 px-3 bg-indigo-500 text-white">Import</x-csv-button>

Where to look first: the published config/livewire-csv.php (it's four keys: layout, file_type, set_delimiter, file_upload_size) and the migrations the setup command drops in. Then read the component source in vendor/askdkc/livewire-csv/src — the README is decent but the props table is the entire public API surface, so the class will answer "what actually happens on upload."

First use case: an admin-only "Import contacts" page behind your existing auth middleware, mapping name/email from a spreadsheet, upserting on email. Build that before you touch queues or TSV.

Implementation Patterns

Wrap the component in your own page, not your layout. The importer is a full Livewire component with its own upload/mapping/confirm steps. Give it a dedicated route inside your admin group:

// routes/web.php
Route::middleware(['auth', 'can:import-data'])->group(function () {
    Route::get('/imports/contacts', ImportContacts::class)->name('imports.contacts');
});
{{-- resources/views/livewire/import-contacts.blade.php --}}
<x-app-layout>
    <x-slot name="header">{{ __('Import Contacts') }}</x-slot>

    <div class="max-w-3xl mx-auto py-8">
        <p class="text-sm text-gray-600 mb-4">
            {{ __('CSV must include a header row. Existing records are matched on email.') }}
        </p>

        <livewire:csv-importer
            :model="App\Models\Contact::class"
            :columns-to-map="['name', 'email', 'company']"
            :required-columns="['name', 'email']"
            :column-labels="['name' => 'Full name', 'email' => 'Email', 'company' => 'Company']"
            :upsert-columns="['email']"
        />
    </div>
</x-app-layout>

Make the upsert key a real unique index. This is the single most important design decision. :upsert-columns compiles to a database UPSERT, so the columns you name must carry a primary or unique index — otherwise you get duplicates, or an outright failure:

// in your model's migration
$table->string('email')->unique();
// then in the component: :upsert-columns="['email']"

If you want a non-id primary key, remove $table->id() (you can't have two primary keys) and declare e.g. $table->string('email')->primary();.

Default behavior = upsert on id. Omitting :upsert-columns means the importer looks for an id column in the CSV. For "append new rows" imports that's usually wrong — either supply :upsert-columns with your natural key, or make sure the exported CSV includes existing IDs.

Use the button component as a trigger, not the importer itself. x-csv-button is an Alpine-driven wrapper; treat it as your "open the import panel" affordance. Style it purely with class (Tailwind utilities), since layout only supports tailwindcss today:

<x-csv-button class="rounded-md px-4 py-2 bg-indigo-600 text-white text-sm" type="button">
    {{ __('Import CSV') }}
</x-csv-button>

Non-TALL projects: publish views and compile, or use the directives. Put @csvStyles in <head> and @csvScripts before </body> — the package ships its own assets and will not render correctly without one of these two paths.

<head>@csvStyles</head>
... 
@csvScripts

Large files → keep the queue worker running. Imports run as background jobs using PHP generators, so memory stays flat but results are asynchronous. In production run php artisan queue:work (or Horizon) and set QUEUE_CONNECTION as you normally would; imports are also batched, which is why the batches table matters. Locally, queue:listen on a second terminal while you test.

Tune the config for the file format you actually receive:

return [
    'layout'          => 'tailwindcss',
    'file_type'       => 'csv',     // 'tsv' for tab-separated
    'set_delimiter'   => ',',       // ';' for European Excel exports
    'file_upload_size'=> 102400,    // KB, i.e. 100MB ceiling
];

European Excel exports (semicolon-separated, sometimes BOM-prefixed) are the most common real-world failure — set set_delimiter before you blame the package.

Testing: the package ships helpers and its own suite (composer test). In your app, assert on the end state of the database rather than on the component's internals — upload a fixture CSV, run the queue, and assert counts/values. Because the write path is upsert, the useful assertions are:

Queue::fake(); // or Bus::fake()
// act: upload fixture
$this->assertDatabaseHas('contacts', ['email' => '[email protected]', 'name' => 'A']);
$this->assertDatabaseCount('contacts', 2); // one updated, one created

Integration tip — keep a narrow "import contract" per model. Rather than one generic importer for every table, define one Livewire page per entity with hardcoded columns-to-map/required-columns. You get sane validation messages, a stable label set for users, and no chance of a fat-fingered prop hitting the wrong model class.

Gotchas and Tips

UPSERT bypasses Eloquent entirely. This is the biggest trap. The write goes through the query builder, so model events (creating, saving), observers, accessors/mutators, and casts do not fire. Consequences to plan for:

  • Password hashing in the CSV must be done before import, not in a mutator.
  • created_at/updated_at may need to appear in the CSV or be handled by defaults — don't assume $table->timestamps() gets populated.
  • Slugs, UUIDs, and "normalize this field" logic must happen upstream (transform the file, or import into a staging table first).
  • Broadcasting/notifications in observers won't fire — hook your own post-import step.

HasCsvImports on User is a gate, not magic. The trait on User is required by the package regardless of which model you import into. If you import as a guest, or your auth guard differs, expect the component to misbehave. And because the component is just a Livewire component, it is mountable by anyone who can reach the route — always place it behind auth/authorization. The package provides no built-in ACL.

Unique index = upsert index. If you pass :upsert-columns="['name', 'email']", your migration must declare $table->unique(['name', 'email']);. Mismatches produce duplicate rows silently in some drivers. Verify with SHOW INDEX before debugging anything else.

Column mapping is positional-by-header, so headers must match exactly. :columns-to-map must be the DB column names, and the CSV header row must be the thing the mapping UI presents. Extra columns in the file are ignored; missing required ones fail validation using :required-columns and display via :column-labels.

Two different publish tags for views. The README uses --tag="livewire-csv-views" in the manual-config section and --tag="csv-views" in the TALL section. Try both; if neither produces files, run php artisan vendor:publish without a tag and pick the numeric entry from the list. Same trick applies if livecsv-setup partially fails.

File size is validated twice. file_upload_size in the package config and Livewire's own temporary_file_upload.rules / max_upload_time (plus upload_max_filesize/post_max_size in php.ini). A failing large upload often shows no package error at all — check storage/logs/laravel.log and Livewire's config first.

"It hangs after upload." Nine times out of ten the queue worker isn't running, or job_batches was never migrated. The import is asynchronous by design; the confirm step only dispatches the batch. Check jobs, failed_jobs, and job_batches rows, then php artisan queue:work --queue=default.

Views are your extension point, not PHP. Since there's essentially no documented hook system, customization means publishing the views and editing them: replace the Tailwind markup, add explanatory copy, add a "download template CSV" link, or swap in a Bootstrap layout. Combine with layout in config being effectively hardcoded to tailwindcss — non-Tailwind projects should expect to own the published view files permanently.

Small package, so vendor the risk. 17 stars, 0 dependents, one maintained author. That's fine for an admin bulk-import tool; it's not fine as load-bearing infrastructure. Practical mitigations:

  • Pin the version ("askdkc/livewire-csv": "^2.0"), and read the changelog before upgrading — v2.0.0 is largely dependency bumps plus Laravel 12/13 + Livewire 4 support, so major jumps are where breakage lives.
  • Copy the importer Blade view into your repo and keep a note of which version it came from.
  • Have a fallback: this package is ~a few hundred lines around Laravel's upsert(). If it's abandoned, reimplementing the mapping UI is a day of work, not a rewrite.
  • Note the PHP requirement — the v2 line advertises PHP 8.4 support; verify against your CI matrix.

Data safety checklist before the first production import: take a DB snapshot, import into staging with a production-shaped file, confirm row counts, and dry-run your "unique index" assumption with a deliberately duplicated CSV (which is what you're testing — that updates happen instead of inserts). Because upsert is destructive-by-design, a wrong :upsert-columns can overwrite real records on first upload.

Encoding and delimiters. Excel on Windows produces UTF-8-with-BOM or Shift-JIS depending on the user; the package's set_delimiter/file_type config handles separation but not encoding. Add a "please save as UTF-8 CSV" note next to the component, or normalize the file with a small pre-processing step if you can't control the source.

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.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core