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:
- Batch Inserts (
WithBatchInserts): Instead of hitting the database with an individual database query for every single row, chunk SQL operations together. - Chunk Reading (
WithChunkReading): Loads the file into memory in small segments, keeping memory footprints light. - 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.






