Из выгрузки в отчёт: стили, автоширина, закреплённая шапка, автофильтр и несколько листов

aVadim • 08.10.2026 13:36 •

Почти каждый PHP-разработчик рано или поздно пишет «экспорт в Excel». И почти всегда первая версия выглядит одинаково: цикл по данным, writeRow(), готово. Файл открывается — и по виду напоминает выгрузку CSV: без шапки, колонки шириной по умолчанию, суммы без разделителей разрядов. Менеджер прокручивает вниз, теряет заголовки из виду и пишет вам: «А что в четвёртой колонке?»

В этой статье мы пошагово превратим сырой массив данных в документ, который не стыдно отправить руководству. Использовать будем avadim/fast-excel-writer — потоковую библиотеку записи XLSX на чистом PHP. Все примеры проверены на версии 6.17. Ниже разобраны отдельные приёмы, а в конце приведён полный запускаемый скрипт.

Сквозной пример — отчёт по продажам за месяц. Отправная точка — «просто данные»:

php
use avadim\FastExcelWriter\Excel;

// В реальном проекте $rows — результат SQL-запроса:
// [дата, менеджер, товар, кол-во, цена, сумма]
$excel = Excel::create(['Продажи']);
$sheet = $excel->sheet();

foreach ($rows as $row) {
    $sheet->writeRow($row);
}

$excel->save('sales.xlsx');

Работает, но выглядит плохо. Начинаем улучшать.

Шаг 1. Шапка таблицы: стиль + запись одной строкой

Стиль в FastExcelWriter — это обычный массив опций: шрифт, заливка, границы, выравнивание. Второй аргумент writeRow() принимает такой массив и применяет его к записываемой строке:

php
$headerStyle = [
    'font-style' => 'bold',
    'font-color' => '#ffffff',
    'fill-color' => '#4472c4',   // фирменный синий
    'text-align' => 'center',
    'border'     => 'thin',
    'height'     => 24,
];

$sheet->writeRow(['Дата', 'Менеджер', 'Товар', 'Кол-во', 'Цена', 'Сумма'], $headerStyle);

Есть и специализированный метод writeHeader() — он записывает строку заголовков и заодно задаёт форматы и стили колонок для последующих строк:

php
$sheet->writeHeader([
    // <заголовок> => [<стиль/формат колонки>]
    'Дата'     => ['format' => '@date',    'width' => 12],
    'Менеджер' => ['format' => '@string',  'width' => 16],
    'Товар'    => ['width' => 'auto'],
    'Кол-во'   => ['format' => '@integer', 'width' => 9],
    'Цена'     => ['format' => '#,##0.00', 'width' => 12],
    'Сумма'    => ['format' => '#,##0.00', 'width' => 14],
], $headerStyle);

Одним вызовом мы получили и оформленную шапку, и форматы колонок — дальше writeRow() можно вызывать с «голыми» данными, форматирование подхватится автоматически. Оформление строки можно задать и цепочкой методов apply...():

php
$sheet->writeRow(['Дата', 'Менеджер', 'Товар', 'Кол-во', 'Цена', 'Сумма'])
    ->applyFontStyleBold()
    ->applyFillColor('#4472c4')
    ->applyFontColor('#ffffff')
    ->applyTextAlign('center')
    ->applyBorder('thin')
    ->applyRowHeight(24);

Такая цепочка оформляет текущую строку, но не задаёт форматы колонок: для них по-прежнему нужен writeHeader() с настройками или отдельные вызовы setColFormats(). Методы apply...() вызывайте сразу после записи, пока строка ещё находится в буфере, до перехода к следующей.

Шаг 2. Ширины колонок и высота строк

Ширины по умолчанию — главная причина «расползшегося» вида. Задать их можно явно, списком или автоподбором:

php
$sheet->setColWidth('C', 40); // одна колонка

// Несколько сразу: и явные значения, и автоширина
$sheet->setColWidths(['A' => 12, 'B' => 16, 'C' => 'auto', 'D' => 9, 'E' => 12, 'F' => 14]);

// Автоширина с нижней границей: по содержимому, но не уже 20
$sheet->setColMinWidth('C', 20);
$sheet->setColWidthAuto('C');

Два нюанса автоширины. Во-первых, включайте её до записи данных: расчёт идёт при выводе строк, а уже сброшенные строки библиотека не перечитывает. Во-вторых, расчёт приблизительный: он учитывает текст, формат и параметры шрифта, но не измеряет реальное отображение в Excel. Для колонок с предсказуемым содержимым — датами, суммами — надёжнее явная ширина, а auto можно оставить для текстовых.

Высота строк задаётся аналогично — при записи ('height' => 24 в стиле строки) или сразу после неё через applyRowHeight(24). Адресные настройки, например setRowHeight(2, 24) и setRowHeights([1 => 30, 2 => 24]), проще задать заранее. Они также работают для текущей строки в буфере, но не для строк, уже сброшенных во временный файл.

Шаг 3. Две строки, которые экономят пользователям больше всего времени

Если из всей статьи вы запомните только один фрагмент — пусть будет этот:

php
$sheet->setFreeze('A3');    // закрепить всё, что выше строки 3
$sheet->setAutoFilter(2);   // автофильтр на строке 2 (строке шапки)

setFreeze('A3') закрепляет строки над указанной ячейкой (а 'B3' закрепил бы ещё и колонку A слева) — при прокрутке пятисот строк шапка остаётся на экране. setAutoFilter(2) вешает на шапку стандартные выпадающие фильтры Excel — менеджер сам отфильтрует продажи по сотруднику, не возвращаясь к вам за «тем же отчётом, но только по Ивановой». Обе настройки — свойства листа, их можно вызвать в любой момент до save().

Здесь предполагается, что строка 1 занята названием отчёта, строка 2 — шапкой таблицы, а данные начинаются со строки 3. Когда передан только номер строки, автофильтр охватывает диапазон от шапки до конца заполненного листа. Если внизу есть итоговая строка, задайте точный диапазон без неё:

php
$lastDataRow = count($rows) + 2;
$sheet->setAutoFilter('A2:F' . $lastDataRow);

Шаг 4. Оформление данных: форматы, зебра, заголовок отчёта

Числовые форматы

Форматы колонок мы уже задали через writeHeader(). Если шапка формируется иначе, то же самое делается отдельными вызовами:

php
$sheet->setColFormat('E', '#,##0.00'); // цена с разделителем разрядов
// или всё сразу, начиная с колонки A (null — формат по умолчанию)
$sheet->setColFormats(['@date', null, null, '@integer', '#,##0.00', '#,##0.00']);

Кроме обычных форматов Excel есть именованные: @date, @datetime, @integer, @money, @string. Вид @money зависит от локали и настроек форматов книги; для сумм без валютного знака в этом отчёте используется явная маска #,##0.00. Форматы колонок задавайте до записи данных.

Зебра-полосы

Готовой опции «banded rows» в библиотеке нет, но есть два честных способа. Первый — чередовать заливку прямо в цикле записи:

php
$i = 0;
foreach ($rows as $row) {
    $sheet->writeRow($row, ($i++ % 2) ? ['fill-color' => '#eef3fb'] : []);
}

Второй — условное форматирование с формулой, одно правило на весь диапазон:

php
use avadim\FastExcelWriter\Conditional\Conditional;

$zebra = Conditional::expression('MOD(ROW(),2)=0', ['fill-color' => '#eef3fb']);
$sheet->addConditionalFormatting('A3:F' . (count($rows) + 2), $zebra);

Заголовок отчёта поверх таблицы

Титульная строка отчёта смотрится лучше, если растянута на всю ширину таблицы. Её нужно записать первой, до шапки и данных. writeTo() с диапазоном записывает значение и сразу объединяет ячейки:

php
$sheet->writeTo('A1:F1', 'Отчёт по продажам — июнь 2026')
    ->applyFontStyleBold()
    ->applyFontSize(14)
    ->applyTextCenter();

Если объединить нужно уже записанные ячейки, есть mergeCells('A1:F1') — операция применима и к строкам, сброшенным в файл: она добавляет описание объединения, а не переписывает значения ячеек. Не записывайте разные данные в ячейки будущего объединения: отображаться будет значение его верхней левой ячейки.

Границы диапазона

Рамку вокруг блока ячеек удобно задавать через withRange(). Этот фрагмент вызовите сразу после writeHeader() для строки 2, до начала записи данных:

php
use avadim\FastExcelWriter\Style\Style;

$sheet->withRange('A2:F2')
    ->applyOuterBorder(Style::BORDER_MEDIUM)
    ->applyInnerBorder(Style::BORDER_THIN);

Для данных чаще достаточно 'border' => 'thin' в стиле строки при записи — сетка появится у каждой записанной ячейки.

withRange() не позволяет оформлять произвольный старый диапазон: его строки должны ещё находиться в буфере или быть впереди текущей позиции записи. Поэтому рамку шапки задаём до перехода к следующей строке.

Шаг 5. Несколько листов: сводка + детализация

Хороший отчёт начинается с итогов, а детали лежат на втором листе. Листы создаются сразу в Excel::create() или добавляются через makeSheet():

php
$excel = Excel::create(['Сводка']);       // книга с листом «Сводка»
$details = $excel->makeSheet('Продажи');  // второй лист
// ... заполняем «Продажи» как выше ...
$lastDataRow = count($rows) + 2;

// Сводный лист: формулы могут ссылаться на другие листы
$summary = $excel->getSheet('Сводка');
$summary->writeRow(
    ['Итого, руб.', "=SUM('Продажи'!F3:F" . $lastDataRow . ')'],
    null,
    ['B' => ['format' => '#,##0.00']]
);

// Какой лист откроется первым
$excel->setActiveSheet('Сводка');

Библиотека не вычисляет формулы сама — их посчитает Excel при открытии файла. В формуле указан только диапазон данных: сумма по всей колонке F:F захватила бы и итоговую строку листа «Продажи», удвоив результат. Третий аргумент writeRow() позволяет задать стиль отдельной ячейке: здесь формат суммы получает только колонка B.

Полный листинг

Собираем всё вместе — от массива до готового отчёта:

php
<?php
require __DIR__ . '/vendor/autoload.php';

use avadim\FastExcelWriter\Excel;

// Данные (в жизни — из БД)
$rows = [
    ['2026-06-01', 'Иванов',  'Ноутбук Alpha 15', 2, 78900.00, 157800.00],
    ['2026-06-01', 'Петрова', 'Монитор ProView',  5, 21450.00, 107250.00],
    // ...
];

$excel = Excel::create(['Сводка']);

// ---------- Лист «Продажи» ----------
$sheet = $excel->makeSheet('Продажи');

// Заголовок отчёта: объединённые ячейки A1:F1
$sheet->writeTo('A1:F1', 'Отчёт по продажам — июнь 2026')
    ->applyFontStyleBold()
    ->applyFontSize(14)
    ->applyTextCenter()
    ->applyRowHeight(30);

// Товары бывают длинные — автоширина ('auto' ниже), но не уже 20
$sheet->setColMinWidth('C', 20);

// Шапка таблицы + форматы и ширины колонок одним вызовом
$headerStyle = [
    'font-style' => 'bold',
    'font-color' => '#ffffff',
    'fill-color' => '#4472c4',
    'text-align' => 'center',
    'border'     => 'thin',
    'height'     => 24,
];
$sheet->writeHeader([
    'Дата'     => ['format' => '@date',    'width' => 12],
    'Менеджер' => ['format' => '@string',  'width' => 16],
    'Товар'    => ['width' => 'auto'],
    'Кол-во'   => ['format' => '@integer', 'width' => 9],
    'Цена'     => ['format' => '#,##0.00', 'width' => 12],
    'Сумма'    => ['format' => '#,##0.00', 'width' => 14],
], $headerStyle);

// Данные: границы + зебра
$i = 0;
foreach ($rows as $row) {
    $rowStyle = ['border' => 'thin'];
    if ($i++ % 2) {
        $rowStyle['fill-color'] = '#eef3fb';
    }
    $sheet->writeRow($row, $rowStyle);
}

// Итоговая строка
$lastDataRow = count($rows) + 2; // строка 1 — заголовок, строка 2 — шапка
$sheet->writeRow(
    ['Итого', null, null, null, null, '=SUM(F3:F' . $lastDataRow . ')'],
    ['font-style' => 'bold', 'fill-color' => '#d9e2f3', 'border' => 'thin']
);

// Закреплённая шапка и автофильтр без итоговой строки
$sheet->setFreeze('A3');
$sheet->setAutoFilter('A2:F' . $lastDataRow);

// ---------- Лист «Сводка» ----------
$summary = $excel->getSheet('Сводка');
$summary->setColWidths(['A' => 24, 'B' => 16]);
$summary->writeRow(['Строк в детализации', count($rows)]);
$summary->writeRow(
    ['Итого, руб.', "=SUM('Продажи'!F3:F" . $lastDataRow . ')'],
    null,
    ['B' => ['format' => '#,##0.00']]
);

$excel->setActiveSheet('Сводка');
$excel->save('sales-report.xlsx');

На выходе — документ с титульной строкой, закреплённой шапкой, фильтрами, форматами, зеброй, итогами и сводным листом. Пример рассчитан на непустой набор данных: если выборка пуста, выведите отдельное сообщение вместо формул с диапазоном F3:F2.

Важно: пишем только вперёд

FastExcelWriter — потоковая библиотека: обработанные строки сбрасываются во временный файл, а текущая строка некоторое время остаётся в буфере. Библиотека не держит весь лист в памяти, поэтому вернуться к строке после её сброса нельзя. Отсюда практические правила:

  • стиль строки передавайте в writeRow() или применяйте через apply...() сразу после записи, до перехода к следующей строке;
  • setRowHeight() и setRowStyle() задавайте заранее или пока нужная строка ещё находится в буфере. Попытка изменить сброшенную строку вызовет исключение «Row number must be greater than written rows»;
  • автоширину включайте до записи данных;
  • mergeCells() можно вызвать и для прошлых строк. Для сложных шапок есть beginArea()/makeArea() — области с произвольным доступом к данным в буфере. Они не открывают доступ к строкам, уже сброшенным в файл, и расходуют память пропорционально содержимому области.

Отчёты обычно строятся сверху вниз, поэтому такой порядок записи подходит для табличных выгрузок. Но общий расход памяти зависит и от вашего кода: массив $rows в примере целиком находится в памяти. На больших выборках подавайте данные постепенно, повторно используйте стили и учитывайте, что автоширина кеширует измерения текстов. Фиксированные ширины предсказуемее по ресурсам. FastExcelWriter создаёт новые XLSX-файлы; для чтения и редактирования существующих книг нужен другой инструмент.

Итог

  • Оформление отчёта складывается из стилей шапки и данных, ширин колонок, числовых форматов, закрепления строк и точного диапазона автофильтра.
  • Стили можно задавать массивами опций или цепочкой методов apply...(), пока нужная строка ещё находится в буфере.
  • Зебра — чередование fill-color в цикле или одно правило Conditional::expression() на диапазон.
  • Формулы сводного листа считает Excel. Ограничивайте диапазон исходными данными, чтобы повторно не суммировать итоги.

Установка:

bash
composer require avadim/fast-excel-writer:6.17.0

Документация и исполняемые примеры (папка demo/) — в репозитории версии 6.17. Базовые стили и правила потоковой записи проверены по Sheet.php и Style.php.

Комментарии (0)