Экспорт в Excel просто, быстро и красиво
Проект 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 умеет писать построчно в файл, значит может стримить и напрямую в буфер!
Рефакторинг
А теперь вооружимся SOLID и сделаем этот код переиспользуемым в рамках конкретного приложения. Но не будем оверинжинирить —проектный инструмент должен получиться простым и гибким ровно настолько, насколько этого требуют юзкейсы. Рассмотрим Excel выгрузки Happy Job:
- всегда используется формат
xlsx
; - содержимое таблиц нетиповое, нужен полный контроль над листами, строками и ячейками;
- один экшн— один файл.
Получается, что относительно примера выше в нашем приложении код будет варьироваться только в строках 28–33. Всё остальное можно вынести в сервис.
Самый простой и универсальный способ внедрить кусок кода в середину метода? Аргумент типа callable
!
Обратите внимание, что тип возвращаемого значения Response
, а не StreamedResponse
. Так мы скрыли подробности реализации и объявили более универсальный контракт. Если в будущем нам потребуется несколько реализаций фабрики, достаточно будет сделать ExcelResponseFactory
интерфейсом, и всё уже будет готово для полиморфизма без правки сигнатур и клиентского кода (экшнов).
Иногда код, наполняющий $writer
данными, оказывается слишком сложным для замыкания. В таких случаях его можно вынести в отдельный класс с методом __invoke
. В этом для меня состоит главное преимущество callable
перед интерфейсом при организации простых точек расширения.
Юнит-тесты для фабрики. На мой взгляд, testResponseContent
сложноват. Если есть идеи, как оформить его изящнее, пишите в комментариях.
В заключение повторюсь, данное решение работает лишь в конкретном проекте. Вам может потребоваться более простая или более сложная интеграция Spout в зависимости от специфики. Но никогда не переусложняйте раньше времени.
Пишите библиотеки универсально, а проекты оптимально 👌
Подписывайтесь на канал Пых в телеграме, чтобы узнать еще больше лайфхаков для PHP и Symfony!