Migrating from Laravel Excel imports to Tapix | Tapix                  [ ![Tapix](/img/tapix-logo-light.svg) ![Tapix](/img/tapix-logo-dark.svg) ](https://tapix.dev) [Features](https://tapix.dev#features) [Pricing](https://tapix.dev#pricing) [Docs](https://docs.tapix.dev) [Blog](https://tapix.dev/blog)

   Try Demo  [ Get Tapix from $99](https://tapix.dev#pricing)

  [Features](https://tapix.dev#features) [Pricing](https://tapix.dev#pricing) [Docs](https://docs.tapix.dev) [Blog](https://tapix.dev/blog)   Try Demo  [ Get Tapix from $99](https://tapix.dev#pricing)

   ![Tapix](https://tapix.dev/img/tapix-logo-light.svg)

 ProductMigrating from Laravel Excel imports to Tapix
=============================================

 tapix.dev/blog

  [    Back to blog ](https://tapix.dev/blog) [ Product ](https://tapix.dev/blog/category/product)

Migrating from Laravel Excel imports to Tapix
=============================================

 Manch Minasyan ·  July 10, 2026  · 12 min read

 If your codebase has a class implementing `ToModel`, this post is for you. Laravel Excel has been the default CSV import package for years, and a typical production importer ends up with six concerns stacked on a single class. It works. Until a user uploads a CSV with "First Name" instead of "first\_name", and it silently produces null values.

The interface was designed for developers, not end users. Your importer class knows the exact column order, validation runs silently in the background, and relationship resolution is your problem to solve in the `model()` method. When you need a user-facing import wizard -- column mapping UI, inline error correction, relationship linking -- Laravel Excel requires you to build all of that yourself.

This guide walks through migrating a real Laravel Excel importer to Tapix, step by step, with side-by-side code so you can see exactly what changes and what stays the same.

[\#](#concept-mapping "Permalink")Concept mapping
-------------------------------------------------

Before touching code, it helps to see how the two packages map to each other conceptually. Most of what you know from Laravel Excel has a direct equivalent in Tapix -- it is just organized differently.

Laravel ExcelTapixNotes`implements ToModel``extends BaseImporter`One base class instead of stacking concerns`model(array $row)``fields(): ImportFieldCollection`Declarative field definitions replace manual array mapping`implements WithHeadingRow`AutomaticTapix always parses headers and auto-maps them to fields`implements WithValidation` / `rules()``->rules([...])` on each `ImportField`Validation lives on the field definition, not a separate method`implements WithChunkReading`AutomaticQueue processing with chunked batches is built in`implements WithBatchInserts`AutomaticConfigurable in `config/tapix.php`, not per-importer`implements ShouldQueue`AutomaticAll Tapix imports run through queued jobs by default`implements SkipsOnFailure`Review stepUsers see and fix errors in the wizard before executionManual `firstOrCreate` in `model()``->relationship()` on `ImportField`Declarative relationship linking with configurable match behavior`Excel::import()` in controller`TapixPlugin` or Livewire routeUI wizard handles the entire flowThe pattern is consistent: things you implemented manually as concerns or custom logic in Laravel Excel become declarative configuration in Tapix.

[\#](#step-1-create-the-importer "Permalink")Step 1: Create the importer
------------------------------------------------------------------------

Suppose you have a Laravel Excel importer that brings contacts into a CRM. Here is what it typically looks like:

```
namespace App\Imports;

use App\Models\Contact;
use Maatwebsite\Excel\Concerns\ToModel;
use Maatwebsite\Excel\Concerns\WithHeadingRow;
use Maatwebsite\Excel\Concerns\WithValidation;
use Maatwebsite\Excel\Concerns\WithChunkReading;
use Maatwebsite\Excel\Concerns\WithBatchInserts;
use Illuminate\Contracts\Queue\ShouldQueue;

class ContactsImport implements
    ToModel,
    WithHeadingRow,
    WithValidation,
    WithChunkReading,
    WithBatchInserts,
    ShouldQueue
{
    public function model(array $row): Contact
    {
        return new Contact([
            'first_name' => $row['first_name'],
            'last_name'  => $row['last_name'],
            'email'      => $row['email'],
            'phone'      => $row['phone'],
            'job_title'  => $row['job_title'],
        ]);
    }

    public function rules(): array
    {
        return [
            'first_name' => ['required', 'string', 'max:255'],
            'last_name'  => ['required', 'string', 'max:255'],
            'email'      => ['required', 'email'],
            'phone'      => ['nullable', 'string'],
            'job_title'  => ['nullable', 'string', 'max:255'],
        ];
    }

    public function chunkSize(): int
    {
        return 1000;
    }

    public function batchSize(): int
    {
        return 500;
    }
}

```

Six interfaces, four methods, and zero UI. The column mapping is implicit -- `WithHeadingRow` expects the CSV headers to match the array keys exactly. If someone uploads a file with "First Name" instead of "first\_name", the import silently produces null values.

Generate the Tapix equivalent:

```
php artisan make:tapix-importer Contact

```

This creates `app/Importers/ContactImporter.php` with the scaffolding already in place. You fill in two methods: `model()` to declare which Eloquent model receives the data, and `fields()` to define the importable columns:

```
namespace App\Importers;

use App\Models\Contact;
use Tapix\Core\Fields\FieldType;
use Tapix\Core\Fields\ImportField;
use Tapix\Core\Fields\ImportFieldCollection;
use Tapix\Core\Importing\BaseImporter;

class ContactImporter extends BaseImporter
{
    public function model(): string
    {
        return Contact::class;
    }

    public function fields(): ImportFieldCollection
    {
        return ImportFieldCollection::make([
            ImportField::make('first_name')
                ->required()
                ->guess(['first name', 'first', 'given name', 'fname']),

            ImportField::make('last_name')
                ->required()
                ->guess(['last name', 'last', 'surname', 'family name', 'lname']),

            ImportField::make('email')
                ->type(FieldType::Email)
                ->acceptsArbitraryValues()
                ->guess(['email', 'email address', 'e-mail']),

            ImportField::make('phone')
                ->type(FieldType::Phone)
                ->acceptsArbitraryValues()
                ->guess(['phone', 'phone number', 'telephone', 'mobile', 'cell']),

            ImportField::make('job_title')
                ->guess(['job title', 'title', 'position', 'role']),
        ]);
    }
}

```

No `WithHeadingRow` -- Tapix always parses headers. No `WithChunkReading` or `WithBatchInserts` -- queue processing is automatic and configured globally. No `WithValidation` -- field types like `FieldType::Email` and `FieldType::Phone` carry their own default validation rules, and you can add custom rules with `->rules([...])` on any field.

The `guess()` method is the important addition. When a user uploads a CSV with headers like "First Name", "fname", or "given name", Tapix auto-maps those to the `first_name` field in the mapping step. No more silent null values from header mismatches.

[\#](#step-2-define-fields-with-types-and-validation "Permalink")Step 2: Define fields with types and validation
----------------------------------------------------------------------------------------------------------------

In Laravel Excel, validation lives in a separate `rules()` method that returns a flat array. You reference columns by their heading row key, and the validation runs behind the scenes. If it fails, you either get an exception or silently skip rows (with `SkipsOnFailure`):

```
// Laravel Excel: validation in a separate method
public function rules(): array
{
    return [
        'email'      => ['required', 'email'],
        'deal_value' => ['nullable', 'numeric', 'min:0'],
        'status'     => ['nullable', 'in:active,inactive,lead,lost'],
        'phone'      => ['nullable', 'string'],
    ];
}

```

In Tapix, validation is attached directly to the field definition. The `FieldType` enum handles common validation automatically -- `FieldType::Email` adds email validation, `FieldType::Currency` handles numeric parsing including currency symbols and thousands separators, `FieldType::Phone` validates phone formats. You only add explicit rules for business-specific constraints:

```
// Tapix: validation on the field itself
ImportField::make('email')
    ->type(FieldType::Email)
    ->required()
    ->rules(['unique:contacts,email'])
    ->acceptsArbitraryValues(),

ImportField::make('deal_value')
    ->type(FieldType::Currency)
    ->rules(['min:0'])
    ->guess(['deal value', 'deal', 'value', 'revenue', 'budget']),

ImportField::make('status')
    ->type(FieldType::Choice)
    ->options([
        ['label' => 'Active', 'value' => 'active'],
        ['label' => 'Inactive', 'value' => 'inactive'],
        ['label' => 'Lead', 'value' => 'lead'],
        ['label' => 'Lost', 'value' => 'lost'],
    ]),

ImportField::make('phone')
    ->type(FieldType::Phone)
    ->acceptsArbitraryValues(),

```

The practical difference: when Tapix validates and finds errors, the user sees them inline in the review step. They can correct "john@" to "john@example.com" right there in the wizard, fix just the broken rows, and continue. In Laravel Excel, validation failures either throw an exception or get collected into an array that you have to build a UI for yourself.

The typed field system also handles data parsing. A `Currency` field automatically parses "$1,234.56" into `1234.56` before insertion. A `Date` field detects common date formats. You do not need to write parsing logic in your `model()` method.

[\#](#step-3-add-relationships----the-part-laravel-excel-cannot-do "Permalink")Step 3: Add relationships -- the part Laravel Excel cannot do
--------------------------------------------------------------------------------------------------------------------------------------------

This is where the migration gets interesting, because you are adding capability that did not exist before.

In a typical Laravel Excel importer, handling relationships means writing manual lookup logic inside the `model()` method:

```
// Laravel Excel: manual relationship resolution
public function model(array $row): Contact
{
    $companyId = null;

    if (! empty($row['company'])) {
        $company = Company::firstOrCreate(
            ['name' => $row['company']],
        );
        $companyId = $company->id;
    }

    return new Contact([
        'first_name'  => $row['first_name'],
        'last_name'   => $row['last_name'],
        'email'       => $row['email'],
        'company_id'  => $companyId,
    ]);
}

```

This works for simple cases, but it has real limitations. The user has no visibility into what happens -- they do not know which companies will be created versus matched. There is no way to configure the behavior per import. If the company column has typos ("Acme Corp" vs "Acme Corporation"), both get created as separate records. And if you need this pattern for multiple relationship columns, the `model()` method becomes a wall of `firstOrCreate` calls.

In Tapix, relationships are declared on the field with configurable match behavior:

```
// Tapix: declarative relationship with match behavior
ImportField::make('company')
    ->label('Company')
    ->guess(['company', 'company name', 'organization', 'org'])
    ->relationship(
        name: 'company',
        model: Company::class,
        matchBy: ['name'],
        behavior: MatchBehavior::MatchOrCreate,
    ),

```

The `relationship()` method takes four arguments: the Eloquent relationship name on the model, the related model class, which columns to match against when looking up existing records, and the match behavior. For the design decisions behind when to use each option, see [The match-or-create pattern: linking CSV data to existing records](/blog/match-or-create-pattern). The `MatchBehavior` enum has three options:

- `MatchOrCreate` -- look up by the match columns; if no record is found, create one
- `MatchOnly` -- look up only; skip the row if no match exists
- `Create` -- always create a new related record, no lookup

The wizard presents the relationship step to users before execution. They see which companies already exist and which will be created, and they can adjust the match behavior per relationship. This is the kind of UI you would spend days building on top of Laravel Excel.

For importers that need to run custom logic when saving, Tapix provides lifecycle hooks. The `prepareForSave` method lets you transform data before it hits the database, and `beforeSave` runs with the model instance right before persistence:

```
private int|string|null $importUserId = null;

public function beforeImport(Import $import): void
{
    $this->importUserId = $import->user_id;
}

public function prepareForSave(array $data, ?Model $existing, array &$context): array
{
    unset($data['id'], $data['company']);

    return $data;
}

public function beforeSave(Model $model, array $data): void
{
    $model->user_id = $this->importUserId;
}

```

Note that `auth()->id()` returns `null` inside queue workers because there is no HTTP request. The `Import` model stores the `user_id` from the original request, so `beforeImport` caches it on the importer instance for use in per-row hooks.

These hooks replace the inline logic you would put in Laravel Excel's `model()` method. The difference is that `prepareForSave` runs on the raw data array, while `beforeSave` runs on the Eloquent model -- giving you control at both stages. For a full reference of all six hooks and what each is for, see [CSV import lifecycle hooks: running custom logic before, during, and after import](/blog/csv-import-lifecycle-hooks).

[\#](#step-4-register-the-importer "Permalink")Step 4: Register the importer
----------------------------------------------------------------------------

In Laravel Excel, you call `Excel::import()` from a controller and build whatever UI you want around it:

```
// Laravel Excel: manual controller + route
class ImportController extends Controller
{
    public function store(Request $request)
    {
        $request->validate(['file' => 'required|file|mimes:csv,xlsx']);

        Excel::import(new ContactsImport, $request->file('file'));

        return back()->with('success', 'Import started.');
    }
}

```

With Tapix, you register the importer with your Filament panel or expose it via a standalone Livewire route.

For Filament, register `TapixPlugin` in your panel provider and list your importers:

```
use Tapix\Core\Filament\TapixPlugin;
use App\Importers\ContactImporter;

public function panel(Panel $panel): Panel
{
    return $panel
        ->plugins([
            TapixPlugin::make()
                ->importers([
                    ContactImporter::class,
                ]),
        ]);
}

```

The plugin registers the import wizard pages automatically. Users get a full 4-step wizard: upload, column mapping, review and error correction, and execution with live progress. No additional routes, controllers, or Blade views to create. For plugin auto-discovery, tenant context setup, and configuration options, see [Adding a CSV import wizard to your Filament panel](/blog/filament-csv-import-panel).

For standalone Laravel applications without Filament, mount the Livewire wizard component on any route:

```
// routes/web.php
Route::get('/import/contacts', function () {
    return view('import', ['importer' => 'contact']);
})->name('import.contacts');

```

Then render the wizard in your Blade template:

```

```

The wizard component handles the entire flow -- file upload, mapping, validation, review, and execution.

[\#](#what-stays-the-same "Permalink")What stays the same
---------------------------------------------------------

Migration is not a full rewrite. Large portions of your existing codebase carry over unchanged:

**Your Eloquent models do not change.** Tapix creates model instances through your existing relationships. If `Contact` has a `company()` BelongsTo relationship today, Tapix uses it directly.

**Your validation rules transfer directly.** Any rule you pass to Laravel Excel's `rules()` method works the same in Tapix's `->rules([...])` on an `ImportField`. Same validation engine underneath.

**Your queue configuration carries over.** Tapix reads queue settings from `config/tapix.php`, using whatever driver and connection you already have configured.

**Your existing data stays intact.** You are replacing how new data enters the system, not modifying what is already there.

The migration is additive. Run both packages side by side during the transition -- keep Laravel Excel for importers that work fine, move to Tapix for the ones that need a user-facing wizard or relationship handling.

[\#](#what-you-gain "Permalink")What you gain
---------------------------------------------

After migration, your contacts importer goes from a developer-facing concern stack to a user-facing wizard. Concretely:

**Header mismatches become non-issues.** The `guess()` aliases handle the common variations. For anything the heuristic does not catch, the mapping step gives users a UI to correct it before a single row is inserted.

**Validation errors become fixable, not fatal.** Instead of a Laravel Excel exception or a silent skip, users see exactly which rows failed and why. They fix the values inline and retry without re-uploading the file.

**Relationships get visibility.** Instead of `firstOrCreate` running silently in a queue worker, users see which companies already exist and which will be created. They can adjust the behavior before execution.

**New importers take minutes.** Once you have the `ContactImporter` pattern down, adding an `AccountImporter` or `ProductImporter` is a matter of copying the scaffold and filling in the fields. The queue processing, validation pipeline, and wizard UI are already wired up.

[\#](#where-to-go-from-here "Permalink")Where to go from here
-------------------------------------------------------------

If you are evaluating whether to migrate, start with the importer that gets the most user complaints. The one where support tickets mention "the CSV had different column names" or "it created duplicate companies." That importer will show the biggest improvement from the switch.

For more background on the concepts covered here:

- [The complete guide to CSV imports in Laravel](/blog/complete-guide-csv-imports-laravel) covers all four approaches to CSV importing, from raw PHP to dedicated packages
- [Laravel Excel vs Tapix](/blog/laravel-excel-vs-tapix) is a direct feature comparison if you are still deciding
- [Filament Import Action: when it's enough](/blog/filament-import-action-when-enough) covers the middle ground for Filament users

Ready to make the switch? [See pricing and get started](/#pricing).

 ### Enjoyed this post?

Get notified when we publish new articles about Laravel imports and data handling.

  Email address   Subscribe

Almost there — confirm your subscription via email.

 Related posts
-------------

 [  Product   Jun 30, 2026

 How Relaticle CRM uses Tapix for contact imports
--------------------------------------------------

Case study: how an open-source Laravel CRM uses Tapix to handle contact imports with company relationships, multi-value emails, and currency parsing.

 ](https://tapix.dev/blog/how-relaticle-uses-tapix) [  Product   Jun 16, 2026

 SaaS CSV importers vs self-hosted: which one fits your Laravel app?
---------------------------------------------------------------------

CSVBox, OneSchema, and Flatfile are hosted. Tapix is self-hosted. Here's the tradeoff matrix for choosing between them.

 ](https://tapix.dev/blog/csvbox-vs-self-hosted-import) [  Product   Jun 9, 2026

 Tapix under the hood: how we built a 4-step import wizard
-----------------------------------------------------------

The architecture behind Tapix: database-persisted state, batched per-column validation, immutable value objects, and chunked queue execution.

 ](https://tapix.dev/blog/tapix-under-the-hood-architecture)

   [ ![Tapix](/img/tapix-logo-light.svg) ![Tapix](/img/tapix-logo-dark.svg) ](https://tapix.dev)CSV and Excel import wizard for Laravel.

  Product [Pricing](https://tapix.dev#pricing) [Docs](https://docs.tapix.dev) [Blog](https://tapix.dev/blog) [Contact](mailto:hello@tapix.dev)

 Compare [vs Laravel Excel](https://tapix.dev/vs/laravel-excel) [vs Filament Import](https://tapix.dev/vs/filament-import)

 Legal [Privacy](https://tapix.dev/privacy-policy) [Terms](https://tapix.dev/terms-of-service)

© 2026 Tapix. All rights reserved.
