Введение
Есть одна мебельная фабрика. Исторически у этой фабрики IT хозяйство было не очень развито и со временем накопилось множество внутренних сервисов для расчета материалов, нагрузки на станки, учета временных затрат работников, доставки и прочего. Исходя из этих расчетов составляется себестоимость продукции. Далее накладывается маржа, и идет расчет доставки и сборки, в зависимости от условий. Фабрика захотела объединить все сервисы в один, основанный на веб, чтобы все расчёты и результаты можно было видеть в одном месте. Было выбрано делать новую веб систему на фреймворке Symfony (PHP).
По задумке данные из разных Excel файлов и локальной базы данных должны стекаться туда, группироваться по проектам и красиво отображаться. Изначально не планировалось изменять какие-либо данные, только просматривать. Также заказчик не хотел полностью отказываться от Excel файлов и переносить формулы в новую систему. Да, это странно со стороны разработчика, но объяснимо со стороны бизнеса: формул очень много, и они очень старые и запутанные. Но они отлично работают и всех устраивают. Их анализ, перенос в новую систему, отладка, и обучение работников пользоваться новой системой не входило в планы и бюджет проекта.
Проблема
На этапе разработки, когда первые Excel файлы были загружены и красиво отображались таблицы с материалами и суммы, клиенту захотелось немного "поиграть" цифрами внутри системы. Вместо одного стола, "поставить" два и посмотреть как изменятся суммы материалов. Естественно, возник вопрос, что мы не можем пересчитать формулы в нашей системе, так как мы их не переносили. Вариант увеличивать итог пропорционально количеству не мог быть использован, так как это не было пропорциональное увеличение. Упрощенный пример: для изготовления стола нужна заготовка столешницы из которой можно сделать пять столов. Но если надо сделать шесть столов, то заготовок нужно уже две. Так же и доставка: если в грузовик влезает сто столов, то чтобы привести сто один стол нужно уже два грузовика. Excel всё это учитывал, а еще скидки для конкретных клиентов, разный расчет стоимости сборки и прочее, прочее.
Первая попытка — PhpSpreadsheet
Кажется встала невыполнимая задача - пересчитывать формулы, которых нет. Но как говорил мой первый наставник и шеф: "Мы можем сделать всё, для программиста нет ничего невозможного". Для импорта Excel файла я использовал библиотеку https://github.com/phpoffice/phpspreadsheet с помощью нее можно прочитать лист внутри файла и получить значение (как саму формулу так и ее результат) из любой ячейки.
Пример кода, как прочитать результат формулы из ячейки
use PhpOffice\PhpSpreadsheet\IOFactory; $spreadsheet = IOFactory::load('/path/file.xlsx'); $sheet = $spreadsheet->getSheetByName('Totals'); $value = $sheet->getCell('F18')->getCalculatedValue();
У этой библиотеки есть также функция задать значение в ячейку. То есть теоретически можно создать форму на сайте, например с полями "количество товара". При отправке формы, получить данные и поменять в файле количество товара но новое, рассчитать новые значения сумм после этого.
Скрытый текст
use PhpOffice\PhpSpreadsheet\IOFactory; $newQuantity = 5; $spreadsheet = IOFactory::load('/path/file.xlsx'); $sheet = $spreadsheet->getSheetByName('Products'); $sheet->setCellValue('D:5', $newQuantity);
Но на практике это не сработало - формулы не могли рассчитаться так как было много листов и перекрестных ссылок. PhpOffice\PhpSpreadsheet это не полноценный движок Excel а лишь библиотека для работы с готовым файлом. А значит, можно попробовать использовать полноценный движок. Что если установить на сервер headless LibreOffice и делать перерасчет формул там? Стал пробовать.
Вторая попытка — использовать настоящий движок Excel
Установил LibreOffice на сервер
sudo apt update sudo apt install -y libreoffice libreoffice-calc fonts-dejavu fontconfig
И все получилось!
Перерасчет формул в headless LibreOffice
use PhpOffice\PhpSpreadsheet\IOFactory; $newQuantity = 5; $spreadsheet = IOFactory::load('/path/file.xlsx'); $sheet = $spreadsheet->getSheetByName('Products'); $sheet->setCellValue('D5', $newQuantity); $writer = IOFactory::createWriter($spreadsheet, 'Xlsx'); $writer->setPreCalculateFormulas(false); $writer->save('/path/file.xlsx'); $cmd = sprintf( 'HOME=/tmp libreoffice --headless --nologo --nofirststartwizard --calc --convert-to xlsx --outdir %s %s', escapeshellarg(dirname($absolutePath)), escapeshellarg($absolutePath) ); exec($cmd, $output, $code); if ($code !== 0) { throw new RuntimeException('LibreOffice recalculation failed'); } $updatedSpreadsheet = IOFactory::load('/path/file.xlsx'); $updatedSheet = $updatedSpreadsheet->getSheetByName('Totals'); $updatedValue = $updatedSheet->getCell('F18')->getCalculatedValue();
В итоге Excel так и остался внутри системы — но уже не как файл, из которого берём данные, а как движок бизнес-логики.
Вывод
Да, решение получилось немного необычным: веб-приложение на Symfony меняет входные данные в Excel, затем headless LibreOffice пересчитывает формулы, и система забирает обратно готовые результаты. На практике это занимает около 15 секунд. Но для пользователя, который делает расчёт проекта мебели, это вполне приемлемо. Ему важнее менять параметры и видеть результат.
Можно ли было переписать все формулы на PHP? Конечно. Но это заняло бы месяцы работы, большой бюджет и риск сломать давно работающую логику.
Наверное такой подход можно использовать в MVP где надо много менять формулы, и это должен делать не программист, а например менеджер или сам клиент.
Иногда подходящее решение - не переписать всё заново, а грамотно встроить существующий инструмент в новую систему. И в этот раз Excel оказался не пережитком прошлого, а вполне рабочим микросервисом для расчётов. Вместо того, чтобы бороться с существующим инструментом, проще признать: Excel уже является системой расчётов. И его можно использовать программно.
