Генерация EXCEL отчётов с предварительным просмотром

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

6 комментариев

  1. Алексей:

    Привет!
    Отличная статья!
    Затестим!

    Тебе за твой блог — давно пора установить способ доната!
    Буду первым!

  2. >> Ставим через композер (где и как размещаем пакеты описано в этой статье).

    Ссылка не рабочая. Статья супер!

Добавить комментарий