Friday, May 22, 2026Today's Paper

Omni Apps

The Ultimate Guide to Laravel 8 Excel Import: Step-by-Step
May 21, 2026 · 12 min read

The Ultimate Guide to Laravel 8 Excel Import: Step-by-Step

Master Laravel 8 excel import using Maatwebsite Excel. Learn to handle massive datasets with chunk reading, queue imports, custom validation, and skip errors.

May 21, 2026 · 12 min read
LaravelPHPWeb DevelopmentBackend Development

Introduction to Excel Imports in Laravel 8

Importing tabular data is a ubiquitous requirement in modern web applications. Whether you are building an HR management system, an e-commerce catalog tool, or a CRM, your users will inevitably ask for the ability to upload bulk data using spreadsheet software like Microsoft Excel or Google Sheets. In a typical application, manual data entry can be a massive bottleneck, whereas a fast, resilient import utility streamlines workflows and scales productivity.

In the Laravel ecosystem, the uncontested standard for processing spreadsheets is the maatwebsite/excel (also known as Laravel Excel) package. It wraps the powerful PhpSpreadsheet library into an elegant, Laravel-friendly syntax.

This comprehensive guide walks you through setting up a robust, enterprise-grade Laravel 8 excel import solution. We won't just cover the basic "hello world" import; we will dive deep into production-ready patterns: heading row mapping, advanced row-by-row validation, background queuing for massive files, and handling import failures gracefully.


Local Setup and Schema Preparation

Before we install our spreadsheet package, we need a working Laravel 8 application linked to a database. Let's configure a clean workspace to avoid configuration collisions.

Step 1: Install a Clean Laravel 8 Application

If you are starting from scratch, generate a new Laravel project using Composer:

composer create-project laravel/laravel:^8.0 laravel-excel-import-demo
cd laravel-excel-import-demo

Configure your database connection in your .env file. Modify the following parameters with your database engine's credentials:

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel_excel_db
DB_USERNAME=root
DB_PASSWORD=your_secure_password

Step 2: Creating the Migration and Model

Let's design a target table named customers with fields for name, email, phone, company, and status. This gives us a richer schema to demonstrate validation (such as unique emails and status state checks) rather than a simple, generic user table.

Generate a Customer model and its corresponding database migration:

php artisan make:model Customer -m

Open the newly created migration file in database/migrations/xxxx_xx_xx_xxxxxx_create_customers_table.php and define the schema as follows:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateCustomersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('customers', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('email')->unique();
            $table->string('phone')->nullable();
            $table->string('company');
            $table->string('status')->default('active');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('customers');
    }
}

Apply the migration using the artisan command:

php artisan migrate

Next, ensure our Customer model is ready to receive mass-assigned parameters. Open app/Models/Customer.php and add the $fillable array:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Customer extends Model
{
    use HasFactory;

    protected $fillable = [
        'name',
        'email',
        'phone',
        'company',
        'status',
    ];
}

Step 1: Installing and Configuring Maatwebsite/Excel

The Laravel Excel package by Spartner (formerly Maatwebsite) simplifies PhpSpreadsheet integration. To pull this package into your Laravel 8 project, run the following Composer command:

composer require maatwebsite/excel

Laravel 8 utilizes package auto-discovery, meaning the service provider and Excel facade are registered automatically. However, to customize the package configurations—such as changing temporary storage paths, setting default file extensions, or defining CSV parsing delimiters—you should publish the configuration file:

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

This command generates a config/excel.php file containing comprehensive options. For example, you can adjust the delimiter used for importing flat CSV files directly in this file, or set up cell-formatting engines.


Step 2: Creating a Basic Import Class with Heading Mapping

Instead of writing file parsing logic inside your controller (which quickly becomes bloated and hard to test), Laravel Excel promotes dedicated "Import" classes located in the app/Imports namespace.

Generate an import class using the built-in artisan command:

php artisan make:import CustomersImport --model=Customer

By default, this command generates a simple import class implementing the ToModel concern. However, relying on hardcoded numeric indexes ($row[0], $row[1]) is fragile; if your users shift the column order, the import breaks or, worse, maps values to the wrong fields.

To solve this, we will implement the WithHeadingRow concern. This tells the package to treat the first row of your spreadsheet as the "heading row" and map columns to associative array keys.

Open app/Imports/CustomersImport.php and refactor it as follows:

<?php

namespace App\Imports;

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

class CustomersImport implements ToModel, WithHeadingRow
{
    /**
     * Map each row of the file to a Customer model.
     *
     * @param array $row
     * @return \Illuminate\Database\Eloquent\Model|null
     */
    public function model(array $row)
    { 
        // Note: WithHeadingRow automatically normalizes headers to snake_case
        return new Customer([
            'name'    => $row['name'],
            'email'   => $row['email'],
            'phone'   => $row['phone'] ?? null,
            'company' => $row['company'],
            'status'  => $row['status'] ?? 'active',
        ]);
    }
}

Understanding Header Normalization

When WithHeadingRow is active, Laravel Excel automatically formats your columns. A spreadsheet column named "Customer Name" is translated into the array key $row['customer_name']. If your spreadsheet columns use accent marks or spaces, they will be transformed into slugified snake_case keys. Make sure your mapping arrays match this styling.


Step 3: Developing Routes, Controller, and Upload View

Now, let's build the routing and user interface layer so users can upload their files directly through their web browser.

1. Creating the Controller

Generate an HTTP controller to orchestrate the import process:

php artisan make:controller CustomerController

Within app/Http/Controllers/CustomerController.php, implement the methods for rendering the upload form and processing the file submission:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Imports\CustomersImport;
use Maatwebsite\Excel\Facades\Excel;

class CustomerController extends Controller
{
    /**
     * Render the import upload form.
     */
    public function index()
    {
        return view('import');
    }

    /**
     * Process the uploaded file.
     */
    public function import(Request $request)
    {
        // Validate that a file has been uploaded and matches expected extensions
        $request->validate([
            'file' => 'required|mimes:xlsx,xls,csv|max:10240', // Max file size of 10MB
        ]);

        try {
            Excel::import(new CustomersImport, $request->file('file'));
            return redirect()->back()->with('success', 'Customers imported successfully!');
        } catch (\Exception $e) {
            return redirect()->back()->with('error', 'There was an error importing your file: ' . $e->getMessage());
        }
    }
}

2. Registering the Routes

Add the target routes to routes/web.php so your application can receive requests:

<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\CustomerController;

Route::get('/import-customers', [CustomerController::class, 'index'])->name('import.index');
Route::post('/import-customers', [CustomerController::class, 'import'])->name('import.store');

3. Crafting the Blade Template

Create a new file resources/views/import.blade.php. This view displays success or error messages and renders a neat upload form:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Laravel 8 Excel Import Tutorial</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css">
</head>
<body class="bg-light">
    <div class="container mt-5">
        <div class="row justify-content-center">
            <div class="col-md-8">
                <div class="card shadow">
                    <div class="card-header bg-primary text-white">
                        <h4 class="mb-0">Import Customers Excel/CSV</h4>
                    </div>
                    <div class="card-body">
                        {{-- Success Alert --}}
                        @if(session('success'))
                            <div class="alert alert-success">
                                {{ session('success') }}
                            </div>
                        @endif

                        {{-- Generic Error Alert --}}
                        @if(session('error'))
                            <div class="alert alert-danger">
                                {{ session('error') }}
                            </div>
                        @endif

                        {{-- Form Validation Errors --}}
                        @if($errors->any())
                            <div class="alert alert-danger">
                                <ul class="mb-0">
                                    @foreach($errors->all() as $error)
                                        <li>{{ $error }}</li>
                                    @endforeach
                                </ul>
                            </div>
                        @endif

                        <form action="{{ route('import.store') }}" method="POST" enctype="multipart/form-data">
                            @csrf
                            <div class="mb-3">
                                <label for="file" class="form-label">Choose Excel/CSV File</label>
                                <input type="file" class="form-control" id="file" name="file" required>
                                <div class="form-text">Allowed formats: xlsx, xls, csv. Maximum size: 10MB.</div>
                            </div>
                            <div class="d-grid">
                                <button type="submit" class="btn btn-primary">Upload and Import</button>
                            </div>
                        </form>
                    </div>
                </div>
            </div>
        </div>
    </div>
</body>
</html>

Important Tip: Never omit enctype="multipart/form-data" on your <form> tag. Without this attribute, files uploaded via filesystems will not carry any binary data, causing PHP to register an empty payload.


Step 4: Adding Validation and Failure Handling (Production Grade)

Basic imports fail spectacularly when invalid data is introduced. If an Excel file contains 1,000 records, and row 943 contains a duplicate email or missing fields, standard setups throw an unhandled database exception. This leaves your database in a partially-imported state, causing data inconsistency.

Laravel Excel integrates natively with Laravel's validation engine. By implementing the WithValidation concern, we can validate each row using normal rules.

Furthermore, instead of crashing the entire import upon encountering a validation error, we can implement the SkipsOnFailure concern. This lets the system skip invalid rows, complete successful ones, and compile a collection of errors to show the user.

Let's rewrite app/Imports/CustomersImport.php with this production-grade configuration:

<?php

namespace App\Imports;

use App\Models\Customer;
use Maatwebsite\Excel\Concerns\ToModel;
use Maatwebsite\Excel\Concerns\WithHeadingRow;
use Maatwebsite\Excel\Concerns\WithValidation;
use Maatwebsite\Excel\Concerns\SkipsOnFailure;
use Maatwebsite\Excel\Concerns\SkipsFailures;

class CustomersImport implements ToModel, WithHeadingRow, WithValidation, SkipsOnFailure
{
    // The SkipsFailures trait automatically gathers failed validation records
    use SkipsFailures;

    /**
     * @param array $row
     * @return \Illuminate\Database\Eloquent\Model|null
     */
    public function model(array $row)
    { 
        return new Customer([
            'name'    => $row['name'],
            'email'   => $row['email'],
            'phone'   => $row['phone'] ?? null,
            'company' => $row['company'],
            'status'  => $row['status'] ?? 'active',
        ]);
    }

    /**
     * Define validation rules for rows.
     *
     * @return array
     */
    public function rules(): array
    {
        return [
            'name'    => 'required|string|max:255',
            'email'   => 'required|email|unique:customers,email',
            'phone'   => 'nullable|string',
            'company' => 'required|string|max:255',
            'status'  => 'nullable|in:active,inactive',
        ];
    }

    /**
     * Optional: Define custom validation error messages
     *
     * @return array
     */
    public function customValidationMessages()
    {
        return [
            'email.unique' => 'The customer email :input on this row is already registered.',
            'status.in'    => 'The status must be either "active" or "inactive".',
        ];
    }
}

Updating the Controller to Log Failures

Now, let's update the controller's import method to inspect and handle validation errors gracefully:

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

        $import = new CustomersImport;
        
        // Execute the import
        Excel::import($import, $request->file('file'));

        // Check if there are failures
        if ($import->failures()->isNotEmpty()) {
            return redirect()->back()
                ->with('failures', $import->failures())
                ->with('success', 'Import processed with some skipped rows due to validation failures.');
        }

        return redirect()->back()->with('success', 'All customers imported successfully!');
    }

Rendering the Validation Failures Table

Update your resources/views/import.blade.php to display failed rows alongside specific column issues:

                        {{-- Validation Failures Table --}}
                        @if(session('failures'))
                            <div class="card border-danger mb-4">
                                <div class="card-header bg-danger text-white">
                                    <h5 class="mb-0">Validation Failures Detected</h5>
                                </div>
                                <div class="table-responsive">
                                    <table class="table table-striped table-hover mb-0">
                                        <thead>
                                            <tr>
                                                <th>Row Number</th>
                                                <th>Attribute Column</th>
                                                <th>Error Messages</th>
                                                <th>Rejected Value</th>
                                            </tr>
                                        </thead>
                                        <tbody>
                                            @foreach(session('failures') as $failure)
                                                <tr>
                                                    <td>{{ $failure->row() }}</td>
                                                    <td><code>{{ $failure->attribute() }}</code></td>
                                                    <td>
                                                        <ul class="text-danger mb-0">
                                                            @foreach($failure->errors() as $error)
                                                                <li>{{ $error }}</li>
                                                            @endforeach
                                                        </ul>
                                                    </td>
                                                    <td><span class="badge bg-secondary">{{ is_array($failure->values()[$failure->attribute()] ?? '') ? json_encode($failure->values()[$failure->attribute()]) : ($failure->values()[$failure->attribute()] ?? 'N/A') }}</span></td>
                                                </tr>
                                            @endforeach
                                        </tbody>
                                    </table>
                                </div>
                            </div>
                        @endif

This setup allows users to immediately fix specific formatting mistakes in their spreadsheet file and re-upload only the corrected rows without duplicating successfully imported data.


Step 5: Handling Large Datasets with Chunks, Batches, and Queues

When importing spreadsheets with tens of thousands of rows, processing them all within a single HTTP request poses significant risks. It wastes valuable system memory and can cause PHP execution timeouts.

To construct a reliable and scalable pipeline, we can leverage three robust features of Laravel Excel:

  1. Batch Inserts (WithBatchInserts): Instead of hitting the database with an individual database query for every single row, chunk SQL operations together.
  2. Chunk Reading (WithChunkReading): Loads the file into memory in small segments, keeping memory footprints light.
  3. Queues (ShouldQueue): Processes the file entirely in the background, returning a near-instant response to the user.

Creating a Queued Import Class

Let's write a dedicated background import class. Create app/Imports/QueuedCustomersImport.php:

<?php

namespace App\Imports;

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

class QueuedCustomersImport implements ToModel, WithHeadingRow, WithBatchInserts, WithChunkReading, WithValidation, ShouldQueue
{
    /**
     * @param array $row
     * @return \Illuminate\Database\Eloquent\Model|null
     */
    public function model(array $row)
    { 
        return new Customer([
            'name'    => $row['name'],
            'email'   => $row['email'],
            'phone'   => $row['phone'] ?? null,
            'company' => $row['company'],
            'status'  => $row['status'] ?? 'active',
        ]);
    }

    /**
     * Define validation rules.
     */
    public function rules(): array
    {
        return [
            'name'    => 'required|string|max:255',
            'email'   => 'required|email|unique:customers,email',
            'company' => 'required|string|max:255',
        ];
    }

    /**
     * Control how many models are inserted at once.
     */
    public function batchSize(): int
    {
        return 1000;
    }

    /**
     * Control how many rows are loaded into memory per chunk.
     */
    public function chunkSize(): int
    {
        return 1000;
    }
}

Configuring the Queue Backend

To run this import in the background, ensure your queue connection is set up in your .env file:

QUEUE_CONNECTION=database

Generate the system queue database tables and run migrations if you haven't already:

php artisan queue:table
php artisan migrate

To begin listening for and executing imports in the background, open a terminal window and run:

php artisan queue:work

When a user uploads a sheet, Laravel Excel will read the file in chunks of 1,000, parse them, pass them to separate queued jobs, and perform multirow database batch inserts, all while keeping memory consumption steady.


Troubleshooting & FAQ

1. How do I import files using a custom delimiter, like a semicolon?

If you are dealing with semicolons or other custom symbols in CSVs, implement the WithCustomCsvSettings concern in your import class. This opens the getCsvSettings() method:

use Maatwebsite\Excel\Concerns\WithCustomCsvSettings;

class CustomersImport implements ToModel, WithCustomCsvSettings
{
    public function getCsvSettings(): array
    {
        return [
            'delimiter' => ';',
            'enclosure' => '"',
        ];
    }
}

2. Why am I seeing the error: "Class 'Maatwebsite\Excel\ExcelServiceProvider' not found"?

This error occurs when the package isn't properly loaded into Composer or your application classes are heavily cached. First, ensure you installed the correct package using composer require maatwebsite/excel. Afterward, clear bootstrap configuration files cache and test auto-discovery again:

composer dump-autoload
php artisan config:clear
php artisan cache:clear

3. How can I read the Excel file directly into a PHP array without saving to a model?

If you want to validate or interact with raw datasets without mapping directly to model collections, you can leverage the helper methods inside the controller:

use Maatwebsite\Excel\Facades\Excel;
use App\Imports\CustomersImport;

$dataArray = Excel::toArray(new CustomersImport, $request->file('file'));

This reads the entire file into memory as a nested multidimensional array. (Avoid this with files larger than 10MB to protect memory limits).

4. How can I format dates correctly during import?

Excel formats internal storage for date elements as numeric indexes. To parse these correctly inside your import models, use PhpSpreadsheet's helper class:

'created_at' => \PhpOffice\PhpSpreadsheet\Shared\Date::excelToDateTimeObject($row['date_column_name']),

5. What are the best practices for handling duplicates in database imports?

If you want to update records instead of throwing duplicate entry violations, implement the WithUpserts concern. Returning uniqueBy() inside the import class instructs Eloquent on which unique constraints to search for (e.g., 'email').


Conclusion

Implementing a robust Laravel 8 excel import solution is essential for managing bulk datasets in modern web applications. By leveraging the maatwebsite/excel package, you can go beyond basic file uploads to build high-performance pipelines that support snake-cased heading row mapping, advanced row validation, skipped errors, and background processing via queue workers. Incorporating these advanced techniques ensures your application remains fast, stable, and user-friendly, even when processing files with thousands of rows.

Related articles
How to Export Excel in Laravel: The Ultimate High-Performance Guide
How to Export Excel in Laravel: The Ultimate High-Performance Guide
Learn how to export Excel in Laravel 8 through modern versions. Master high-performance chunking, styled Blade views, imports, and queue-based background tasks.
May 22, 2026 · 11 min read
Read →
SVG Code to PNG Online: Convert Vector Markup to Images Instantly
SVG Code to PNG Online: Convert Vector Markup to Images Instantly
Need to convert SVG code to png online? Learn how to turn raw XML markup into high-quality PNG images instantly with our comprehensive guide and tools.
May 22, 2026 · 13 min read
Read →
Image Color Picker Chrome: How to Grab Hex Codes Instantly
Image Color Picker Chrome: How to Grab Hex Codes Instantly
Looking for an image color picker in Chrome? Discover the best native shortcuts, free extensions, and developer hacks to grab HEX codes from any image.
May 22, 2026 · 10 min read
Read →
How to Convert SVG to Transparent Background: The Complete Guide
How to Convert SVG to Transparent Background: The Complete Guide
Learn how to convert SVG to transparent background files. Step-by-step methods to convert SVG to PNG, PNG to SVG, and ICO using ImageMagick, Illustrator, and Inkscape.
May 22, 2026 · 11 min read
Read →
SVG Image to PNG: How to Convert Vector to Raster (and Vice Versa)
SVG Image to PNG: How to Convert Vector to Raster (and Vice Versa)
Learn how to convert an SVG image to PNG without losing quality, scale vector graphics for high-res output, and turn PNGs into SVGs using professional tools.
May 22, 2026 · 12 min read
Read →
WebP to GIF Bulk Converter Guide: Fast, Free Batch Methods
WebP to GIF Bulk Converter Guide: Fast, Free Batch Methods
Need to convert multiple WebP images at once? Learn how to use a webp to gif bulk tool, run powerful command-line scripts, or automate with Python Pillow.
May 22, 2026 · 12 min read
Read →
Find and Replace Text Editor: The Ultimate Guide for Creators
Find and Replace Text Editor: The Ultimate Guide for Creators
Struggling with tedious text updates? Discover the best desktop and online find and replace text editor options, complete with a powerful RegEx guide.
May 22, 2026 · 12 min read
Read →
Reverse Geo IP Lookup: How It Works, Use Cases & Best APIs
Reverse Geo IP Lookup: How It Works, Use Cases & Best APIs
Learn how reverse geo ip lookup bridges the gap between digital IP addresses and physical locations. Explore use cases, mechanics, APIs, and privacy compliance.
May 22, 2026 · 15 min read
Read →
PNG to SVG Path Online: Convert, Generate & Extract Vector Paths
PNG to SVG Path Online: Convert, Generate & Extract Vector Paths
Convert PNG to SVG path online with ease. Learn how to generate clean vector path code from raster images, extract path data, and reverse the process.
May 22, 2026 · 11 min read
Read →
HTTP Password Generator Guide: Web, Browser, and Apache Security
HTTP Password Generator Guide: Web, Browser, and Apache Security
Looking for a secure HTTP password generator? Discover how web-based tools, Safari and Mozilla browser utilities, and Apache htpasswd generators keep you safe.
May 22, 2026 · 13 min read
Read →
Related articles
Related articles