Добавить в корзинуПозвонить
Найти в Дзене

Кастомная выгрузка в Excel в 1С-Битрикс

# Кастомная выгрузка в Excel в Битрикс
Бывает так что стандартная выгрузка в Excel из инфоблока падает с ошибкой о нехватке памяти, когда много записей или много свойств выбрано в фильтре.
Это возникает из-за того что много JOIN-ов и запрос получается слишком большим.
Самый простой способ - это написать свою выгрузку.
Добавить в админке кнопку, добавить роут, создать контроллер и в сервисе написать бизнес-логику. Обычно на проекте есть какой-нибудь модуль, который содержит общую бизнес-логику для проекта (core/engine/main), туда этот функционал и добавим.
В этой статье разберем, как создать собственную кастомную выгрузку с полным контролем над процессом.
## Архитектура решения
Наше решение будет состоять из нескольких компонентов:
- Обработчик события для добавления кнопки в административную панель
- Роут для обработки запроса на выгрузку
- Контроллер для координации процесса
- Сервис с бизнес-логикой выгрузки
## Шаг 1. Создание обработчика события
Первым делом создадим класс-обр

# Кастомная выгрузка в Excel в Битрикс
Бывает так что стандартная выгрузка в Excel из инфоблока падает с ошибкой о нехватке памяти, когда много записей или много свойств выбрано в фильтре.
Это возникает из-за того что много JOIN-ов и запрос получается слишком большим.
Самый простой способ - это написать свою выгрузку.
Добавить в админке кнопку, добавить роут, создать контроллер и в сервисе написать бизнес-логику. Обычно на проекте есть какой-нибудь модуль, который содержит общую бизнес-логику для проекта (core/engine/main), туда этот функционал и добавим.
В этой статье разберем, как создать собственную кастомную выгрузку с полным контролем над процессом.

## Архитектура решения

Наше решение будет состоять из нескольких компонентов:
- Обработчик события для добавления кнопки в административную панель
- Роут для обработки запроса на выгрузку
- Контроллер для координации процесса
- Сервис с бизнес-логикой выгрузки

## Шаг 1. Создание обработчика события

Первым делом создадим класс-обработчик, который добавит кнопку выгрузки в контекстное меню административной панели:

```
php
<?php

namespace Vendor\Engine\Events;

use Bitrix\Main\Context;

class AdminListDisplay
{
public static function onAdminListDisplayHandler(&$items): void
{
$request = Context::getCurrent()->getRequest();
if ($request->get('IBLOCK_ID') === '4') {
$items[] = [
'GLOBAL_ICON' => 'adm-menu-excel',
'TEXT' => 'Выгрузка в Excel',
'LINK' => '/internal/export/excel?IBLOCK_ID=4&type=content&lang=ru&find_el_y=Y&clear_filter=Y&mode=excel',
'LINK_PARAM' => '_blank',
'SHOW_TITLE' => true,
'TITLE' => 'Выгрузить данные из списка в Excel',
];
}
}
}
```

В этом коде мы проверяем, что находимся в нужном инфоблоке (с ID = 4), и добавляем новый пункт меню с параметрами выгрузки.

## Шаг 2. Регистрация обработчика события

Создаем миграцию для подписки на событие `OnAdminContextMenuShow`:

```
php
use Bitrix\Main\EventManager;

EventManager::getInstance()->registerEventHandler(
'main',
'OnAdminContextMenuShow',
'main',
AdminListDisplay::class,
'onAdminListDisplayHandler'
);
```

Это событие срабатывает при формировании контекстного меню в административной части Битрикс.

## Шаг 3. Настройка маршрутизации

Добавляем роут для обработки запроса на выгрузку:

```
php
<?php

/** @noinspection PhpMultipleClassDeclarationsInspection */

use Vendor\Engine\Controller\ExportExcel;
use Bitrix\Main\Routing\RoutingConfigurator;

return static function (RoutingConfigurator $configurator) {
$configurator->get('/internal/export/excel', [ExportExcel::class, 'exportAction']);
};

```

Его можно поместить в файл routes.php в папке модуля.
Роут будет перехватывать GET-запросы на указанный URL и направлять их в соответствующий контроллер.

## Шаг 4. Создание контроллера

Контроллер служит связующим звеном между запросом и бизнес-логикой:

```
php
<?php

namespace Vendor\Engine\Controller;

class ExportExcel extends BaseController
{
protected function getDefaultPreFilters(): array
{
return []; //Тут указываете настройки авторизации, CSRF и допустимые методы
}

public function exportAction(): void
{
(new \Vendor\Engine\Service\ExportExcel())->exportExcel($this->getRequestValues());
}
}
```

Контроллер получает параметры запроса и передает их в сервис для обработки.

## Шаг 5. Реализация сервиса выгрузки

Здесь находится основная логика формирования Excel-файла:

```
php
<?php

namespace Vendor\Engine\Service;

use Bitrix\Main\Loader;
use Bitrix\Main\UserOptions;
use Bitrix\Iblock\IblockTable;
use Bitrix\Main\Type\DateTime;
use Bitrix\Main\LoaderException;
use Bitrix\Iblock\PropertyTable;
use Bitrix\Main\SystemException;
use Bitrix\Main\ArgumentException;
use Bitrix\Main\ObjectPropertyException;
use Bitrix\Highloadblock\HighloadBlockTable as HLBT;

class ExportExcel
{
private int $iblockId;
private string $prefix = 'tbl_iblock_element_';

private array $fields = [];

/**
* В этом методе выбираем все колонки, которые есть в фильтре списка элементов инфоблока
* собираем выгрузку и отправляем в браузер
*
* @throws SystemException
* @throws ArgumentException|LoaderException
*/
public function exportExcel($values): void
{
$type = $values['type'];
$this->iblockId = (int)$values["IBLOCK_ID"];
$tableId = $this->prefix . md5($type . '.' . $this->iblockId);
$aOptions = UserOptions::getOption("main.interface.grid", $tableId, array());
$fields = explode(',', $aOptions['views']['default']['columns']);
$this->fields = $this->prepareSelect($fields);
$elements = $this->getElements($this->iblockId);

$this->sendFile($elements);
}

/**
* Собираем все свойства
*
* @throws ObjectPropertyException
* @throws SystemException
* @throws ArgumentException
*/
protected function prepareSelect(array $fields): array
{
$result = [];

foreach ($fields as $field) {
if (str_contains($field, 'PROPERTY')) {
$propId = explode('_', $field)[1];
$result[] = $this->getCodeById($propId);
} else {
$result[] = [
'code' => $field
];
}
}

return $result;
}

/**
* Получаем код свойства по ID
*
* @throws ArgumentException
* @throws ObjectPropertyException
* @throws SystemException
*/
protected function getCodeById($propId): array
{
$object = PropertyTable::query()
->addSelect('ID')
->addSelect('CODE')
->addSelect('NAME')
->where('ID', $propId)
->where('IBLOCK_ID', $this->iblockId)
->fetchObject();

return [
'id' => $object->getId(),
'code' => $object->getCode(),
'name' => $object->getName()
];
}

/**
* Собираем все значения
*
* @throws ArgumentException
* @throws SystemException|LoaderException
*/
protected function getElements($iblockId): array
{
$entity = IblockTable::getEntity()->wakeUpObject($iblockId);
$query = $entity->getEntityDataClass()::query();

foreach ($this->fields as $column) {
$query->addSelect($column['code']);
}
$collection = $query->fetchCollection();
$result = [];

foreach ($collection as $item) {
$values = [];
foreach ($this->fields as $field) {
$value = '';
//Тут перечисляем все свойства, которые есть в инфоблоке (или которые нужны в выгрузке)
switch ($field['code']) {
case 'ACTIVE':
$value = $item->get($field['code']) ? 'Да' : 'Нет';
break;
case 'BUILDER_NAME': //Свойство типа Справочник
$propXmlId = $item->get($field['code'])?->getValue();
if ($propXmlId !== null) {
$value = $this->getValueFromHLB('BuildersInnIdLink', $propXmlId);
}
break;
case 'ADDRESS':
$propXmlId = $item->get($field['code'])?->getValue();
if ($propXmlId !== null) {
$value = $this->getValueFromHLB('Address', $propXmlId);
}
break;
case 'BUILDER': //Свойства типа Список
case 'READY_TO_PUBLICATE':
case 'READY_EXPLOIT':
case 'READY_CADASTRAL':
case 'BOOL_COMPENSATION':
$propId = $item->get($field['code'])?->getValue();
if ($propId !== null) {
$value = Property::getPropertyEnumValueById($field['id'], $propId);
}
break;
case 'BROADCAST_LINK': //Свойства типа Привязка
case 'ALBUMS':
case 'FILE':
case 'TR_PICTURE':
$value = '';
break;
default:
$value = $item->get($field['code']);
}

if (is_object($value)) {
if ($value instanceof DateTime) {
$value = $value->format('d.m.Y');
} else {
$value = $value->getValue();
}
}
$values[$field['code']] = $value;
}
$result[] = $values;
}
return $result;
}

/**
* Оборачиваем все элементы в вёрстку
*
* @param $items
* @return void
*/
private function buildTable($items): void
{
echo '<html><head><title>Экспорт</title><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<style>
td {mso-number-format:\@;}
.number0 {mso-number-format:0;}
.number2 {mso-number-format:Fixed;}
</style>
</head><body><table border="1">';
//Title
echo '<tr>';
foreach ($this->fields as $header) {
echo '<th>';
echo $header['name'] ?: $header['code'];
echo '</th>';
}
echo "</tr>";
//Body
foreach ($items as $row) {
echo '<tr>';
foreach ($row as $item) {
echo '<td class="number0">' . $item . '</td>';
}
echo '</tr>';
}
echo '</table></body></html>';
}

/**
* Говорим браузеру что это Excel (он охотно верит и отдаёт нам выгрузку в формате xls)
*
* @param $elements
* @return void
*/
private function sendFile($elements): void
{
header("Content-Type: application/vnd.ms-excel");
header("Content-Disposition: filename=export.xls");
$this->buildTable($elements);
require($_SERVER["DOCUMENT_ROOT"].BX_ROOT."/modules/main/include/epilog_admin_after.php");
die();
}

/**
* Получить значение из HL-блока
*
* @throws ObjectPropertyException
* @throws LoaderException
* @throws ArgumentException
* @throws SystemException
*/
private function getValueFromHLB($name, $xmlId): string
{
Loader::includeModule('highloadblock');
$hlblock = HLBT::query()
->addSelect('ID')
->addFilter('NAME', $name)
->fetchObject();
if (!$hlblock) {
throw new \Exception('Не удалось подключить HL-block');
}

$entity = HLBT::compileEntity((int)$hlblock->getId());
$element = $entity->getDataClass()::query()
->addSelect('UF_NAME')
->addSelect('UF_XML_ID')
->where('UF_XML_ID', $xmlId)
->fetchObject();
if ($element === null) {
return '';
}
return $element->getUfName() ?? '';
}
}

```

В комментариях описал основные моменты, в методе getElements в цикле с switch нужно прописать все свойства вашего инфоблока, если их много, то только те, которые понадобятся в выгрузке.

## Замечание

Property - это хелпер, в котором есть вот такой метод

```
php
public static function getPropertyEnumValueById(int $propertyId, int $id): string
{
$object = PropertyEnumerationTable::query()
->addSelect('VALUE')
->where('PROPERTY_ID', $propertyId)
->where('ID', $id)
->setCacheTtl(86400)
->fetchObject();

if (!$object) {
throw new ObjectNotFoundException('Значение типа список не найдено');
}

return $object->getValue();
}
```

## Заключение

Кастомная выгрузка в Excel дает полный контроль над процессом экспорта данных. Вы можете реализовать любую логику формирования отчетов, добавить вычисляемые поля, применить форматирование и создать документы, полностью соответствующие требованиям проекта.