Introduction
When building modern web applications, handling file uploads and data downloads is a ubiquitous requirement. Whether you are generating monthly financial reports for stakeholders or enabling users to batch-upload system inventories, dealing with spreadsheet files can quickly turn into an engineering bottleneck. Fortunately, the laravel maatwebsite excel package (officially known as Laravel Excel) acts as an elegant, Laravel-flavored wrapper around PhpSpreadsheet. It simplifies exports and imports into a clean, fluent syntax that integrates seamlessly with Eloquent, Laravel's validation engine, and the queue system.
In this comprehensive guide, we will explore everything you need to master maatwebsite excel in laravel, from installation across multiple Laravel versions to advanced formatting, robust import validation, multi-sheet handling, and processing massive multi-million row datasets. Whether you are maintaining legacy systems using maatwebsite excel laravel 7, migrating projects on laravel 8 maatwebsite excel, or working on modern deployments using maatwebsite excel laravel 9 and newer (including Laravel 10 and 11), this tutorial provides the production-ready code you need.
1. Architectural Evolution: Maatwebsite Excel 2.x vs 3.1
Before diving into installation, it is helpful to understand how the architecture of this library has evolved. If you are updating a legacy application or looking up tutorials online, you will frequently find outdated guides written for Laravel Excel 2.x.
In the old 2.x version, operations were highly procedural. Code was typically crammed into controllers using heavy closures, like this:
// Outdated 2.1 syntax - Do not use this in modern projects!
Excel::create('users', function($excel) {
$excel->sheet('Sheet 1', function($sheet) {
$sheet->fromArray(User::all()->toArray());
});
})->download('xlsx');
This old pattern suffered from major issues. It loaded all data directly into memory at once, causing memory leaks on moderate-sized datasets. It was also difficult to unit test and lacked encapsulation.
Laravel Excel 3.x completely reimagined this. Every import or export is now encapsulated in its own clean PHP class, called a Concern-based object. Instead of procedural configurations, you implement interfaces (known in this library as 'Concerns') like FromQuery, WithHeadings, and WithMapping. This decoupled architecture drastically increases code reuse, enables automated performance optimization (like automated chunking), and allows exports and imports to be tested in isolation.
2. Installation & Multi-Version Configuration
Setting up the package is straightforward, but composer dependency constraints can occasionally cause friction depending on which version of PHP and Laravel you are running.
Step-by-Step Installation
To add the package to your project, run the following composer command:
composer require maatwebsite/excel
This command automatically grabs the latest stable release and downloads its underlying engine, phpoffice/phpspreadsheet.
Resolving Dependency Conflicts
If you are working on older systems like maatwebsite excel laravel 7 or upgrading to newer frameworks like Laravel 10/11, you may encounter a 'Your requirements could not be resolved to an installable set of packages' error. This is often caused by mismatched PHP versions or conflicting spreadsheet dependencies. You can resolve this by running:
composer require maatwebsite/excel --with-all-dependencies
Configuration and Service Providers
In older installations (such as maatwebsite excel in laravel 8 or Laravel 7), you might need to register the service provider and facade manually if auto-discovery fails. Open config/app.php and add the following lines:
'providers' => [
/* ... other providers ... */
Maatwebsite\Excel\ExcelServiceProvider::class,
],
'aliases' => [
/* ... other aliases ... */
'Excel' => Maatwebsite\Excel\Facades\Excel::class,
]
Note: If you are using Laravel 11, provider registration has been simplified. You do not need to edit config/app.php unless you have manually disabled package auto-discovery.
To publish the configuration file to customize formatting, CSV delimiters, or default temporary storage disks, run:
php artisan vendor:publish --provider="Maatwebsite\Excel\ExcelServiceProvider" --tag=config
This creates a config/excel.php file, which allows you to fine-tune your spreadsheet behaviors.
3. Crafting Professional Exports with Custom Formatting
Many tutorials show you how to export a raw, unstyled database table, but in production, clients demand readable, professional, and well-structured reports. We will build a highly customized export class that structures data, maps columns cleanly, styles the headers, and formats cell values.
Step 1: Create the Export Class
Generate a dedicated export file using the Artisan CLI. Separating your export logic from your controller keeps your codebase clean and testable:
php artisan make:export UsersExport --model=User
This command creates a skeleton file in app/Exports/UsersExport.php.
Step 2: Implement Advanced Concerns
By default, the generator creates a class using FromCollection. While this is fine for small tables, loading thousands of Eloquent models directly into memory can easily exceed your server's memory limits. To scale efficiently, we will use the FromQuery concern. We will also implement WithHeadings for header rows, WithMapping to control column order, WithColumnFormatting for cell data formats, and WithStyles to give our sheet a polished look.
Update your UsersExport.php to match the following implementation:
namespace App\Exports;
use App\Models\User;
use Maatwebsite\Excel\Concerns\FromQuery;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\WithMapping;
use Maatwebsite\Excel\Concerns\WithColumnFormatting;
use Maatwebsite\Excel\Concerns\WithStyles;
use PhpOffice\PhpSpreadsheet\Shared\Date;
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
class UsersExport implements FromQuery, WithHeadings, WithMapping, WithColumnFormatting, WithStyles
{
/**
* Pull the query builder instead of fetching all records into memory at once.
* This allows Laravel Excel to chunk your records automatically under the hood.
*/
public function query()
{
return User::query()->where('is_active', true);
}
/**
* Define the headers of the generated Excel sheet.
*/
public function headings(): array
{
return [
'ID',
'Full Name',
'Email Address',
'Registration Date',
];
}
/**
* Map database columns to specific spreadsheet rows.
* Use PhpSpreadsheet's helper to convert model dates into Excel-serial dates.
*/
public function map($user): array
{
return [
$user->id,
$user->name,
$user->email,
Date::dateTimeToExcel($user->created_at),
];
}
/**
* Apply correct cell formats. Column 'D' (Registration Date) is formatted as YYYY-MM-DD.
*/
public function columnFormats(): array
{
return [
'D' => NumberFormat::FORMAT_DATE_YYYYMMDD2,
];
}
/**
* Apply elegant styling directly to our worksheet.
*/
public function styles(Worksheet $sheet)
{
return [
// Row 1 (Header) styled with bold text, larger size, and a light-gray background
1 => [
'font' => [
'bold' => true,
'size' => 12
],
'fill' => [
'fillType' => \PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID,
'startColor' => ['rgb' => 'F2F2F2']
]
],
];
}
}
Step 3: Trigger the Export in Your Controller
Next, create a controller to execute this export and return it as a direct download:
namespace App\Http\Controllers;
use App\Exports\UsersExport;
use Maatwebsite\Excel\Facades\Excel;
class UserController extends Controller
{
public function export()
{
return Excel::download(new UsersExport, 'active_users.xlsx');
}
}
Add your route to routes/web.php to expose this action:
use App\Http\Controllers\UserController;
Route::get('/users/export', [UserController::class, 'export'])->name('users.export');
When you hit this route, the package compiles the query, streams it through your formatted export file, and initiates a secure browser download.
Writing Dynamic Formulas
To compute calculations dynamically within Excel rather than performing raw PHP math, you can inject formulas into your row mapping array. If you have a column for price, tax rate, and final cost, you can write Excel formulas directly into your cells:
public function map($invoice): array
{
static $rowNumber = 1;
$rowNumber++;
return [
$invoice->sku,
$invoice->subtotal,
$invoice->tax_rate,
'=B' . $rowNumber . '*C' . $rowNumber // Computes the tax amount dynamically inside Excel
];
}
4. High-Performance Imports with Row-by-Row Data Validation
While exporting is relatively simple, executing a laravel maatwebsite excel import safely requires a lot more caution. Standard files provided by users are notorious for having missing values, invalid email formats, and unexpected data structures. If you process imports blindly, you will run into database constraints, duplicate keys, or catastrophic application crashes.
Let us build a robust importer that validates incoming rows, reports errors gracefully, and ignores invalid rows without crashing the entire import script.
Step 1: Create the Import Class
Run the following Artisan command to generate your importer:
php artisan make:import UsersImport --model=User
Step 2: Implement Validation Concerns
We will make the importer incredibly robust by applying:
WithHeadingRow: Allows referencing columns by their textual header instead of numeric array indexes.WithValidation: Validates each row using standard Laravel validation syntax.SkipsOnFailure: Collects validation failures gracefully instead of throwing exceptions that stop the import execution.
Here is the production-ready code for app/Imports/UsersImport.php:
namespace App\Imports;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rule;
use Maatwebsite\Excel\Concerns\ToModel;
use Maatwebsite\Excel\Concerns\WithHeadingRow;
use Maatwebsite\Excel\Concerns\WithValidation;
use Maatwebsite\Excel\Concerns\SkipsFailures;
use Maatwebsite\Excel\Concerns\SkipsOnFailure;
class UsersImport implements ToModel, WithHeadingRow, WithValidation, SkipsOnFailure
{
use SkipsFailures;
/**
* Convert each spreadsheet row into a database record.
* Column names correspond directly to your spreadsheet headers (converted to snake_case).
*/
public function model(array $row)
{
return new User([
'name' => $row['name'],
'email' => $row['email'],
'password' => Hash::make($row['password'] ?? 'Temporary123!'),
]);
}
/**
* Set validation rules for each row.
* 'email' checks against standard format and enforces database uniqueness.
*/
public function rules(): array
{
return [
'name' => 'required|string|max:100',
'email' => 'required|email|unique:users,email',
];
}
/**
* Custom validation messages (optional, but highly recommended for clear UX).
*/
public function customValidationMessages()
{
return [
'email.unique' => 'The email address :input is already registered in our system.',
'name.required' => 'The name field cannot be empty.',
];
}
}
Step 3: Handle Import Failures in Your Controller
When executing this import in your controller, you can catch the collected validation errors and display them on your frontend. This provides high-quality, self-service feedback to your users so they can fix their spreadsheet data and re-upload.
namespace App\Http\Controllers;
use App\Imports\UsersImport;
use Illuminate\Http\Request;
use Maatwebsite\Excel\Facades\Excel;
class UserImportController extends Controller
{
public function import(Request $request)
{
// 1. Validate that a file is actually uploaded and is of the right format
$request->validate([
'excel_file' => 'required|mimes:xlsx,xls,csv|max:10240', // Limit to 10MB
]);
$import = new UsersImport();
// 2. Perform the import
Excel::import($import, $request->file('excel_file'));
// 3. Inspect validation failures
if ($import->failures()->isNotEmpty()) {
return back()->with('import_failures', $import->failures());
}
return back()->with('success', 'Excel data successfully imported!');
}
}
Step 4: Render Failures on Your Frontend
In your Blade template, you can loop through these failures to tell the user exactly where they went wrong:
@if (session()->has('import_failures'))
<div class="alert alert-danger">
<h4>Some rows could not be imported:</h4>
<ul>
@foreach (session()->get('import_failures') as $validationFailure)
<li>
<strong>Row {{ $validationFailure->row() }}:</strong>
{{ implode(', ', $validationFailure->errors()) }}
(Submitted values: <em>{{ json_encode($validationFailure->values()) }}</em>)
</li>
@endforeach
</ul>
</div>
@endif
This prevents bad data from corrupting your database, ensures user-friendliness, and keeps your backend secure.
5. Advanced Import Handling: Multi-Sheet Support
Often, a spreadsheet uploaded by a business team contains multiple sheets containing different types of structured data—for example, a sheet for 'Clients' and another for 'Orders'. Importing this manually cell-by-cell would be extremely tedious.
To handle multi-sheet imports cleanly, we can implement the WithMultipleSheets concern. This allows us to route separate sheets to entirely independent importer classes, keeping our codebase clean and strictly single-responsibility.
Creating the Multi-Sheet Router Class
Create a root importer class, such as app/Imports/CompanyDataImport.php:
namespace App\Imports;
use Maatwebsite\Excel\Concerns\WithMultipleSheets;
class CompanyDataImport implements WithMultipleSheets
{
/**
* Map sheet names to specific import classes.
*/
public function sheets(): array
{
return [
'Clients' => new ClientsImport(),
'Orders' => new OrdersImport(),
];
}
}
In this setup, ClientsImport and OrdersImport are standard importer files implementing ToModel (and optionally WithValidation and WithHeadingRow). You trigger this import inside your controller exactly the same way:
Excel::import(new CompanyDataImport, $request->file('excel_file'));
This architecture keeps your code incredibly clean and makes handling multi-faceted business files highly manageable.
6. Scaling Imports & Exports for Massive Datasets
When you work with massive spreadsheets—such as an inventory of 200,000 products—processing everything in a single web request will trigger PHP's max_execution_time limits or exhaust the server's PHP memory allocation. To keep your application blazing fast and secure under high load, you must integrate chunking, batch inserts, and background queueing.
Optimizing Imports: Chunking and Batching
To scale imports, we combine WithChunkReading and WithBatchInserts:
- WithChunkReading: Reads the Excel file in small, manageable pieces (e.g., 500 rows at a time). This keeps the memory usage of the underlying parser extremely low.
- WithBatchInserts: Batches the Eloquent database queries. Instead of executing 500 individual
INSERTqueries, Laravel Excel wraps them into a single fast SQL statement.
Modify your import class to implement these performance-oriented interfaces:
namespace App\Imports;
use App\Models\Product;
use Maatwebsite\Excel\Concerns\ToModel;
use Maatwebsite\Excel\Concerns\WithChunkReading;
use Maatwebsite\Excel\Concerns\WithBatchInserts;
class ProductsLargeImport implements ToModel, WithChunkReading, WithBatchInserts
{
public function model(array $row)
{
return new Product([
'sku' => $row[0],
'title' => $row[1],
'price' => $row[2],
]);
}
/**
* Determine how many rows to insert in a single SQL query.
*/
public function batchSize(): int
{
return 1000;
}
/**
* Determine how many rows to read into memory from the file at once.
*/
public function chunkSize(): int
{
return 1000;
}
}
Optimizing Exports: Queueing with Background Jobs
For heavy database queries during exports, do not make your users wait for a loading spinner. Instead, push the process to your Laravel queue worker by implementing the ShouldQueue contract.
namespace App\Exports;
use App\Models\Invoice;
use Illuminate\Contracts\Queue\ShouldQueue;
use Maatwebsite\Excel\Concerns\FromQuery;
use Maatwebsite\Excel\Concerns\Exportable;
class InvoicesLargeExport implements FromQuery, ShouldQueue
{
use Exportable;
public function query()
{
return Invoice::query()->where('status', 'paid');
}
}
In your controller, you can initiate the queued export like this:
public function queueExport()
{
(new InvoicesLargeExport)->store('exports/invoices_report.xlsx', 'public');
return back()->with('status', 'Your large export has started in the background. We will notify you when it is ready for download!');
}
Behind the scenes, Laravel Excel chunks your query, dispatches multiple background jobs, compiles the final spreadsheet on your server's storage disk, and frees up your user's browser immediately.
7. Troubleshooting Common Pitfalls and FAQs
Why is Composer trying to install version 1.1 instead of 3.x?
This usually happens when your environment's PHP configuration or Laravel version has a conflict with PhpSpreadsheet. Since PhpSpreadsheet requires specific PHP extensions (like ext-zip, ext-xml, ext-gd), Composer fallback patterns might incorrectly suggest ancient versions of the library. Make sure your local and production environments have these dependencies enabled:
php_zipphp_xmlphp_gd2php_iconv
Verify this by running php -m on your terminal, then try installing again with the --with-all-dependencies flag.
Why do my imported dates look like random integers (e.g., "44197")?
Excel stores dates internally as sequential serial numbers counting the days since January 1, 1900. When importing, you need to parse this number back into a native PHP DateTime object. You can easily achieve this using PhpSpreadsheet's built-in helper:
$dateTimeObject = \PhpOffice\PhpSpreadsheet\Shared\Date::excelToDateTimeObject($row['created_at']);
Can I write directly into an existing Excel template?
Yes. If you have a pre-designed spreadsheet with complex formulas, charts, and brand styling, you can load the template, write data to specific coordinates, and download the updated version. This is accomplished using events:
use Maatwebsite\Excel\Concerns\WithEvents;
use Maatwebsite\Excel\Events\BeforeExport;
class TemplateExport implements WithEvents
{
public function registerEvents(): array
{
return [
BeforeExport::class => function(BeforeExport $event) {
$event->writer->reopen(new \Maatwebsite\Excel\Files\LocalTemporaryFile(storage_path('templates/report.xlsx')), \Maatwebsite\Excel\Excel::XLSX);
$sheet = $event->writer->getSheetByIndex(0);
$sheet->setCellValue('B2', 'Dynamic Monthly Report');
},
];
}
}
How do I export simple arrays without Eloquent models?
If you are processing highly customized datasets, API results, or aggregate queries that do not map directly to Eloquent models, use the FromArray concern instead of FromQuery or FromCollection:
namespace App\Exports;
use Maatwebsite\Excel\Concerns\FromArray;
class CustomDataExport implements FromArray
{
protected $data;
public function __construct(array $data)
{
$this->data = $data;
}
public function array(): array
{
return $this->data;
}
}
Conclusion
The laravel maatwebsite excel library is an indispensable tool in the modern Laravel ecosystem. By shifting from loading full collections to using query-based chunking, enforcing validation logic with user-friendly error views, and pushing massive tasks to background queues, you can deliver a smooth, resilient experience to your users.
Implement these patterns in your next deployment to keep your memory footprint low, your database inputs secure, and your spreadsheets looking visually flawless.










