Заполнение бланка счёта из XLSX-шаблона: обзор FastExcelTemplator
Знакомая задача: нужно сгенерировать счёт, акт или накладную. И не «табличку с данными», а именно фирменный бланк — с логотипом в углу, объединёнными ячейками шапки, рамками нужной толщины, денежным форматом с разделителями разрядов и формулой суммы внизу. Бухгалтер такой бланк уже сделал в Excel за пять минут. А теперь его просят «то же самое, но чтобы генерировалось из базы».
Почему наивные подходы буксуют
Первое, что приходит в голову джуну, — взять PhpSpreadsheet и собрать документ ячейка за ячейкой в коде. И тут начинается боль:
$sheet->getStyle('A1:F1')->getFont()->setBold(true)->setSize(14);
$sheet->getStyle('A5:F5')->getBorders()->getAllBorders()
->setBorderStyle(Border::BORDER_THIN);
$sheet->getStyle('E10')->getNumberFormat()->setFormatCode('#,##0.00');
$sheet->mergeCells('A1:D1');
// ...и так ещё сто строк, чтобы повторить то, что уже нарисовано в файле
Вы вручную переписываете кодом оформление, которое уже существует в готовом файле. Любая правка бланка («сделайте шапку синей», «добавьте колонку НДС») превращается в поиск нужных строк в PHP. А если файл большой — PhpSpreadsheet загружает всю книгу в объектную модель и легко упирается в память на десятках тысяч строк.
Второй подход — «открыть файл-образец и подменить в нём значения» — в PhpSpreadsheet тоже возможен, но это снова полная загрузка модели в память со всеми вытекающими.
Идея FastExcelTemplator: шаблон — это обычный XLSX
FastExcelTemplator переворачивает задачу. Оформление остаётся там, где ему место, — в самом файле Excel. Дизайнер или бухгалтер верстает бланк руками, расставляет плейсхолдеры вроде {{COMPANY}}, а код только подставляет данные.
Технически библиотека потоково читает шаблон XML-ридером и тут же переписывает его XML-райтером, по пути заменяя значения ячеек и вставляя строки данных. Она не загружает лист в объектную модель — идёт по нему сверху вниз. Отсюда низкое потребление памяти и высокая скорость даже на больших выгрузках.
Всё в статье проверено на версии 2.4.0.
Разбираем на счёте
Шаг 0. Готовим шаблон
В Excel рисуем бланк как нам нравится и оставляем плейсхолдеры:
- в шапке —
{{COMPANY}},{{ADDRESS}},Счёт от {{DATE}}; - строка 7 — образец строки позиции: колонки
A(номер),B(наименование),C(кол-во),D(цена),E(сумма). В ячейкеE7уже стоит формула=C7*D7; - внизу —
{{TOTAL}}для итога.
Файл со всеми стилями, логотипом и рамками сохраняем как invoice-tpl.xlsx.
Шаг 1. Открываем шаблон и задаём замены
use avadim\FastExcelTemplator\Excel;
$excel = Excel::template('invoice-tpl.xlsx', 'invoice-out.xlsx');
$sheet = $excel->sheet();
// fill() — замена ЦЕЛОЙ ячейки: сработает, только если всё её
// значение равно ключу (ячейка целиком '{{COMPANY}}')
$sheet->fill([
'{{COMPANY}}' => 'ООО «Ромашка»',
'{{ADDRESS}}' => 'Москва, ул. Садовая, 1',
]);
// replace() — замена ПОДСТРОКИ: сработает даже внутри текста
// (ячейка 'Счёт от {{DATE}}' превратится в 'Счёт от 25.07.2026')
$sheet->replace([
'{{DATE}}' => date('d.m.Y'),
]);
Разница между fill() и replace() — самые частые грабли новичка, поэтому запомните сразу:
fill()заменяет значение, только если вся ячейка равна ключу.{{COMPANY}}— заменит;Компания: {{COMPANY}}— не тронет.replace()ищет подстроку в любом месте текста ячейки.
Обе замены применяются ко всем ячейкам, которые библиотека пишет в вывод, — и к переносимым из шаблона, и к вставляемым.
Шаг 2. Переносим шапку
Копируем верх бланка (строки 1–6) из шаблона в вывод как есть:
$sheet->transferRowsUntil(6);
Шаг 3. Размножаем строки таблицы
Берём строку 7 как образец и вставляем по ней столько строк, сколько у нас позиций:
$rowTemplate = $sheet->getRowTemplate(7);
foreach ($positions as $item) {
$sheet->insertRow($rowTemplate, [
'A' => $item['num'],
'B' => $item['name'],
'C' => $item['qty'],
'D' => $item['price'],
// колонку E не трогаем — формула из шаблона встанет сама
]);
}
Ключи массива — это буквы колонок. Каждая вставленная строка получает стили, формат чисел и объединённые ячейки из строки-образца.
Отдельно про формулу. В шаблоне в E7 стоит =C7*D7. При захвате строки-образца библиотека переводит формулу в относительную (RC) запись, поэтому при вставке она пересчитывается на номер целевой строки: в первой позиции получится =C7*D7, в следующей — =C8*D8 и так далее. Вам не нужно генерировать формулы строками в PHP — достаточно один раз поставить её в шаблоне.
Шаг 4. Итог, остаток бланка и сохранение
// Итоговую сумму подставим как обычную замену целой ячейки
$sheet->fill(['{{TOTAL}}' => array_sum(array_column($positions, 'total'))]);
// Переносим оставшиеся строки шаблона (подвал, реквизиты, подпись)
$sheet->transferRows();
$excel->save();
Готово. На выходе — тот же бланк, что нарисовал бухгалтер, но с данными из базы.
Что переносится «бесплатно»
Главная ценность подхода в том, что всё, чего вы не трогали, остаётся нетронутым. Библиотека сохраняет копию исходного XLSX и вставляет в неё только перезаписанные листы, поэтому в выводе уцелевают:
- стили ячеек — шрифты, цвета, заливки, рамки, форматы чисел и дат;
- объединённые ячейки;
- изображения (логотип) и примечания;
- параметры печати, автофильтр, закрепление областей.
Всё это — без единой строчки кода про оформление.
Не только бланки: правка существующих файлов
Тот же движок умеет не вставлять, а модифицировать строки на лету. Метод rows() читает каждую строку, отдаёт её в колбэк и пишет результат:
$sheet->rows(function ($sourceRowNum, $targetRowNum, $rowData) {
// пропустить строку заголовка
if ($sourceRowNum === 1) {
return null; // null — пропустить строку
}
if ($rowData->getValue('A') === 'STOP') {
return false; // false — прекратить обработку
}
// изменить значение ячейки
$rowData->setValue('C', $rowData->getValue('C') * 1.2);
return $rowData; // вернуть изменённую строку
});
$excel->save();
В колбэке доступны и более тонкие операции над строкой: appendCell() — добавить ячейку в конец (со стилями соседа), cloneCell('A', 'E') — скопировать ячейку в другую колонку, removeCells(['B', 'D']) — убрать лишние столбцы.
Отдать файл в браузер
Кроме save() в файл, готовый документ можно сразу отправить пользователю на скачивание или в поток вывода:
$excel->download('Счёт №128.xlsx'); // заголовки + отдача файла в браузер
// output() — псевдоним download() с тем же поведением
$excel->output('Счёт №128.xlsx');
Честные границы
FastExcelTemplator — инструмент под конкретный класс задач, и важно понимать, чего он не делает:
- Только формат XLSX (Office 2007+). Шаблон в старом бинарном
.xlsне подойдёт — весь механизм построен вокруг структуры XLSX (ZIP из XML-частей). Если исходник в.xls, сначала пересохраните его в.xlsx. - Движение только вперёд. Библиотека идёт по листу сверху вниз и пишет вывод потоково. Вернуться и поправить уже записанную строку нельзя — это цена низкого потребления памяти.
- Формулы библиотека не вычисляет. Она записывает формулу как текст (и корректно её перебазирует), а сам результат посчитает Excel при открытии файла. В самом XLSX кэшированного значения формулы не будет, пока его не откроют.
- Нужен готовый шаблон. Это шаблонизатор: чтобы что-то подставить, в файле должны быть плейсхолдеры, а чтобы размножить строку — строка-образец. Строить таблицу с нуля целиком из кода — это задача для родственного
fast-excel-writer. - Это не про чтение данных. Если нужно вытащить данные из чужого Excel, а не сгенерировать свой, смотрите в сторону
fast-excel-reader.
Кому это подходит
FastExcelTemplator — ваш инструмент, если:
- документ имеет фиксированный фирменный вид, который проще нарисовать в Excel, чем описывать кодом: счета, акты, накладные, КП, договоры-приложения;
- в таблице есть повторяющиеся строки с одинаковым оформлением: реестры, спецификации, ведомости;
- нужно массово генерировать однотипные файлы из одного бланка (тысячи счетов в цикле) при скромной памяти;
- оформлением владеет не программист (бухгалтер, аналитик), и хочется, чтобы правки бланка не требовали релиза кода.
Если же вам нужно построить произвольную таблицу целиком из данных — берите fast-excel-writer, а для чтения — fast-excel-reader. Вместе они закрывают весь цикл работы с Excel в PHP.