Генерация PDF и Excel в PHP: приёмы, которые выживают в проде
Отчёты на 20 тысяч строк валят память, кириллица в PDF превращается в квадратики, а Excel открывает файл с предупреждением о повреждении. Разбираем рабочие подходы к генерации документов на PHP.

Задача звучит просто: сгенерировать счёт в PDF и выгрузить отчёт в Excel. На демо-данных из трёх строк всё работает. Потом приходит бухгалтер с выгрузкой за квартал — 18 тысяч строк, — и скрипт падает с Allowed memory size of 134217728 bytes exhausted. Или PDF формируется, но вместо русских букв в нём квадраты. Или Excel открывается, но LibreOffice ругается на повреждённый файл. Разберём, чем именно платишь за каждый способ и когда какой брать.
PDF: три библиотеки, три компромисса
В PHP-мире живут три рабочих инструмента для PDF, и выбор между ними — это выбор между вёрсткой, скоростью и размером макета. Версии на момент написания: mPDF 8.x, Dompdf 3.x, TCPDF 6.x.
| Библиотека | Как верстается | Скорость | Кириллица | Больное место |
|---|---|---|---|---|
| mPDF 8.x | HTML + подмножество CSS | Средняя | Из коробки при верном шрифте | Жрёт память на больших таблицах |
| Dompdf 3.x | HTML + CSS 2.1 | Ниже mPDF | Нужен шрифт с кириллицей | Слабая поддержка сложной вёрстки |
| TCPDF 6.x | Прямые вызовы API (координаты, ячейки) | Высокая | Нужны шрифты (есть встроенные) | Вёрстку пишешь руками, HTML ограничен |
Кириллица: главная причина квадратиков
Проблема почти всегда одна — шрифт по умолчанию не содержит русских глифов. Латиница есть, кириллицы нет, отсюда квадраты. Решение: явно подключить шрифт с поддержкой кириллицы, например DejaVu Sans (свободная лицензия, есть в поставке mPDF).
<?php
// mPDF 8.x, composer require mpdf/mpdf
use Mpdf\Mpdf;
$mpdf = new Mpdf([
'mode' => 'utf-8',
'format' => 'A4',
'default_font' => 'dejavusans', // шрифт с кириллицей
]);
$html = '<h1>Счёт № 42</h1><p>Итого: 15 000 рублей</p>';
$mpdf->WriteHTML($html);
// I — отдать в браузер, D — скачать, F — записать в файл
$mpdf->Output('invoice.pdf', 'I');
Если верстаете свой шаблон и подключаете кастомный шрифт через @font-face в CSS, файл шрифта должен быть доступен mPDF по указанному пути, а сам шрифт — содержать нужные глифы. Проверить содержимое шрифта проще всего, открыв его в системном просмотрщике: если кириллицы там нет, PDF её не покажет никаким кодом.
Память на больших PDF
mPDF держит в памяти всю структуру документа до момента вывода. Таблица на несколько тысяч строк раздувает потребление в десятки раз относительно исходного HTML. Здесь работают два приёма.
- Разбивать документ на блоки. Вместо одного гигантского
WriteHTML()вызывать его порциями — например, по 500 строк, — сбрасывая промежуточные данные. Так пиковое потребление ниже, чем при одном большом вызове. - Не генерировать многотысячные PDF вовсе. PDF — формат для печати и подписи, а не для выгрузки данных. Отчёт на 18 тысяч строк человек всё равно не читает глазами, он его фильтрует. Такому отчёту место в Excel или CSV.
Если документ длиннее пары десятков страниц и его никто не собирается печатать целиком — это не PDF. Это таблица, которую по ошибке одели в PDF.
Excel: перестаньте отдавать HTML под видом .xls
Классическая ошибка — отдать HTML-таблицу с заголовком Content-Type: application/vnd.ms-excel и расширением .xls. Excel такое откроет, но с предупреждением о несоответствии формата, а LibreOffice может вовсе отказаться. Это не Excel-файл, это HTML, которому наврали про тип. Формулы, типы ячеек и несколько листов так не сделать.
Рабочий инструмент — библиотека PhpSpreadsheet (актуальная ветка на момент написания — 4.x; ставится через composer require phpoffice/phpspreadsheet). Она пишет настоящий формат XLSX.
<?php
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->setCellValue('A1', 'Товар');
$sheet->setCellValue('B1', 'Цена');
$sheet->setCellValue('A2', 'Кабель USB-C');
$sheet->setCellValue('B2', 890);
$writer = new Xlsx($spreadsheet);
$writer->save('report.xlsx');
Файл откроется в Excel и LibreOffice без предупреждений, потому что это настоящий XLSX, а не переодетый HTML.
Где PhpSpreadsheet упирается в память
PhpSpreadsheet держит в памяти каждую ячейку как объект. На отчёте в десятки тысяч строк с несколькими столбцами это легко уводит за лимит memory_limit. Симптом тот же: Allowed memory size exhausted на этапе заполнения.
Для больших выгрузок в XLSX у библиотеки есть отдельный писатель, работающий в потоковом режиме — он сбрасывает строки на диск, а не копит в памяти. Точное имя класса и порядок настройки смотрите в разделе про memory saving официальной документации PhpSpreadsheet: интерфейс менялся между мажорными версиями, и подставлять по памяти имя класса здесь — прямой путь к нерабочему коду.
CSV как быстрый выход из проблемы памяти
Если от Excel не требуются формулы, форматирование и несколько листов — а от большинства выгрузок не требуются, — генерируйте CSV. Стандартная функция fputcsv() пишет строку за строкой прямо в поток, память не растёт с размером файла.
<?php
// Выгрузка без риска упереться в память
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="report.csv"');
$output = fopen('php://output', 'w');
// BOM, чтобы Excel распознал UTF-8 и не показал кракозябры
fwrite($output, "\xEF\xBB\xBF");
fputcsv($output, ['Товар', 'Цена']);
fputcsv($output, ['Кабель USB-C', 890]);
fclose($output);
Один нюанс с кириллицей: без BOM в начале файла Excel открывает UTF-8 CSV как Windows-1251 и показывает кракозябры. Три байта \xEF\xBB\xBF в начале снимают проблему.
Когда что выбирать
- PDF (mPDF) — счета, договоры, акты: документы с фиксированной вёрсткой на печать, до нескольких десятков страниц.
- PDF (TCPDF) — когда нужна точная раскладка по координатам, штрихкоды, много одинаковых документов и важна скорость, а не удобство вёрстки.
- XLSX (PhpSpreadsheet) — отчёты, где нужны формулы, форматирование, несколько листов, и объём умеренный.
- CSV (fputcsv) — большие выгрузки данных, которые пойдут в другую систему или под фильтрацию. Не упирается в память ни на какой величине.
Что ломается чаще всего
- Квадраты вместо букв в PDF — шрифт без кириллицы. Подключите DejaVu Sans или другой шрифт с русскими глифами.
- Excel предупреждает о повреждённом файле — вы отдали HTML под видом .xls. Генерируйте настоящий XLSX через PhpSpreadsheet.
- memory_limit exhausted — библиотека копит документ в памяти. Переходите на потоковый писатель или на CSV.
- Битый файл при скачивании — перед выводом бинарного файла в поток попал лишний вывод: пробел до
<?php, эхо, предупреждение PHP. Любой байт до заголовков ломает файл. - Кракозябры в CSV из Excel — нет BOM. Добавьте
\xEF\xBB\xBFв начало.
Что сделать дальше
Замерьте, какой у вас реальный объём документов в проде — не на демо, а на боевых данных за месяц. Если хоть один отчёт переваливает за несколько тысяч строк, выносите генерацию тяжёлых файлов в фоновую очередь: синхронная генерация по HTTP-запросу упрётся либо в max_execution_time, либо в таймаут веб-сервера. Пользователь нажимает кнопку, задача уходит в очередь, готовый файл приходит ссылкой. Инвалидация и хранение сгенерированных файлов — отдельная тема, здесь мы разбирали только саму генерацию.
Карточки для маркетплейсов: Таблица товаров и папка фотографий на входе — комплект слайдов и описаний на выходе, по требованиям Wildberries и Ozon.
Спросить за 5 ₽Источники: PhpSpreadsheet — официальная документация, mPDF — официальная документация
Частые вопросы
А если у меня PHP 7.4, PhpSpreadsheet 4.x установится?
PDF генерируется, но кириллица всё равно квадратами — что ещё проверить?
Скрипт падает с memory_limit exhausted, а поднимать лимит не хочется. Что делать?
Можно ли отдавать сгенерированный файл прямо в браузер без сохранения на диск?
Файл скачивается, но открывается повреждённым, хотя код верный. В чём дело?
Нужен один документ и с таблицами, и с печатной вёрсткой — брать PDF или Excel?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.