Файлы, форматы и преобразование

Модуль Генератор документов documentgenerator сначала создает файл в формате тела шаблона, а затем при необходимости преобразует его в PDF и JPG. Класс Bitrix\DocumentGenerator\Document управляет файлами конкретного документа, классы Body обрабатывают содержимое, а реализации Storage сохраняют и возвращают файлы.

Используйте API документа, чтобы получить готовые ссылки и запустить преобразование. Обращайтесь к Storage напрямую только при разработке собственной реализации хранилища. Запись документа должна оставаться согласованной с исходным и производными файлами.

тело шаблона
    -> Body обрабатывает поля
    -> Storage сохраняет исходный файл документа
    -> TransformerManager ставит задачу преобразования
    -> Storage сохраняет PDF и JPG

Выбрать формат результата

Формат исходного файла зависит от класса тела шаблона. Модуль сохраняет результат обработки в том же формате, а PDF и JPG создает как дополнительные представления.

Результат

Когда создается

Как получить

Исходный файл

Во время первого успешного вызова Document::getFile()

Ключ downloadUrl в данных Result

PDF

После успешной асинхронной задачи преобразования

Ключ pdfUrl в данных повторного вызова getFile(false)

JPG

После успешной асинхронной задачи преобразования

Ключ imageUrl в данных повторного вызова getFile(false)

Представление для печати

После создания PDF

Ключ printUrl в данных повторного вызова getFile(false)

PDF и JPG не заменяют исходный файл. Если преобразователь недоступен, исходный документ можно сформировать и получить отдельно.

Форматы тел документов

Класс тела определяет:

  • как прочитать шаблон,

  • найти плейсхолдеры,

  • подставить значения и собрать содержимое документа.

Штатные классы поддерживают два исходных формата. MIME-тип — это стандартное обозначение типа содержимого файла, которое хранилище и HTTP-ответ используют вместе с расширением.

Класс тела

Расширение

MIME-тип

Производные форматы

Bitrix\DocumentGenerator\Body\Docx

docx

application/vnd.openxmlformats-officedocument.wordprocessingml.document

PDF и JPG при доступном преобразователе

Bitrix\DocumentGenerator\Body\PlainText

txt

text/plain

PDF и JPG при доступном преобразователе

Тип тела хранится в шаблоне. Не выбирайте класс тела по расширению уже после создания документа. Шаблон должен быть связан с классом, который умеет обработать его содержимое.

Собственный формат требует отдельного класса Body. Реализуйте в нем методы:

  • process() — обработать содержимое,

  • getPlaceholders() — получить плейсхолдеры,

  • getFileExtension() — получить расширение,

  • getFileMimeType() — получить MIME-тип.

После обработки содержимого метод Body::save() передает его выбранному хранилищу и создает запись файла модуля. Чтобы модуль мог выбрать новый тип тела для шаблона, зарегистрируйте класс обработчиком события documentgenerator:onGetBodyTypeList. Класс должен наследовать Body и реализовывать Bitrix\DocumentGenerator\Nameable, иначе реестр его отклонит.

Методы process(), getPlaceholders(), getFileExtension() и getFileMimeType() являются абстрактными. Однако для подключения нового формата недостаточно только реализовать эти методы.

При создании класса учтите дополнительные требования:

  • конструктор получает содержимое шаблона,

  • метод process() возвращает объект Result,

  • метод save() ожидает имя файла в ключе fileName массива $options,

  • формат описания типа и обработка ошибок должны соответствовать классам Bitrix\DocumentGenerator\Body и Bitrix\DocumentGenerator\Registry\Body установленной версии модуля.

Как хранятся файлы

Шаблон, документ и содержимое файлов хранятся раздельно. Запись файла модуля содержит класс хранилища и внутренний идентификатор содержимого в этом хранилище.

Template::FILE_ID ──> запись файла ──> Storage ──> тело шаблона

Document::FILE_ID ──> запись файла ──> Storage ──> исходный документ
Document::PDF_ID  ──> запись файла ──> Storage ──> PDF
Document::IMAGE_ID──> запись файла ──> Storage ──> JPG

Интерфейс Bitrix\DocumentGenerator\Storage отделяет работу с файлами от обработки шаблона. Реализация хранилища отвечает за сохранение, получение и удаление содержимого, но не подставляет значения полей и не запускает генерацию документа.

Метод Storage

Параметры

Результат

read($path)

  • $path — внутренний идентификатор содержимого в выбранном хранилище

Строка с содержимым файла или false

write($content, array $options = [])

  • $content — содержимое файла
  • $options — параметры реализации хранилища. По умолчанию пустой массив

Bitrix\Main\Entity\AddResult. Его идентификатор используют как $path

upload(array $file)

  • $file — массив данных загружаемого файла

Bitrix\Main\Entity\AddResult

download($path, $fileName = '')

  • $path — внутренний идентификатор содержимого
  • $fileName — имя скачиваемого файла. По умолчанию пустая строка

true при успешной отправке, иначе false

delete($path)

  • $path — внутренний идентификатор содержимого

true при успешном удалении, иначе false

getModificationTime($path)

  • $path — внутренний идентификатор содержимого

Время изменения в виде Unix timestamp или false

getSize($path)

  • $path — внутренний идентификатор содержимого

Размер содержимого в байтах

Файловый сценарий использует перечисленные операции интерфейса. Перед разработкой собственного хранилища проверьте обязательные ключи массива $options, формат данных загрузки и обработку ошибок в интерфейсе Bitrix\DocumentGenerator\Storage и выбранной штатной реализации. Правила регистрации класса проверьте в Bitrix\DocumentGenerator\Registry\Storage установленной версии модуля.

Класс Driver предоставляет хранилище по умолчанию:

use Bitrix\DocumentGenerator\Driver;
use Bitrix\Main\Loader;

if (!Loader::includeModule('documentgenerator'))
{
    throw new \RuntimeException('Модуль documentgenerator не установлен');
}

$storage = Driver::getInstance()->getDefaultStorage();
$storageClass = get_class($storage);

Метод getDefaultStorage(): Storage выбирает хранилище в следующем порядке:

  1. Если в настройке default_storage_type указан класс-наследник Storage, метод создает объект этого класса.

  2. Если собственный класс не задан и установлен модуль disk, метод возвращает Bitrix\DocumentGenerator\Storage\Disk.

  3. Если модуль disk не установлен, метод возвращает Bitrix\DocumentGenerator\Storage\BFile.

Метод всегда возвращает объект Storage. Абстрактный класс Bitrix\DocumentGenerator\Storage\File содержит общую реализацию файловых операций для штатных хранилищ.

Код приложения не должен предполагать, где физически находится файл. Получайте ссылки и содержимое через API документа или выбранного Storage.

Получить исходный файл

Метод Document::getFile(bool $sendToTransformation = true, bool $skipTransformationError = false): Result обрабатывает и сохраняет новый документ. У сохраненного документа метод возвращает данные существующего файла и при необходимости запускает создание недостающих производных форматов.

Параметры метода:

  • $sendToTransformation — отправлять ли недостающие PDF и JPG на преобразование. По умолчанию true,

  • $skipTransformationError — исключать ли ошибку преобразования из общего результата. По умолчанию false.

Чтобы сформировать только исходный файл, передайте false первым аргументом:

// Перед запуском подготовьте объект $document и проверьте доступ к его данным
$result = $document->getFile(false);

if (!$result->isSuccess())
{
    throw new \RuntimeException(implode('; ', $result->getErrorMessages()));
}

$data = $result->getData();
$documentId = (int)($data['id'] ?? 0);
$downloadUrl = $data['downloadUrl'];

if ($documentId <= 0)
{
    throw new \RuntimeException('Исходный файл документа не сформирован');
}

Перед вызовом подготовьте документ по инструкции из статьи Документы: загрузите шаблон, создайте объект Document, задайте пользователя, проверьте доступ и обязательные поля.

Проверяйте Result::isSuccess() до чтения данных. Наличие объекта Document не означает, что тело обработано и файл сохранен.

Получить файл сохраненного документа

Загрузите документ по идентификатору и вызовите getFile(false). Такой вызов не ставит новую задачу преобразования.

use Bitrix\DocumentGenerator\Document;

$document = Document::loadById($documentId);

if ($document === null)
{
    throw new \RuntimeException('Документ не найден');
}

$result = $document->getFile(false);

if (!$result->isSuccess())
{
    throw new \RuntimeException(implode('; ', $result->getErrorMessages()));
}

$downloadUrl = $result->getData()['downloadUrl'];

При успешном getFile() ключ downloadUrl присутствует всегда. Ссылка ведет на маршрут скачивания документа, но ее наличие не подтверждает, что хранилище сможет прочитать содержимое в следующем HTTP-запросе. Проверяйте сам ответ скачивания при диагностике хранилища.

Ссылка учитывает реализацию хранилища. Не собирайте путь к файлу из FILE_ID и не обращайтесь напрямую к таблице файлов модуля.

Создать PDF и JPG

Преобразование выполняет модуль Конвертер файлов transformer. Перед вызовом подключите модули documentgenerator и transformer:

use Bitrix\Main\Loader;

if (!Loader::includeModule('documentgenerator'))
{
    throw new \RuntimeException('Модуль documentgenerator не установлен');
}

if (!Loader::includeModule('transformer'))
{
    throw new \RuntimeException('Модуль transformer не установлен');
}

$result = $document->getFile(true);

if (!$result->isSuccess())
{
    throw new \RuntimeException(implode('; ', $result->getErrorMessages()));
}

$data = $result->getData();
$pullTag = $data['pullTag'] ?? null;

Метод getFile(true) сохраняет исходный файл и ставит задачу преобразования в асинхронную очередь documentgenerator_create. Если PDF или JPG уже связан с документом, готовый формат исключается из задания. Если оба файла существуют, новая задача не создается.

Преобразование выполняется асинхронно, поэтому первый результат может содержать downloadUrl и pullTag, но еще не содержать pdfUrl и imageUrl. Ссылки появятся после того, как преобразователь создаст файлы и отправит результат обратно.

Дождаться результата преобразования

Узнать о завершении преобразования можно одним из трех способов: обработать серверное событие, получить уведомление Push and Pull или периодически проверять состояние документа. Выберите способ в зависимости от того, где приложение продолжает работу.

Обработать серверное событие

Используйте событие documentgenerator:onDocumentTransformationComplete, если дальнейшие действия выполняются на сервере. Событие вызывается после обратного вызова преобразователя и передает идентификатор документа documentId и массив результата data. В обработчике загрузите документ заново, проверьте появление pdfUrl и imageUrl, затем продолжите сценарий приложения.

Зарегистрируйте обработчик onDocumentTransformationComplete при установке своего модуля. В примере модуль mycompany.orders получает идентификатор документа, повторно читает его состояние и продолжает работу только после появления PDF и JPG.

use Bitrix\DocumentGenerator\Document;
use Bitrix\Main\Event;

final class DocumentTransformationHandlers
{
    public static function onComplete(Event $event): void
    {
        $documentId = (int)$event->getParameter('documentId');
        $eventData = $event->getParameter('data');

        if ($documentId <= 0 || !is_array($eventData))
        {
            return;
        }

        $document = Document::loadById($documentId);

        if ($document === null)
        {
            return;
        }

        $result = $document->getFile(false);

        if (!$result->isSuccess())
        {
            return;
        }

        $fileData = $result->getData();

        if (empty($fileData['pdfUrl']) || empty($fileData['imageUrl']))
        {
            return;
        }

        // Продолжите сценарий приложения с готовыми PDF и JPG
    }
}

Зарегистрируйте класс обработчика:

use Bitrix\Main\EventManager;

EventManager::getInstance()->registerEventHandler(
    'documentgenerator',
    'onDocumentTransformationComplete',
    'mycompany.orders',
    DocumentTransformationHandlers::class,
    'onComplete'
);

При удалении модуля снимите обработчик с теми же идентификаторами модулей, именем события, классом и методом.

Получить уведомление Push and Pull

Используйте Push and Pull, если результат нужен клиентскому коду и в приложении уже настроена подписка на уведомления. Для этого должны быть установлены модули transformer и pull.

Ключ pullTag имеет вид TRANSFORMDOCUMENT<идентификатор>. Уведомление модуля documentgenerator приходит с командой showImage, а массив params содержит данные getFile(false). Сопоставьте уведомление с документом по тегу, затем проверьте pdfUrl, imageUrl и признаки ошибки в params.

В примере переменная pullTag содержит значение из результата getFile(true). Обработчик пропускает уведомления других документов и продолжает работу только после успешного создания PDF и JPG:

BX.addCustomEvent(
    'onPullEvent-documentgenerator',
    function (command, params)
    {
        if (command !== 'showImage' || params.pullTag !== pullTag)
        {
            return;
        }

        if (params.isTransformationError)
        {
            // Обработайте ошибку преобразования
            return;
        }

        if (params.pdfUrl && params.imageUrl)
        {
            // Продолжите сценарий приложения с готовыми PDF и JPG
        }
    }
);

Наличие pullTag в результате getFile() не подтверждает, что метод поставил новую задачу: оба производных файла могли быть созданы раньше. Если в приложении нет готовой клиентской подписки, используйте серверное событие или периодическую проверку.

Проверять готовность периодически

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

use Bitrix\DocumentGenerator\Document;

$document = Document::loadById($documentId);

if ($document === null)
{
    throw new \RuntimeException('Документ не найден');
}

$result = $document->getFile(false);

if (!$result->isSuccess())
{
    throw new \RuntimeException(implode('; ', $result->getErrorMessages()));
}

$data = $result->getData();
$pdfUrl = $data['pdfUrl'] ?? null;
$imageUrl = $data['imageUrl'] ?? null;
$printUrl = $data['printUrl'] ?? null;

if ($pdfUrl === null || $imageUrl === null)
{
    throw new \RuntimeException('Преобразование еще не завершено');
}

Не используйте отсутствие ссылок в первом результате как признак ошибки. Сначала проверьте isTransformationError, затем дождитесь завершения асинхронной задачи.

Повторно запустить преобразование

Чтобы повторно запустить преобразование, загрузите сохраненный документ по идентификатору и вызовите getFile(true). Метод поставит в очередь только отсутствующие форматы:

use Bitrix\DocumentGenerator\Document;

$document = Document::loadById($documentId);

if ($document === null)
{
    throw new \RuntimeException('Документ не найден');
}

$result = $document->getFile(true);

if (!$result->isSuccess())
{
    throw new \RuntimeException(implode('; ', $result->getErrorMessages()));
}

Не вызывайте update() или actualize() только ради повторного преобразования. Эти методы заново обрабатывают тело и заменяют исходный файл.

При одновременных запросах приложение должно предотвращать постановку дублирующих задач. Храните признак ожидаемого преобразования по идентификатору документа. Установите признак после getFile(true) и снимите после события завершения или ошибки. Пока признак установлен, не вызывайте getFile(true) повторно.

Работать со ссылками

Данные результата содержат downloadUrl после успешного сохранения документа и ссылки на производные файлы, идентификаторы которых уже записаны в документе.

Ключ

Назначение

Условие появления

downloadUrl

Скачать исходный файл документа

Исходный файл сохранен

pdfUrl

Получить PDF

Преобразование в PDF завершено

imageUrl

Получить JPG

Преобразование в JPG завершено

printUrl

Открыть PDF в сценарии печати

PDF создан

publicUrl

Открыть опубликованный документ

Публичная ссылка включена

Файловая часть результата также может содержать:

  • publicUrlView — время просмотра публичной ссылки, если оно сохранено,

  • transformationCancelReason — причина отмены задачи, если преобразователь вернул ее без ошибки,

  • emailDiskFile — сведения о файле для сценария отправки по электронной почте.

Полная таблица данных Result — в статье Документы.

Результат возвращает ссылки как объекты Bitrix\Main\Web\Uri. Если ссылку нужно передать в строковый ответ, приведите объект к строке на границе приложения.

Не сохраняйте полученный URL как постоянный адрес файла. Реализация хранилища и параметры доступа могут измениться. Для нового запроса повторно загрузите документ и получите актуальные данные.

Публичная ссылка позволяет открыть документ без авторизации в продукте. Включайте ее только для документов, которые разрешено передавать внешнему получателю.

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

  • enablePublicUrl(bool $status = true): Result — включает ссылку или отключает ее, если передано false,

  • getPublicUrl(bool $absolute = true): ?Uri — возвращает ссылку или null, если она не включена. По умолчанию формирует абсолютный URL.

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

  1. Проверьте право пользователя на исходный объект и операцию публикации.

  2. Убедитесь, что документ сформирован и содержит актуальные данные.

  3. Сохраните связь между исходным объектом и документом, чтобы позднее отключить доступ.

Включите ссылку, проверьте Result и получите URL:

use Bitrix\DocumentGenerator\Document;

$document = Document::loadById($documentId);

if ($document === null)
{
    throw new \RuntimeException('Документ не найден');
}

$result = $document->enablePublicUrl();

if (!$result->isSuccess())
{
    throw new \RuntimeException(implode('; ', $result->getErrorMessages()));
}

$publicUrl = $document->getPublicUrl();

if ($publicUrl === null)
{
    throw new \RuntimeException('Публичная ссылка не создана');
}

Методы публичной ссылки не проверяют права пользователя. До вызова enablePublicUrl() проверьте право читать исходный объект и разрешение на публикацию документа. Используйте модель прав модуля, который управляет исходным объектом и документом.

После завершения внешнего сценария отключите ссылку и убедитесь, что она больше не связана с документом:

$result = $document->enablePublicUrl(false);

if (!$result->isSuccess())
{
    throw new \RuntimeException(implode('; ', $result->getErrorMessages()));
}

if ($document->getPublicUrl() !== null)
{
    throw new \RuntimeException('Публичная ссылка не отключена');
}

Не считайте удаление локально сохраненного URL отключением доступа. Нужно изменить состояние самого документа.

Публичная ссылка позволяет открыть документ без авторизации пользователя. Не размещайте ее в журналах, сообщениях об ошибках и открытых данных приложения.

Заменить или удалить файлы

Файлы принадлежат сохраненному объекту Document, поэтому заменяйте и удаляйте их только в рамках жизненного цикла документа:

  • чтобы заменить исходный файл, используйте update() или actualize() по инструкции из раздела «Обновить сохраненный документ» статьи Документы,

  • чтобы удалить все файлы вместе с документом, используйте операцию продукта или модуля, который его создал. Подробнее — в разделе «Удалить документ» статьи Документы.

Не удаляйте FILE_ID, PDF_ID или IMAGE_ID напрямую через хранилище. Иначе запись документа продолжит ссылаться на отсутствующее содержимое.

Обработать ошибки

Формирование исходного файла и преобразование — разные этапы. Определяйте этап сбоя до повторного запуска.

Признак

Возможная причина

Действие

getFile() вернул неуспешный Result, идентификатора документа нет

Ошибка тела шаблона, обязательных данных или записи исходного файла

Исправьте входные данные или хранилище и повторите создание документа

Документ сохранен, но результат содержит ошибку преобразования

Модуль transformer недоступен или задача завершилась ошибкой

Сохраните идентификатор документа, устраните причину и повторно вызовите getFile(true)

pdfUrl и imageUrl отсутствуют, ошибки преобразования нет

Асинхронная задача еще не завершена

Дождитесь события, уведомления pull или выполните повторную проверку позже

Маршрут downloadUrl не отдает файл

Запись файла или содержимое недоступны в выбранном хранилище

Проверьте связь документа с записью файла и возможность Storage::read() получить содержимое

Один производный формат отсутствует

Задача создала не все ожидаемые результаты

Проверьте данные ошибки и повторите преобразование после устранения причины

Данные результата могут содержать:

  • isTransformationError — признак ошибки преобразования,

  • transformationErrorMessage — сообщение преобразователя,

  • transformationErrorCode — код ошибки преобразователя.

Второй аргумент getFile() управляет тем, должна ли ошибка преобразования сделать общий результат неуспешным. Передавайте true в $skipTransformationError, только если приложение умеет отдельно зафиксировать и обработать ошибку производных форматов.

$result = $document->getFile(true, true);
$data = $result->getData();

if (!empty($data['isTransformationError']))
{
    $message = (string)($data['transformationErrorMessage'] ?? '');
    $code = $data['transformationErrorCode'] ?? null;

    // Передайте ошибку в журнал приложения без публичных ссылок на документ
}

if (!$result->isSuccess())
{
    throw new \RuntimeException(implode('; ', $result->getErrorMessages()));
}

Флаг не исправляет ошибку и не создает производные файлы. Он позволяет приложению продолжить работу с успешно сохраненным исходным документом и обработать сбой преобразования отдельно.

Проверить результат

После файловой операции проверьте:

  1. Результат операции Result::isSuccess() соответствует выбранной политике обработки ошибок.

  2. Идентификатор документа больше нуля.

  3. Исходный файл downloadUrl доступен для скачивания.

  4. После завершения преобразования появились нужные pdfUrl, imageUrl и printUrl.

  5. Ошибка преобразования isTransformationError не установлен или обработан отдельно.

  6. Ссылки относятся к новой версии исходного файла.

  7. После отключения прежняя публичная ссылка больше не открывает документ.

Для диагностики сохраняйте идентификатор документа, этап операции и сообщения Result. Не записывайте в журнал содержимое документа и публичные ссылки.

Связанные материалы

Предыдущая
Следующая