Эта статья будет посвящена подробному разбору одной интересной задачи. Меня попросили помочь с реализацией механизма генерации отчётов с возможностью фильтрации данных, предпросмотра и скачивания xlsx документа.
Шаг 1. Постановка задачи
Требуется реализовать компонент, который позволит генерировать «различные отчёты» 🙂 . Должны быть предусмотрены:
- Возможность фильтрации;
- установка обязательных параметров фильтрации;
- предварительный просмотр документа (отображение на странице перед скачиванием);
- генерация и скачивание xlsx документа.
В публичной части портала должен быть размещён компонент, позволяющий открыть страницу генерации отчёта по идентификатору. В качесте идентификатора выступает символьный код отчёта.
Предварительный просмотр в виде списка не подходит (Excel документы могут весьма разнообразными), так что следует отображать содержимое xlsx файла в виде html.
Внешне компонент должен выглядеть следующим образом:

Шаг 2. Архитектура
Определимся с тем, что нам потребуется для реализации задуманного. Объект отчётов, менеджер для их получения, библиотека для генерации документов и bitrix компонент, который будет всё это дело отображать.
Библиотека для генерации XLSX документов
Для генерации Excel документов возьмём библиотеку PHPSpreadsheet. Использование данной библиотеки подробно описано в официальной документации. Ставим через композер (где и как размещаем пакеты описано в этой статье).
Отчёт
Отчёт будет являться, своего рода, сервисом для получения и обработки данных, формирования документа. По хорошему, задачу получения данных должен выполнять другой компонент, но так как каждый отчёт содержит собственную логику выборки данных и правила формирования документа, расположим их на одном уровне.
Интерфейс отчётов ReportInterface.php
<?php /** * file: local/php_interface/classes/Aclips/Report/Type/ReportInterface.php */ namespace Aclips\Report\Type; /** * Интерфейс для отчётов */ interface ReportInterface { /** * Полученеи идентификатора отчёта * @return string */ public function getReportId(): string; /** * Получение названия отчёта * @return string */ public function getName(): string; /** * Установка названия отчёта * @param string $title * @return mixed */ public function setTitle(string $title); /** * Установка фильтра * @param array $filter */ public function setFilter(array $filter = []); /** * Получение формата генерируемого отчёта * @return string */ public function getReportFormat(): string; /** * Получение заголовка для скачивания * @return string */ public function getDataApplication(): string; /** * Генерация документа * @return bool */ public function generateDocument(): bool; /** * Получение параметров фильтрации * @return array|null */ public function getFilterParams(): ?array; /** * Получение кастомных стилей для страницы отчёта * @return string|null */ public function getCustomStyleHtml(): ?string; /** * Формирование документа * @param string $outputFile * @return string */ public function makeResultFile(string $outputFile): string; /** * Формирование Html документа * @param string $outputFile * @return string */ public function makeHtmlFile(string $outputFile): string; }
Менеджер отчётов
Класс, который возвращает отчёт по его коду. На данный момент это его единственная обязанность
<?php /** * file: local/php_interface/classes/Aclips/Report/ReportManager.php */ namespace Aclips\Report; use Aclips\Report\Type\ReportInterface; /** * Менеджер отчётов */ class ReportManager { public function __construct() { } /** * Получение отчёта по коду * Кодом является название класса отчёта без суффикса 'Report' * @param string $code * @return ReportInterface|null */ public function getReportByCode(string $code): ?ReportInterface { $code = ucfirst($code); $reportClassName = '\\Aclips\\Report\\Type\\' . $code . 'Report'; if (class_exists($reportClassName)) { try { $reportClass = new $reportClassName(); return $reportClass; } catch (\Throwable $e) { //@TODO обработка ошибки } } return null; } }
Шаг 3. Реализация
Поскольку часть методов будет идентичная для всех отчётов, создадим базовые классы и реализуем их там, при необходимости их всегда можно будет расширить или переопределить в самих отчётах.
Класс BaseReport включает в себя общие методы для всех отчётов, а класс BaseXlsxReport содержит функционал, который касается исключительно генерации XLSX документов. Вполне может потребоваться генерация документов других типов (например docx) и вместо переписывания кода родительского класса достаточно будет создать новый и реализовать методы, определённые в интерфейсе ReportInterface.
Тестовый отчёт
Теперь проверим, что у нас получилось и как работает. Создадим класс TestReport, расширяющий BaseXlsxReport.
В методе getFilterParams() опишем поля фильтра, которые будут доступны для формирования отчёта.
/** * file: local/php_interface/classes/Aclips/Report/Type/TestReport.php */ public function getFilterParams(): ?array { return [ 'FILTER' => [ [ 'id' => 'SOME_LIST', 'name' => 'Список отображаемых значений', 'default' => true, 'type' => 'list', 'items' => [ 1 => 1, 2 => 2, 3 => 3, ], 'params' => [ 'multiple' => 'Y' ], 'require' => true ], ], 'FILTER_PRESETS' => [] // Можно указать пресеты ]; }
Элементы массива соответствуют штатному компоненту bitrix:main.ui.filter, за исключением параметра require. Этот параметр отвечает за обязательность выбора или заполнения значением поля фильтра (это может быть полезно при формировании отчётов за какой-либо период).
В методе getData() описывается логика получения данных. В качестве единственного параметра, метод принимает массив значений, указанных в фильтре. Для проверки вернём значения, которые указали в фильтре.
/** * file: local/php_interface/classes/Aclips/Report/Type/TestReport.php */ protected function getData(array $filter = []): array { $data = []; $data['SOME_LIST'] = $filter['SOME_LIST']; return $data; }
Остаётся нарисовать сам отчёт. За это отвечает метод generateDocument().
/** * file: local/php_interface/classes/Aclips/Report/Type/TestReport.php */ public function generateDocument(): bool { if (!$this->validateFilter($this->filter)) { return false; } $data = $this->getData($this->filter); $sheet = $this->spreadsheet->getActiveSheet(); $sheet->mergeCells("A1:C1"); $sheet->setCellValue('A1', 'Заголовок'); $i = 2; foreach ($data['SOME_LIST'] as $value) { $sheet->setCellValue('A' . $i, 'Значение'); $sheet->setCellValue('B' . $i, $value); $sheet->setCellValue('C' . $i, '...'); $i++; } return true; }
Проверяем
Получим экземпляр класса тестового отчёта через менеджер очередей, установим ему значения фильтра, сгенерируем документ и посмотрим на сформированных html код (если сформируется html — значит и xlsx готов, убьём двух зайцев сразу)
$reportManager = new Aclips\Report\ReportManager(); $report = $reportManager->getReportByCode('Test'); $report->setFilter([ 'SOME_LIST' => [1,2,3] ]); $report->generateDocument(); $result = $report->makeHtmlFile('/tmp/test2.html'); print file_get_contents($result);
В рузельтате должен вернуться html код, который отрисует таблицу, представленную на рисунке ниже:

Шаг 4. Компонент
Библиотека для генерации готова и работает. Осталось создать компонент и вывести его на публичной странице портала. В качестве единственного параметра, компонент будет принимать код отчёта. Сам компонент можно посмотреть в здесь.
... $APPLICATION->includeComponent('aclips:report.detail', '', [ 'CODE' => 'Test' ]); ...
Как работает компонент:
- Получение на входе кода отчёта;
- получение отчёта на основании кода;
- заполнение параметров компонента значениями отчёта (фильтр, пресеты, стили);
- отображение шаблона компонента, включая фильтр и элементы управления (кнопка «Сформировать отчёт»);
- при формировании отчёта отправляются данные с формы отправляются данные и отображается html таблица сформированного документа с возможностью скачивания xlsx файла или сообщение об ошибки.


Итог
Таким образом был реализован механизм, выполняющий поставленные задачи. Дальнейшее добавление отчётов будет сводиться к реализации 2-3 методов при том, что общий функционал будет оставаться без изменений и может использоваться в других проектах. Полный код можно взять в GitHub репозитории.
P.S. Если вам нужна помощь или консультация по поводу решения ваших задач — смело пишите, с удовольствием помогу.