How to Dynamically Map Excel Columns and Import Data in Laravel (With Preview)

Need to import Excel data in Laravel 12 when column headers vary? Learn how to let users map headers, preview rows, and import data cleanly and safely using Laravel Excel.

Muhammad Ishaq
Muhammad Ishaq
11 Jul 2025
6 minute read
How to Dynamically Map Excel Columns and Import Data in Laravel (With Preview)

Introduction

When building admin panels, dashboards, or business tools in Laravel, Excel imports are a must-have. Whether you're importing user records, inventory, leads, or transaction logs. Excel is the go-to format for non-technical users.

But we can’t expect end-users to always use the same headers as we do.

You might expect:

Name, Email, Phone

But the uploaded file could have:

Full Name, Mail, Contact Number

If your app expects fixed headers, this will break. Instead, the smart way is to let users map their Excel columns to your database fields, preview the data, and then import it.

In this guide, we'll build a dynamic Excel importer in Laravel 12, complete with:

  1. File upload

  2. Header mapping UI

  3. Preview of first few rows

  4. Final data import

Let’s build a system that adapts to the data, not the other way around.


Prerequisites

You’ll need:

Laravel 12 installation

Laravel Excel (maatwebsite/excel)

Basic setup with Blade, routes, and controllers


Step 1: Install Laravel Excel

Install the package via Composer:

composer require maatwebsite/excel

Optional but helpful (if you want customization):

php artisan vendor:publish --provider="Maatwebsite\Excel\ExcelServiceProvider"

Step 2: Upload the Excel File

Create a basic file upload form:

<form action="{{ route('users.import.preview') }}" method="POST" enctype="multipart/form-data">
    @csrf
    <input type="file" name="file" accept=".xlsx,.xls,.csv" required>
    <button type="submit">Upload & Continue</button>
</form>

Step 3: Read Excel Headers and Show Mapping Form

In your controller:

use Maatwebsite\Excel\Facades\Excel;

public function ImportPreview(Request $request)
{
    $request->validate([
        'file' => 'required|file|mimes:xlsx,xls,csv',
    ]);

    $collection = Excel::toCollection(null, $request->file('file'));
    $headers = collect($collection[0]->first())->keys();
    $storedFile = $request->file('file')->store('temp');

    return view('users.import.map', [
        'headers' => $headers,
        'temp_file' => $storedFile,
    ]);
}

Then show the mapping form (resources/views/users/import/map.blade.php):

<form action="{{ route('users.import.preview.rows') }}" method="POST">
    @csrf
    <input type="hidden" name="temp_file" value="{{ $temp_file }}">
    @foreach ($headers as $header)
        <label>{{ $header }}</label>
        <select name="mapping[{{ $header }}]" required>
            <option value="">-- Select Field --</option>
            <option value="name">Name</option>
            <option value="email">Email</option>
            <option value="phone">Phone</option>
        </select>
        <br>
    @endforeach
    <button type="submit">Preview Rows</button>
</form>

This lets users map Excel headers to your actual database fields.


Step 4: Preview First 10 Rows Based on Mapping

In your controller:

public function previewData(Request $request)
{
    $request->validate([
        'mapping' => 'required|array',
        'temp_file' => 'required|string',
    ]);

    $file = storage_path('app/' . $request->temp_file);
    $collection = Excel::toCollection(null, $file);
    $rows = $collection[0]->slice(1, 10); // Skip heading row

    $preview = $rows->map(function ($row) use ($request) {
        $data = [];
        foreach ($request->mapping as $excelHeader => $field) {
            $data[$field] = $row[$excelHeader] ?? null;
        }
        return $data;
    });

    return view('users.import.preview-data', [
        'preview' => $preview,
        'mapping' => $request->mapping,
        'temp_file' => $request->temp_file,
    ]);
}

And show the preview (resources/views/users/import/preview-data.blade.php):

<form method="POST" action="{{ route('users.import.final') }}">
    @csrf
    <input type="hidden" name="temp_file" value="{{ $temp_file }}">
    @foreach ($mapping as $excel => $field)
        <input type="hidden" name="mapping[{{ $excel }}]" value="{{ $field }}">
    @endforeach
    <table border="1" cellpadding="5">
        <thead>
            <tr>
                @foreach (array_keys($preview->first() ?? []) as $field)
                    <th>{{ ucfirst($field) }}</th>
                @endforeach
            </tr>
        </thead>
        <tbody>
            @foreach ($preview as $row)
                <tr>
                    @foreach ($row as $value)
                        <td>{{ $value }}</td>
                    @endforeach
                </tr>
            @endforeach
        </tbody>
    </table>
    <button type="submit">Confirm & Import</button>
</form>

Step 5: Final Import With Mapping

Back in your controller:

use App\Imports\UsersImport;

public function finalImport(Request $request)
{
    $request->validate([
        'mapping' => 'required|array',
        'temp_file' => 'required|string',
    ]);

    $filePath = storage_path('app/' . $request->temp_file);
    Excel::import(new UsersImport($request->mapping), $filePath);

    return redirect()->route('users.import.form')->with('success', 'Data imported successfully!');
}

Import Class

use App\Models\User;
use Maatwebsite\Excel\Concerns\ToModel;
use Maatwebsite\Excel\Concerns\WithHeadingRow;

class UsersImport implements ToModel, WithHeadingRow
{
    protected array $mapping;

    public function __construct(array $mapping)
    {
        $this->mapping = $mapping;
    }

    public function model(array $row): User
    {
        return new User([
            'name'  => $row[$this->getHeader('name')] ?? null,
            'email' => $row[$this->getHeader('email')] ?? null,
            'phone' => $row[$this->getHeader('phone')] ?? null,
        ]);
    }

    protected function getHeader(string $field): ?string
    {
        return collect($this->mapping)->flip()->get($field);
    }
}

You can even expand this logic to support conditional mapping, transformation, and per-field validation.


Optional Enhancements

  • Use WithValidation interface to validate each row

  • Queue large imports using ShouldQueue + WithChunkReading

  • Save and reuse mapping templates

Final Thoughts

Excel import is a common requirement, but hardcoding column names makes your system fragile. By giving users the power to map Excel headers to database fields, and preview their data before importing, you:

  • Prevent bad data from going into your system

  • Build trust with users

  • Make your Laravel app enterprise-ready

This approach works beautifully for admin dashboards, CRMs, ERP software, and any system dealing with external data.