Импорт XLSX в модели Eloquent за три строки: заголовки и маппинг колонок
Импорт Excel-файла в базу почти всегда пишется по одному сценарию: открыть файл, пройти циклом по строкам, для каждой строки руками разобрать ячейки по буквам колонок, собрать массив, создать модель, сохранить. Кода получается много, и он хрупкий: стоит пользователю поменять порядок колонок или прислать файл с лишним столбцом — и всё едет.
avadim/fast-excel-laravel (версия 4.2) убирает эту рутину. Разберём три способа импорта — от самого короткого до гибкого маппинга — и честно обозначим, где заканчиваются возможности «из коробки».
Базовый случай: первая строка — имена полей
Если первая строка файла содержит заголовки, совпадающие с именами атрибутов модели, импорт занимает буквально пару строк:
// Формат (XLSX, устаревший XLS или CSV) определяется по сигнатуре файла
$excel = \Excel::open(storage_path('imports/users.xlsx'));
// Первая строка — имена полей, остальные строки создают модели
$excel->withHeadings()->importModel(User::class);
Что здесь происходит. withHeadings() без аргументов включает режим «первая строка — ключи»: её значения становятся именами атрибутов, а сама она в данные не попадает. importModel(User::class) для каждой оставшейся строки создаёт экземпляр модели, заполняет его через fill() и вызывает save().
Важное следствие: работает штатный $fillable модели. Атрибуты, не перечисленные в $fillable (или отсечённые $guarded), не будут заполнены — это ровно то же поведение, что у обычного Model::create(), и оно защищает от массового присвоения лишних полей из файла.
Когда заголовков в файле нет или они «не те»
Реальные файлы редко идеальны. Заголовки бывают на русском, с пробелами, в другом регистре — словом, не совпадают с именами колонок в базе. На этот случай withHeadings() принимает свои имена атрибутов в порядке колонок:
// Первая строка файла всё равно пропускается (там «человеческие» заголовки),
// но её значения игнорируются — вместо них берутся эти имена, слева направо
$excel->withHeadings(['name', 'birthday', 'email'])
->importModel(User::class);
Здесь первая колонка ляжет в name, вторая — в birthday, третья — в email, как бы они ни назывались в шапке файла.
Можно также ограничить область импорта — указать диапазон колонок или левую верхнюю ячейку данных:
// Только колонки A и B
$excel->withHeadings()->importModel(User::class, 'A:B');
// Данные начинаются с ячейки B4 (первая строка области — заголовки)
$excel->withHeadings()->importModel(User::class, 'B4');
// Прямоугольная область
$excel->withHeadings()->importModel(User::class, 'B4:D7');
Гибкая привязка: mapping()
withHeadings() хорош, когда порядок колонок соответствует нужным полям один-к-одному. Но часто нужно больше: взять не все колонки, поменять их местами, склеить два поля в одно, привести тип. Для этого есть mapping() — в двух формах.
Форма первая — массив соответствий «колонка → атрибут». Компактно, когда нужна просто перестановка:
$excel->mapping(['B' => 'name', 'C' => 'birthday', 'D' => 'email'])
->importModel(User::class, 'B5');
// То же самое, но короче — маппинг третьим аргументом
$excel->importModel(User::class, 'B5', ['B' => 'name', 'C' => 'birthday', 'D' => 'email']);
Форма вторая — колбэк. Даёт полный контроль над строкой: получаете сырой массив ячеек (ключи — буквы колонок), возвращаете готовый массив атрибутов модели.
$excel->mapping(function (array $record) {
return [
'name' => trim($record['B']),
'birthday' => new \Carbon\Carbon($record['C']),
'email' => mb_strtolower($record['D']),
];
})->importModel(User::class, 'B:D');
Колбэк — место, где удобно нормализовать данные до записи: обрезать пробелы, привести дату к Carbon, привести регистр e-mail, преобразовать «да/нет» в булево. Массив, который вернёт колбэк, попадёт в fill().
Когда нужно больше контроля: ручной цикл
importModel() рассчитан на одну модель на строку. Если из одной строки файла надо создать несколько связанных записей (например, пользователя и его заказ) или встроить сложную логику, спускайтесь на уровень чтения строк и пишите цикл сами:
$excel = \Excel::open($file);
$sheet = $excel->sheet('Articles');
$sheet->setReadArea('B5');
foreach ($sheet->nextRow() as $rowData) {
$user = User::create([
'name' => $rowData['B'],
'birthday' => new \Carbon\Carbon($rowData['C']),
'password' => bcrypt($rowData['D']),
]);
Article::create([
'user_id' => $user->id,
'title' => $rowData['E'],
'public' => $rowData['F'] === 'yes',
]);
}
nextRow() — генератор: он отдаёт строки по одной и не держит весь лист в памяти. Поэтому такой цикл спокойно переваривает большие файлы, оставаясь потоковым — как и importModel().
Файл не обязан лежать на диске
Пользовательский файл редко оказывается на локальном диске в удобном месте: он приходит загрузкой, лежит в S3 через Storage или вообще хранится BLOB'ом в базе. Класть его во временный файл руками не нужно — кроме open() есть ещё два входа:
// Книга из строки: Storage::get(), BLOB из базы, тело HTTP-ответа
\Excel::openString(Storage::get('imports/users.xlsx'))
->withHeadings()
->importModel(User::class);
// Книга из потока: Storage::readStream(), fopen('https://...'), php://memory
\Excel::openStream(Storage::readStream('imports/users.xlsx'))
->withHeadings()
->importModel(User::class);
Дальше всё как обычно: формат определяется по содержимому, работают области чтения, withHeadings(), mapping() и importModel(). Временную копию библиотека удаляет сама при завершении скрипта, а переданный поток не закрывает — закрыть его ваша забота.
Честные границы
Чтобы импорт не принёс сюрпризов в проде, держите в голове, чего этот API не делает сам:
- Нет встроенной валидации строк.
importModel()заполняет модель и сохраняет — он не проверяет типы, обязательность или уникальность и не собирает отчёт об ошибках «строка N, поле X». Если данные грязные, валидируйте их сами: удобное место — колбэкmapping()или ручной цикл, где можно прогнать строку черезValidator::make()и решить, пропустить её или прервать импорт. - Сохранение построчно. Каждая строка — это
save(), то есть отдельный запрос. Для очень больших файлов это медленнее пакетной вставки; при необходимости оберните импорт в транзакцию сами (DB::transaction(...)), чтобы частично загруженный файл не оставил базу в несогласованном состоянии при сбое на середине. - Срабатывают события Eloquent. Поскольку идёт
save()на каждую модель, отрабатывают наблюдатели и события (creating,savedи т. д.). Это удобно, но на больших импортах учитывайте их стоимость. - Нет постановки в очередь «из коробки». Тяжёлый импорт пользовательского файла лучше выносить в очередь — это вы организуете штатными job'ами Laravel, читая внутри job'а нужную область листа.
Итог
- Совпадают заголовки с полями модели — хватит
withHeadings()->importModel(User::class). - Заголовки «не те» или их нет — передайте свои имена в порядке колонок:
withHeadings(['name', 'birthday', 'email']). - Нужна перестановка, выбор колонок или нормализация —
mapping()массивом или колбэком. - Сложная логика (несколько моделей из строки) — ручной цикл по
nextRow(), тоже потоковый. - Файл пришёл загрузкой или лежит в
Storage—openString()иopenStream()вместоopen(), остальной код не меняется. - Валидацию и транзакции добавляете сами — библиотека отвечает за быстрое чтение, а не за бизнес-правила импорта.
composer require avadim/fast-excel-laravel
Импорт читает XLSX, устаревший XLS и CSV одинаково — формат определяется по содержимому файла, а не по расширению. Репозиторий — github.com/aVadim483/fast-excel-laravel.