Эта статья будет посвящена подробному разбору одной интересной задачи. Меня попросили помочь с реализацией механизма генерации отчётов с возможностью фильтрации данных, предпросмотра и скачивания 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. Если вам нужна помощь или консультация по поводу решения ваших задач — смело пишите, с удовольствием помогу.