Генерация PDF и Excel в PHP: сравниваем библиотеки
Отчёт на 5000 строк кладёт PHP-воркер по памяти, а красивый инвойс из HTML разъезжается на печати. Разбираем, какие библиотеки для PDF и Excel выбрать под задачу и чем каждая платит.

Задача звучит одинаково для всех: «сделай кнопку, чтобы выгрузить отчёт в PDF и Excel». А дальше выясняется, что генерация счёта на одну страницу и выгрузка склада на 50 000 строк — это два разных мира с разными библиотеками, разным потреблением памяти и разными способами упасть. Ниже — сравнение того, что реально живёт в PHP-проектах в 2024 году, с ценой каждого решения по памяти, скорости и качеству вёрстки.
Что вообще выбираем
Для PDF в PHP есть два принципиально разных подхода. Первый — рендер из HTML/CSS: пишете разметку, библиотека превращает её в страницы. Второй — императивная отрисовка: вы сами ставите текст в координаты, рисуете линии, вставляете картинки. Первый удобен для документов с текстом и таблицами, второй — для жёсткого контроля над каждым миллиметром.
Для Excel выбор проще по числу игроков, но острее по памяти: разница между «удобным объектным API» и «потоковой записью» может означать разницу между работающим экспортом и Allowed memory size exhausted.
PDF: три рабочих варианта
- Dompdf — рендер из HTML/CSS. Чистый PHP, ставится через Composer, не требует внешних процессов. Поддержка CSS ограниченная: старый добрый табличный и блочный layout работает, а вот flexbox и grid — нет.
- mPDF — тоже из HTML, но с более широкой поддержкой CSS, кириллицы «из коробки» и таких вещей, как колонтитулы, водяные знаки, штрихкоды. Тяжелее Dompdf по памяти на сложных документах.
- TCPDF / FPDF — императивная отрисовка. FPDF — минималистичный и очень быстрый, TCPDF — его идейный наследник с поддержкой UTF-8, HTML-подмножества и множества функций. Здесь вы контролируете каждую координату, но и вёрстку пишете руками.
Есть ещё вариант вне PHP — headless-браузер (Chromium через Puppeteer или библиотеку-обёртку). Он даёт идеальный современный CSS, потому что рендерит настоящий движок. Цена — отдельный процесс Chromium, память в сотни мегабайт на запуск и внешняя зависимость, которую надо держать в контейнере. В этой статье фокус на чистом PHP; браузерный рендер — отдельная тема.
Excel: два варианта
- PhpSpreadsheet — преемник PHPExcel, полноценный объектный API: стили, формулы, графики, чтение и запись xlsx/xls/csv. Удобно, но каждая ячейка — это объект в памяти.
- OpenSpout (бывший Box/Spout) — потоковая запись и чтение. Не умеет сложных стилей и формул, зато пишет большие файлы с почти постоянным потреблением памяти.
Цена каждого варианта
Главный водораздел — объём данных. На одностраничном инвойсе разница между библиотеками незаметна: любая справится за доли секунды. На больших выгрузках всё решает модель памяти.
| Библиотека | Формат | Модель | Сильная сторона | Чем платит |
|---|---|---|---|---|
| Dompdf | HTML → PDF | Простой старт, чистый PHP | Слабый CSS, медленный на больших таблицах | |
| mPDF | HTML → PDF | Кириллица, колонтитулы, штрихкоды | Прожорлив по памяти на сложной вёрстке | |
| TCPDF | Императивная | Полный контроль, стабильность | Вёрстку пишете руками, многословно | |
| FPDF | Императивная | Минимум памяти, очень быстрый | Нет UTF-8 без расширений, голый API | |
| PhpSpreadsheet | Excel | Объектная в памяти | Стили, формулы, графики, чтение | Память растёт линейно с числом ячеек |
| OpenSpout | Excel | Потоковая | Постоянная память на больших файлах | Минимум стилей, нет формул и графиков |
Цифры по памяти PhpSpreadsheet сильно зависят от версии, содержимого ячеек и включённых стилей, поэтому конкретный «X мегабайт на 10 000 строк» приводить нечестно — замерьте на своих данных через memory_get_peak_usage(). Порядок величины таков: объектная модель на десятках тысяч строк со стилями упирается в лимит памяти PHP по умолчанию, а потоковая запись OpenSpout держится примерно на одном уровне независимо от числа строк.
Правило, которое экономит недели отладки: если строк больше нескольких тысяч и вам не нужны сложные стили — берите потоковую запись сразу, не дожидаясь падения воркера на проде.
Excel: где ломается PhpSpreadsheet и как пишет OpenSpout
PhpSpreadsheet держит всю книгу в памяти как дерево объектов. Пока данных мало — это удобно:任意 ячейка доступна, стили применяются пачками, можно вставить формулу. Но выгрузка отчёта на десятки тысяч строк с форматированием — классический сценарий, где процесс упирается в memory_limit.
OpenSpout пишет строки в поток и не держит их после записи. Вот минимальный экспорт большого файла (OpenSpout 4.x, PHP 8.1+):
<?php
// composer require openspout/openspout
use OpenSpout\Writer\XLSX\Writer;
use OpenSpout\Common\Entity\Row;
$writer = new Writer();
$writer->openToFile(__DIR__ . '/report.xlsx');
// Заголовок таблицы
$writer->addRow(Row::fromValues(['ID', 'Товар', 'Остаток']));
// Пишем построчно — память не растёт с числом строк
foreach ($rowsIterator as $item) {
$writer->addRow(Row::fromValues([
$item['id'],
$item['name'],
$item['qty'],
]));
}
$writer->close();
Ключевой момент — $rowsIterator должен быть именно генератором или курсором к базе, а не заранее загруженным в память массивом на 50 000 элементов. Иначе память сожрёт не библиотека, а ваш собственный $rows = $repository->findAll().
Если тот же объём нужен через PhpSpreadsheet ради стилей — включайте кеширование ячеек и подберите драйвер хранения под окружение; конкретные имена настроек и доступных кеш-адаптеров сверяйте в документации вашей версии, они менялись между релизами.
PDF: HTML-рендер против координат
Если документ — это текст, таблицы и логотип (счёт, договор, отчёт), HTML-рендер выигрывает по скорости разработки. Пример на Dompdf (2.x, PHP 8.1+):
<?php
// composer require dompdf/dompdf
use Dompdf\Dompdf;
use Dompdf\Options;
$options = new Options();
$options->set('defaultFont', 'DejaVu Sans'); // шрифт с кириллицей
$dompdf = new Dompdf($options);
$html = '<h1>Счёт № 42</h1>'
. '<table><tr><td>Товар</td><td>1000 ₽</td></tr></table>';
$dompdf->loadHtml($html, 'UTF-8');
$dompdf->setPaper('A4', 'portrait');
$dompdf->render();
file_put_contents(__DIR__ . '/invoice.pdf', $dompdf->output());
Обратите внимание на шрифт: если поставить кириллицу без шрифта, который её содержит, вы получите пустые прямоугольники вместо букв — самая частая жалоба новичков на Dompdf. DejaVu Sans идёт в комплекте и покрывает кириллицу.
Как только документу нужны точные координаты, штрихкоды в конкретной точке, поворот текста или строгое соответствие полиграфическому макету — HTML-рендер начинает бороться с вами. Здесь берут TCPDF или FPDF и ставят элементы по координатам напрямую. Это дольше писать, но результат предсказуем до пикселя.
Когда что выбирать
- Инвойс, договор, отчёт из текста и таблиц, кириллица — mPDF, если нужны колонтитулы и штрихкоды; Dompdf, если хочется минимум зависимостей и вёрстка простая.
- Документ с жёстким макетом, метки, поворот, точные координаты — TCPDF или FPDF.
- Идеальный современный CSS, не жалко Chromium в контейнере — headless-браузер (отдельная инфраструктура).
- Excel до нескольких тысяч строк со стилями, формулами, графиками — PhpSpreadsheet.
- Excel на десятки тысяч строк и больше, без сложных стилей — OpenSpout, потоковая запись.
Что ломается на проде
- Память на больших выгрузках. Симптом —
Allowed memory size exhaustedили тихая смерть воркера. Причина чаще в том, что данные загружены в массив целиком, а не в самой библиотеке. Итерируйте курсором. - Таймаут при синхронной генерации. Пользователь жмёт кнопку и ждёт 40 секунд, пока веб-сервер не оборвёт запрос. Тяжёлые отчёты выносите в фоновую очередь, отдавайте файл по готовности.
- Пустые квадраты вместо кириллицы в PDF. Шрифт не содержит нужных символов. Ставьте шрифт с кириллицей и указывайте его явно.
- Разъехавшаяся вёрстка в Dompdf. Использован flexbox или grid, которых движок не понимает. Верстайте отчёты на таблицах и блоках.
- Скачивается битый файл. Перед отдачей PDF/Excel в браузер что-то уже вывело в поток — лишний
echo, пробел до<?phpили предупреждение PHP. Проверьте буфер вывода и заголовки.
Что делать дальше
Определитесь с объёмом данных до выбора библиотеки — это главный критерий, а не «удобство API». Замерьте пиковую память своего экспорта через memory_get_peak_usage(true) на реальном объёме, а не на трёх тестовых строках. Если генерация занимает больше пары секунд — сразу проектируйте её как фоновую задачу в очереди, а не как синхронный ответ на HTTP-запрос. И зафиксируйте версии библиотек в composer.json: API PhpSpreadsheet и OpenSpout заметно менялся между мажорными релизами, и код из старых гайдов часто не запускается на новых версиях.
Задачи: математика, физика, химия: Разбор задачи по шагам с проверкой и похожей задачей
Спросить за 10 ₽Источники: PhpSpreadsheet — официальная документация, OpenSpout — репозиторий на GitHub
Частые вопросы
У меня другая версия OpenSpout — код из примера не работает
Что взять, если нужны и стили Excel, и большой объём одновременно
Почему в PDF вместо русских букв квадраты
Генерация большого отчёта валит воркер по памяти — это баг библиотеки
Нужна ли отдельная инфраструктура под headless-браузер
Как отдать сгенерированный файл на скачивание без битого содержимого
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.