Экспорт в Excel просто, быстро и красиво

Удальцов Валентин
Пых / PHPYH
Published in
2 min readMay 18, 2020

Проект Happy Job, где я занимаюсь проектированием и разработкой бэкенда, — это в первую очередь аналитика.

А какая аналитика без выгрузок в Excel 😉

Раньше я всегда использовал библиотеку PHPOffice/PhpSpreadsheet (бывшая PHPOffice/PHPExcel). Она предоставляет почти полный инструментарий для работы с таблицами, но есть один большой минус — формирование листа по умолчанию происходит в памяти. Одна ячейка вместе с метаданными весит примерно 1 Кбайт, поэтому выгрузка лишь 400 000 строк в три колонки уже обойдется более чем в 1 Гбайт памяти. В библиотеке предусмотрена возможность кешировать ячейки на базе PSR-16, но это значительно снижает скорость записи.

Для нашего кейса я нашёл решение получше. Куда менее популярная библиотека box/spout позволяет читать Excel-файлы и писать в них построчно и очень быстро. Расход памяти константный из коробки, без всяких плясок с кешем (по факту она, конечно, создает какие-то временные файлы в sys_get_temp_dir()). Пакет тоже поддерживает листы и типы данных, но не умеет в автоширину, объединение ячеек и продвинутое форматирование. Я уверен, что как и для нас, для большинства проектов это приемлемый компромисс.

Интеграция «в лоб»

Вот как может выглядеть черновой экшн для выгрузки отчёта, сделанный по документации Box Spout и Symfony HttpFoundation:

Экшн для выгрузки отчёта, сделанный “в лоб”

Оптимизация

Что тут можно оптимизировать? Временный файл кажется лишним. Если Spout умеет писать построчно в файл, значит может стримить и напрямую в буфер!

Альтернативная реализация экшна с использованием StreamedResponse

Рефакторинг

А теперь вооружимся SOLID и сделаем этот код переиспользуемым в рамках конкретного приложения. Но не будем оверинжинирить —проектный инструмент должен получиться простым и гибким ровно настолько, насколько этого требуют юзкейсы. Рассмотрим Excel выгрузки Happy Job:

  • всегда используется формат xlsx;
  • содержимое таблиц нетиповое, нужен полный контроль над листами, строками и ячейками;
  • один экшн— один файл.

Получается, что относительно примера выше в нашем приложении код будет варьироваться только в строках 28–33. Всё остальное можно вынести в сервис.

Самый простой и универсальный способ внедрить кусок кода в середину метода? Аргумент типа callable!

Фабрика генерации ответа сервера в формате Excel

Обратите внимание, что тип возвращаемого значения Response, а не StreamedResponse. Так мы скрыли подробности реализации и объявили более универсальный контракт. Если в будущем нам потребуется несколько реализаций фабрики, достаточно будет сделать ExcelResponseFactory интерфейсом, и всё уже будет готово для полиморфизма без правки сигнатур и клиентского кода (экшнов).

Экшн, использующий новую фабрику

Иногда код, наполняющий $writer данными, оказывается слишком сложным для замыкания. В таких случаях его можно вынести в отдельный класс с методом __invoke. В этом для меня состоит главное преимущество callable перед интерфейсом при организации простых точек расширения.

Юнит-тесты для фабрики. На мой взгляд, testResponseContent сложноват. Если есть идеи, как оформить его изящнее, пишите в комментариях.

В заключение повторюсь, данное решение работает лишь в конкретном проекте. Вам может потребоваться более простая или более сложная интеграция Spout в зависимости от специфики. Но никогда не переусложняйте раньше времени.

Пишите библиотеки универсально, а проекты оптимально 👌

Подписывайтесь на канал Пых в телеграме, чтобы узнать еще больше лайфхаков для PHP и Symfony!

--

--