События и расширение модуля
Модуль Генератор документов documentgenerator отправляет события при обработке, сохранении и удалении объектов. Обработчики позволяют связать документ с объектом другого модуля, продолжить сценарий после преобразования файла и очистить связанные данные.
Для добавления собственных классов используйте события реестров. Они регистрируют провайдеры данных, типы тела документа и хранилища. Для подмены провайдера и базовых классов драйвера служат отдельные события. Их нужно обработать до первого создания соответствующего менеджера.
Выбрать точку расширения
Точки расширения делятся на два вида:
-
события жизненного цикла уведомляют об операциях с документами и шаблонами,
-
события расширения добавляют или заменяют классы модуля.
События жизненного цикла
|
Задача |
Событие |
Результат обработчика |
|
Изменить значения перед обработкой тела |
|
Возвращаемое значение не используется |
|
Связать новый документ с объектом приложения |
|
Возвращаемое значение не используется |
|
Обновить связанную запись после повторного сохранения |
|
Возвращаемое значение не используется |
|
Очистить связь перед удалением документа |
|
Результат стандартного ORM-события |
|
Очистить данные после удаления шаблона |
|
Возвращаемое значение не используется |
|
Продолжить работу после преобразования файла |
|
Возвращаемое значение не используется |
|
Обработать дополнительные поля шаблона |
|
Возвращаемое значение не используется |
|
Зафиксировать открытие публичного документа |
|
Возвращаемое значение не используется |
Модуль не отменяет уже выполненные действия, если обработчик события завершился с ошибкой. Возвращаемое значение обработчика также не отменяет создание, обновление или другое действие с документом. ORM-событие удаления работает по правилам ORM.
Используйте события жизненного цикла для интеграции и синхронизации. Обязательные проверки выполняйте до вызова API генератора.
События расширения
|
Задача |
Событие |
Результат обработчика |
|
Зарегистрировать корневой провайдер |
|
Массив описаний классов |
|
Зарегистрировать формат тела |
|
Массив описаний классов |
|
Зарегистрировать хранилище |
|
Массив описаний классов |
|
Подменить провайдер наследником |
|
|
|
Подменить базовый класс драйвера |
|
|
Обработать жизненный цикл документа
Объект Bitrix\Main\Event передает параметры события по имени. Проверяйте тип и состояние полученного объекта перед работой с данными другого модуля.
Перед обработкой тела
Событие documentgenerator:onBeforeProcessDocument срабатывает в начале обработки документа. Модуль уже получил актуальный номер документа, но еще не проверил шаблон и не вызвал обработку тела документа.
Параметр document содержит текущий объект Bitrix\DocumentGenerator\Document. Обработчик может добавить внешние значения через методы документа. Возвращаемое значение обработчика модуль не читает, поэтому через это событие нельзя отменить генерацию.
Пример. Обработчик задает значение для плейсхолдера INTEGRATION_STATUS перед обработкой тела. Код поля должен существовать в шаблоне или описании провайдера.
use Bitrix\DocumentGenerator\Document;
use Bitrix\Main\Event;
final class DocumentGeneratorHandlers
{
public static function onBeforeProcessDocument(Event $event): void
{
$document = $event->getParameter('document');
if (!$document instanceof Document)
{
return;
}
$document->setValues([
'INTEGRATION_STATUS' => 'Передан в обработку',
]);
}
}
Не запускайте из обработчика update(), actualize() или другой сценарий, который повторно обрабатывает тот же документ. Такой вызов снова отправит onBeforeProcessDocument и создаст рекурсию.
После создания и обновления
События onCreateDocument и onUpdateDocument срабатывают только после успешного сохранения тела и записи документа. Оба события передают параметр document с объектом Bitrix\DocumentGenerator\Document.
|
Событие |
Когда срабатывает |
Что уже изменилось |
|
|
При первой записи документа |
Документ получил идентификатор, а исходный файл сохранен |
|
|
При сохранении существующего документа |
Запись и исходный файл заменены, идентификаторы прежних PDF и JPG сброшены |
События отправляются до вызова getFile() в стандартной цепочке генерации. Поэтому обработчик не должен ожидать, что PDF и JPG уже созданы. Для производных форматов используйте onDocumentTransformationComplete.
Пример. Обработчик проверяет новый документ и передает его идентификатор в собственный сервис интеграции. Сервис должен повторно проверять уникальность задания по идентификатору документа.
use Bitrix\DocumentGenerator\Document;
use Bitrix\Main\Application;
use Bitrix\Main\Event;
final class DocumentGeneratorHandlers
{
public static function onCreateDocument(Event $event): void
{
$document = $event->getParameter('document');
if (!$document instanceof Document || $document->ID <= 0)
{
return;
}
try
{
DocumentIntegrationService::enqueueOnce($document->ID);
}
catch (\Throwable $exception)
{
Application::getInstance()
->getExceptionHandler()
->writeToLog($exception)
;
}
}
}
Метод DocumentIntegrationService::enqueueOnce() относится к модулю приложения. Реализуйте в нем постоянный признак уникальности по идентификатору документа. Не заменяйте этот признак статической переменной внутри обработчика. Статическая переменная защищает только от повторного вызова в одном PHP-запросе и не предотвращает дубли между запросами.
Перед удалением документа
У модуля нет отдельного события onDeleteDocument. Для интеграции с удалением используйте стандартное ORM-событие \Bitrix\DocumentGenerator\Model\Document::OnBeforeDelete модуля documentgenerator.
Параметр primary содержит массив первичного ключа удаляемой записи. Идентификатор документа находится в ключе ID. Событие срабатывает до удаления, поэтому обработчик еще может загрузить документ и получить его провайдер.
use Bitrix\DocumentGenerator\Document;
use Bitrix\Main\Event;
final class DocumentGeneratorHandlers
{
public static function onBeforeDeleteDocument(Event $event): void
{
$primary = $event->getParameter('primary');
$documentId = is_array($primary)
? (int)($primary['ID'] ?? 0)
: 0;
if ($documentId <= 0)
{
return;
}
$document = Document::loadById($documentId);
if ($document === null)
{
return;
}
DocumentIntegrationService::removeLink($documentId);
}
}
Не удаляйте файлы документа в обработчике. ORM-модель очищает публичную ссылку и связанные файлы в собственном жизненном цикле. Обработчику интеграции достаточно удалить данные своего модуля.
После удаления шаблона
Событие documentgenerator:onDeleteTemplate срабатывает после удаления записи шаблона. Параметр templateId содержит идентификатор удаленного шаблона типа int.
К этому моменту модуль уже удалил связи шаблона с провайдерами и пользователями. Используйте событие, чтобы очистить дополнительные поля, настройки или ссылки, которые хранит ваш модуль. Загрузить удаленный шаблон через Template::loadById() уже нельзя.
После преобразования файла
Событие documentgenerator:onDocumentTransformationComplete отправляется после обратного вызова модуля Конвертер файлов transformer. Оно срабатывает и после штатной обработки результата, и после исключения внутри обратного вызова.
Параметры события:
-
documentId— идентификатор документа типаint, -
data— массив данных результата. После штатной обработки он содержит данныеDocument::getFile(false)и ключpdfId. При исключении массив может быть пустым.
Обработчик должен повторно загрузить документ и проверить результат getFile(false). Сам факт события не гарантирует наличие PDF и JPG. Готовый пример и правила ожидания приведены в статье Файлы, форматы и преобразование.
При изменении дополнительных полей шаблона
Контроллер шаблонов отправляет событие documentgenerator:onModifyCustomFields после добавления или изменения шаблона, если запрос содержит непустой массив дополнительных полей.
Параметры события:
-
templateId— идентификатор шаблона типаint, -
customFields— массив дополнительных значений из запроса.
Событие предназначено для модуля, который добавил собственные поля в форму шаблона. Проверяйте структуру и допустимые значения массива в обработчике. Не считайте входные данные доверенными только потому, что событие отправил контроллер documentgenerator.
При открытии публичного документа
Событие documentgenerator:onPublicView отправляет компонент публичного просмотра после загрузки документа. Оно передает параметры:
-
document— объектBitrix\DocumentGenerator\Document, -
isFirstTime—true, если время просмотра публичной ссылки еще не было сохранено, посетитель не является сотрудником и компонент успешно записал время первого просмотра.
Используйте событие для действий своего модуля, например для фиксации просмотра связанного объекта. Не меняйте документ и не запускайте его генерацию из обработчика публичного просмотра. Событие может прийти из внешнего запроса, поэтому обработчик должен быть быстрым и устойчивым к повторному вызову.
Зарегистрировать обработчик
Регистрируйте постоянные обработчики один раз при установке своего модуля. В аргументе fromModule передавайте documentgenerator, а в toModuleId — идентификатор собственного модуля.
Разместите методы обработчиков в одном или нескольких классах пространства имен своего модуля и настройте их автозагрузку. В примерах классы сокращены до методов конкретного сценария. Регистрацию выполняйте в установщике модуля, а снятие регистрации — при его удалении.
use Bitrix\Main\EventManager;
$eventManager = EventManager::getInstance();
$eventManager->registerEventHandler(
'documentgenerator',
'onCreateDocument',
'mycompany.documents',
DocumentGeneratorHandlers::class,
'onCreateDocument'
);
При удалении модуля снимите обработчик с теми же идентификаторами модулей, именем события, классом и методом:
use Bitrix\Main\EventManager;
$eventManager = EventManager::getInstance();
$eventManager->unRegisterEventHandler(
'documentgenerator',
'onCreateDocument',
'mycompany.documents',
DocumentGeneratorHandlers::class,
'onCreateDocument'
);
Для ORM-события удаления укажите полное имя события вместо onCreateDocument:
'\Bitrix\DocumentGenerator\Model\Document::OnBeforeDelete'
Общие правила регистрации и форматы событий описаны в статье События.
Расширить реестры классов
Реестры объединяют штатные классы модуля с классами, которые возвращают обработчики. Для реестров используется совместимый формат события: обработчик не получает объект Event, а возвращает обычный массив описаний.
Каждый класс должен существовать к моменту чтения реестра и выполнять требования базового класса или интерфейса. Реестр пропускает неподходящий класс без ошибки в результате.
|
Событие |
Требование к классу |
Назначение |
|
|
Наследует |
Возвращает данные исходного объекта |
|
|
Наследует |
Обрабатывает содержимое формата шаблона |
|
|
Реализует |
Читает, сохраняет, загружает и удаляет содержимое |
Ключ внешнего массива — полное имя регистрируемого класса типа string. Приводите его к нижнему регистру через mb_strtolower(). Значение содержит описание класса.
Обязательные ключи обозначены *.
|
Ключ |
Описание |
|
NAME* |
Локализованное название типа |
|
CLASS* |
Полное имя регистрируемого класса |
|
MODULE* |
Идентификатор модуля, который предоставляет реализацию |
Добавить провайдер данных
Обработчик onGetDataProviderList возвращает провайдеры, с которых документ начинает получать данные. Полная реализация класса, регистрация и проверка результата приведены в разделе «Зарегистрировать корневой провайдер» статьи Провайдеры данных.
Ключ элемента массива задавайте как имя класса в нижнем регистре. Фильтр реестра по MODULE сравнивает значение из описания, поэтому передавайте реальный идентификатор модуля расширения.
Добавить тип тела и хранилище
Сначала реализуйте классы формата и хранилища. Класс формата должен обрабатывать содержимое, находить плейсхолдеры, задавать расширение файла и MIME-тип. Класс хранилища должен читать, записывать, загружать, скачивать и удалять содержимое, а также возвращать время изменения и размер.
Один класс обработчиков может зарегистрировать формат тела документа и хранилище:
use MyCompany\Documents\DocumentGenerator\Body\CustomXml;
use MyCompany\Documents\DocumentGenerator\Storage\PrivateStorage;
final class DocumentGeneratorRegistryHandlers
{
public static function getBodyTypes(): array
{
return [
mb_strtolower(CustomXml::class) => [
'NAME' => CustomXml::getLangName(),
'CLASS' => CustomXml::class,
'MODULE' => 'mycompany.documents',
],
];
}
public static function getStorageTypes(): array
{
return [
mb_strtolower(PrivateStorage::class) => [
'NAME' => PrivateStorage::getLangName(),
'CLASS' => PrivateStorage::class,
'MODULE' => 'mycompany.documents',
],
];
}
}
Зарегистрируйте метод getBodyTypes на событие onGetBodyTypeList, а метод getStorageTypes — на событие onGetStorageTypeList:
use Bitrix\Main\EventManager;
$eventManager = EventManager::getInstance();
$eventManager->registerEventHandler(
'documentgenerator',
'onGetBodyTypeList',
'mycompany.documents',
DocumentGeneratorRegistryHandlers::class,
'getBodyTypes'
);
$eventManager->registerEventHandler(
'documentgenerator',
'onGetStorageTypeList',
'mycompany.documents',
DocumentGeneratorRegistryHandlers::class,
'getStorageTypes'
);
Регистрация хранилища только добавляет класс в реестр. Она не делает его хранилищем по умолчанию. Метод Driver::getDefaultStorage() использует класс из настройки default_storage_type, если он реализует Storage. Иначе драйвер выбирает Bitrix\DocumentGenerator\Storage\Disk при установленном модуле disk или Bitrix\DocumentGenerator\Storage\BFile без него.
Требования к телу документа приведены в разделе «Форматы тел документов» статьи Файлы, форматы и преобразование. Требования к хранилищу приведены в разделе «Как хранятся файлы» статьи Файлы, форматы и преобразование.
Подменить провайдер наследником
Событие documentgenerator:onDataProviderManagerFillSubstitutionProviders позволяет заменить зарегистрированный провайдер совместимым наследником. Обработчик получает объект Bitrix\Main\Event без параметров и возвращает успешный EventResult. В параметрах результата ключом служит исходный класс, а значением — класс подмены.
use Bitrix\Main\Event;
use Bitrix\Main\EventResult;
use MyCompany\Orders\Document\OrderDataProvider;
use MyCompany\Orders\Document\OrderDataProviderExtension;
final class DocumentGeneratorExtensionHandlers
{
public static function substituteProviders(Event $event): EventResult
{
return new EventResult(
EventResult::SUCCESS,
[
OrderDataProvider::class => OrderDataProviderExtension::class,
],
'mycompany.orders'
);
}
}
Чтобы включить подмену, зарегистрируйте метод substituteProviders на событие onDataProviderManagerFillSubstitutionProviders. Сделайте это до первого вызова DataProviderManager::getInstance() или Driver::getInstance() в PHP-запросе. Событие заполняет карту замен при создании менеджера.
Класс замены OrderDataProviderExtension должен наследовать исходный класс OrderDataProvider. Менеджер сравнивает имена исходных классов без учета регистра. Несовместимый класс замены он пропускает.
Чтобы один раз создать исходный класс без подмены, передайте true в опции noSubstitution метода DataProviderManager::getDataProvider(). Если не передать эту опцию, менеджер применит подмену.
Регистрируйте и снимайте обработчик по правилам раздела Зарегистрировать обработчик.
Подменить классы драйвера
Событие documentgenerator:onDriverCollectClasses заменяет базовые классы, которые создает Bitrix\DocumentGenerator\Driver. Подмена действует на весь модуль, поэтому используйте ее только для изменения общего поведения документов, шаблонов, прав или провайдеров. Для одного сценария используйте обработчик события или собственный провайдер.
Обработчик возвращает успешный EventResult с одним или несколькими поддерживаемыми ключами:
|
Ключ |
Базовый класс |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Значение должно быть именем класса-наследника. Драйвер проверяет наследование и оставляет штатный класс, если ключ неизвестен, значение не является строкой или класс несовместим.
use Bitrix\Main\Event;
use Bitrix\Main\EventResult;
use MyCompany\Documents\DocumentGenerator\CustomUserPermissions;
final class DocumentGeneratorExtensionHandlers
{
public static function collectDriverClasses(Event $event): EventResult
{
return new EventResult(
EventResult::SUCCESS,
[
'userPermissionsClassName' => CustomUserPermissions::class,
],
'mycompany.documents'
);
}
}
Зарегистрируйте метод collectDriverClasses на событие onDriverCollectClasses до первого обращения к Driver. При удалении модуля снимите обработчик с теми же аргументами.
Класс CustomUserPermissions должен наследовать Bitrix\DocumentGenerator\UserPermissions и поддерживать те же методы. Событие отправляется только при первом создании Driver в PHP-запросе. Поздняя регистрация обработчика не изменит уже созданный экземпляр.
Подмена влияет на весь модуль documentgenerator, а не на один документ или шаблон. Проверьте создание, загрузку, обновление и удаление документов, права и интеграции других модулей. Для локального изменения поведения предпочитайте собственный провайдер или обработчик конкретного события.
Обработать ошибки и повторные вызовы
Обработчики событий жизненного цикла, кроме ORM-события удаления, выполняются синхронно в том же PHP-процессе, который отправил событие. Модуль не добавляет ошибки обработчика в Result операции и не использует EventResult для отката сохраненных данных. ORM-событие удаления работает по правилам ORM.
Соблюдайте следующие правила:
-
Проверяйте именованные параметры и типы объектов до обращения к ним.
-
Перехватывайте ожидаемые исключения внутри обработчика. Сохраняйте в журнале идентификатор документа или шаблона и этап интеграции, но не содержимое документа и публичные ссылки.
-
Передавайте длительную или нестабильную операцию в очередь своего модуля. Обработчик должен быстро зафиксировать задание и завершиться.
-
Не выполняйте одно действие повторно, если обработчик получил те же данные. Например, не создавайте второе задание для того же документа. Используйте идентификатор документа, имя события и версию интеграции как ключ повторной обработки.
-
Не вызывайте повторную генерацию того же документа из
onBeforeProcessDocument,onCreateDocumentилиonUpdateDocument. Если обновление необходимо, поставьте отдельное задание и храните признак уже выполненного шага. -
Учитывайте частично завершенное состояние. События создания и обновления приходят после сохранения исходного файла, но до готовности производных форматов. Событие преобразования может передать пустой массив данных при исключении.
-
Не полагайтесь на порядок обработчиков разных модулей. Каждый обработчик должен получать актуальное состояние через API и не зависеть от побочного эффекта другого обработчика.
Если обязательная операция приложения должна завершиться вместе с генерацией, выполните ее в сервисе приложения до или после вызова API Document и самостоятельно обработайте Result. Событие оставьте для независимой интеграции, которую можно безопасно повторить.
Проверить расширение
После регистрации обработчиков проверьте законченный сценарий:
-
Установите модуль расширения и убедитесь, что регистрация выполнилась один раз.
-
Создайте документ и проверьте, что
onCreateDocumentполучил объект с идентификатором. -
Обновите документ и убедитесь, что обработчик отличает обновление от создания.
-
Если нужны PDF или JPG, дождитесь
onDocumentTransformationCompleteи повторно проверьтеDocument::getFile(false). -
Удалите документ и шаблон тестовым способом. Проверьте очистку данных только в модуле расширения и отсутствие повторной генерации.
-
Получите список зарегистрированного реестра. Для провайдера используйте
DataProviderManager::getList()с фильтром по идентификатору своего модуля. Для тела вызовитеBitrix\DocumentGenerator\Registry\Body::getList(), для хранилища —Bitrix\DocumentGenerator\Registry\Storage::getList(), затем проверьте ключ своего класса. -
Выполните два одинаковых вызова обработчика и убедитесь, что интеграция не создает дубли.
-
Удалите модуль расширения и проверьте, что все постоянные обработчики сняты с теми же аргументами, с которыми были зарегистрированы.
Связанные материалы
-
События — общие правила регистрации и обработки событий Bitrix Framework.
-
Провайдеры данных — реализация и регистрация собственного источника данных.
-
Документы — жизненный цикл, в который встраиваются обработчики.
-
Файлы, форматы и преобразование — ожидание результата асинхронного преобразования.