Excel-аналог mail-merge: как заполнить готовый XLSX-шаблон данными
В Word есть слияние (mail-merge): дизайнер верстает бланк, расставляет поля, а программа подставляет в них данные из источника. Удобно, потому что вёрстка отделена от данных. Когда та же задача приходит для Excel — «вот бланк договора в xlsx, подставьте туда реквизиты» — разработчик почему-то лезет собирать документ в коде с нуля.
А ведь нужна ровно та же идея, что в Word: шаблон с плейсхолдерами плюс подстановка. Для XLSX её даёт FastExcelTemplator. Всё ниже проверено на версии 3.2.0.
Наивный путь и почему он плохой
Соблазн — открыть готовый файл в PhpSpreadsheet, найти нужные ячейки по адресам и переписать их значения:
$sheet->setCellValue('B3', $company); // а если бланк поправят и B3 переедет?
$sheet->setCellValue('B4', $address); // адреса ячеек захардкожены
Минусов два. Во-первых, вы привязываетесь к конкретным адресам: стоит бухгалтеру вставить строку — и B3 уже не та ячейка. Во-вторых, PhpSpreadsheet ради этого загружает всю книгу в память.
Плейсхолдеры снимают обе проблемы: вы подставляете значение не по адресу, а по метке {{COMPANY}}, где бы она ни оказалась.
Два вида подстановки: fill и replace
В шаблоне размечаем поля любым удобным маркером — например, {{COMPANY}}. Дальше открываем шаблон и задаём замены:
use avadim\FastExcelTemplator\Excel;
$excel = Excel::template('contract-tpl.xlsx', 'contract-out.xlsx');
$sheet = $excel->sheet();
// fill() — заменяет значение ЦЕЛОЙ ячейки
$sheet->fill([
'{{COMPANY}}' => 'ООО «Ромашка»',
'{{INN}}' => '7701234567',
]);
// replace() — заменяет ПОДСТРОКУ внутри значения ячейки
$sheet->replace([
'{{DATE}}' => date('d.m.Y'),
]);
$sheet->transferRows(); // перенести строки шаблона в вывод
$excel->save();
Разница между двумя методами — главные грабли новичка, поэтому разберём подробно.
fill()срабатывает, только если значение ячейки целиком равно ключу. Ячейка со значением{{COMPANY}}— заменится. А вот ячейкаКомпания: {{COMPANY}}черезfill()не изменится, потому что её значение не равно ключу целиком.replace()ищет подстроку в любом месте текста. ЯчейкаДата: {{DATE}}превратится вДата: 25.07.2026— заменяется только вхождение метки, остальной текст остаётся.
Отсюда простое правило: если поле занимает ячейку целиком — берите fill(); если метка сидит внутри фразы — replace(). Когда «плейсхолдер не подставился», в девяти случаях из десяти причина именно эта: fill() применили к ячейке, где метка была частью текста.
Где действуют замены
Замены, заданные через fill() и replace(), применяются ко всем ячейкам, которые библиотека пишет в вывод, — не нужно указывать адреса или листы. Вы один раз объявили словарь замен, а дальше он срабатывает везде, где встретит метку. Это и есть то самое отделение вёрстки от данных: бланк можно перекроить как угодно, лишь бы метки остались на месте.
Значение замены — обычный скаляр: строка, число, дата (заранее отформатированная в строку). Формат самой ячейки (денежный, датовый) берётся из шаблона, так что число подставится и отобразится так, как задумал автор бланка.
Чего плейсхолдеры не делают
Важно не спутать этот механизм с полноценным шаблонизатором вроде Blade или Twig. Плейсхолдеры Templator — это подстановка значений, и только:
- Нет логики в шаблоне. Никаких
{{ if }}, циклов или выражений внутри ячейки — метка либо заменяется на значение, либо нет. - Повторяющиеся строки — это отдельная механика. Если в документе таблица, где строк заранее не известно сколько (позиции счёта, список сотрудников), одними заменами не обойтись: там нужна строка-образец и её размножение (
getRowTemplate()+insertRow()) — это тема отдельного разбора. - Только XLSX. Шаблон в старом
.xlsне подойдёт — сначала пересохраните его в.xlsx. - Формулы библиотека не вычисляет. Если в бланке есть формула, её результат посчитает Excel при открытии файла.
Кому это подходит
Плейсхолдерная подстановка — идеальное решение, когда документ по структуре фиксирован, а меняются только значения: договоры, справки, гарантийные письма, титульные листы, анкеты. Вы отдаёте вёрстку тому, кто умеет в Excel, а в коде оставляете короткий словарь замен. Если же в документе есть таблица переменной длины — к fill()/replace() добавляется размножение строк, о котором стоит почитать отдельно.