Подключение источника данных через Provider SDK
Провайдер данных связывает блок редактора с записями инфоблока, собственной таблицы, каталога или внешнего API. В документе сохраняются идентификаторы выбранных записей и настройки отображения. Серверный код провайдера получает эти записи из источника и формирует HTML.
Начните с учебного каталога из двух записей. После проверки примера подключите свой источник данных. Форматы объектов и ограничения описаны в справочнике Provider SDK.
1. Реализуйте четыре интерфейса
Типы находятся в пространстве имён GlobalIT\WYSIWYG\Domain.
| Интерфейс | Что реализовать |
|---|---|
EntityProviderInterface |
Описание источника, поиск и получение записей по идентификаторам |
EntityRendererInterface |
HTML для предпросмотра в редакторе и для посетителей сайта |
EntityPermissionInterface |
Проверки доступа к списку источников, поиску, выбору и выводу записей |
EntityCachePolicyInterface |
Время хранения, теги и очистка кеша |
interface EntityProviderInterface {
public function descriptor(): EntityProviderDescriptor;
// list<EntityRecord>
public function search(EntitySearchRequest $request, EntityContext $context): array;
// list<string> -> list<EntityRecord>
public function resolve(array $entityIds, EntityContext $context): array;
}
interface EntityRendererInterface {
public function renderEditorPreview(EntitySelection $selection, array $entities, EntityContext $context): EntityRenderResult;
public function renderPublic(EntitySelection $selection, array $entities, EntityContext $context): EntityRenderResult;
}
interface EntityPermissionInterface {
public function canDiscover(EntityContext $context): bool;
public function canSearch(EntityContext $context): bool;
public function canSelect(EntitySelection $selection, EntityContext $context): bool;
public function canRender(EntitySelection $selection, EntityContext $context): bool;
}
interface EntityCachePolicyInterface {
public function ttl(EntitySelection $selection): int;
public function tags(EntitySelection $selection): array; // list<string>
public function invalidate(array $entityIds): void;
}
В примере один класс реализует все четыре интерфейса. При необходимости разделите их на классы и передайте объекты в EntityProviderDefinition.
2. Создайте учебный каталог
Добавьте класс в свой проект или модуль и настройте автозагрузку. Храните его отдельно от модуля редактора, чтобы обновление редактора не затронуло ваш код.
Обе записи примера открыты всем, поэтому проверка публичного доступа их пропускает. Для своего источника проверяйте ещё сайт, состояние записи и права доступа.
<?php
declare(strict_types=1);
namespace Project\Editor;
use GlobalIT\WYSIWYG\Domain as D;
final class PublicCatalog implements
D\EntityProviderInterface, D\EntityRendererInterface,
D\EntityPermissionInterface, D\EntityCachePolicyInterface
{
private const ITEMS = ['intro' => 'Знакомство', 'support' => 'Поддержка'];
public function descriptor(): D\EntityProviderDescriptor
{
return new D\EntityProviderDescriptor(
providerId: 'project.public-catalog',
label: 'Учебный каталог', entityLabel: 'Раздел',
settingsSchema: [[
'key' => 'showTitle', 'type' => 'boolean',
'label' => 'Показать заголовок', 'default' => true,
]],
);
}
public function search(D\EntitySearchRequest $request, D\EntityContext $context): array
{
$records = [];
foreach (self::ITEMS as $id => $label) {
if ($request->query === '' || mb_stripos($label, $request->query) !== false) {
$records[] = new D\EntityRecord($id, $label);
}
}
return array_slice($records, $request->offset, $request->limit);
}
public function resolve(array $entityIds, D\EntityContext $context): array
{
$records = [];
foreach ($entityIds as $id) {
if (isset(self::ITEMS[$id])) $records[] = new D\EntityRecord($id, self::ITEMS[$id]);
}
return $records;
}
public function canDiscover(D\EntityContext $context): bool
{
return $context->editor && $context->userId !== null;
}
public function canSearch(D\EntityContext $context): bool
{
return $this->canDiscover($context);
}
public function canSelect(D\EntitySelection $selection, D\EntityContext $context): bool
{
return $this->canDiscover($context) && $this->containsKnownIds($selection);
}
public function canRender(D\EntitySelection $selection, D\EntityContext $context): bool
{
return $this->containsKnownIds($selection); // Только публичные статические записи.
}
private function containsKnownIds(D\EntitySelection $selection): bool
{
return $selection->providerId === 'project.public-catalog'
&& array_diff($selection->entityIds, array_keys(self::ITEMS)) === [];
}
public function renderEditorPreview(D\EntitySelection $selection, array $entities, D\EntityContext $context): D\EntityRenderResult
{
return $this->renderPublic($selection, $entities, $context);
}
public function renderPublic(D\EntitySelection $selection, array $entities, D\EntityContext $context): D\EntityRenderResult
{
$html = '<section class="project-public-catalog">';
if ($selection->displaySettings['showTitle'] ?? true) $html .= '<h2>Разделы</h2>';
$html .= '<ul>';
foreach ($entities as $entity) {
$html .= '<li>' . htmlspecialchars($entity->label, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</li>';
}
return new D\EntityRenderResult($html . '</ul></section>');
}
public function ttl(D\EntitySelection $selection): int { return 0; }
public function tags(D\EntitySelection $selection): array { return []; }
public function invalidate(array $entityIds): void {}
}
3. Зарегистрируйте провайдер
Добавьте обработчик в загружаемый include.php своего модуля или в local/php_interface/init.php. Класс Project\Editor\PublicCatalog должен быть доступен через автозагрузку к моменту вызова обработчика.
Обработчик должен подключаться при работе редактора, публичном выводе и фоновом формировании HTML — везде, где используется провайдер.
<?php
use Bitrix\Main\Event;
use Bitrix\Main\EventManager;
use Bitrix\Main\Loader;
use GlobalIT\WYSIWYG\Domain\EntityProviderDefinition;
use GlobalIT\WYSIWYG\Domain\EntityProviderRegistry;
if (Loader::includeModule('globalit.wysiwyg')) {
EventManager::getInstance()->addEventHandler(
'globalit.wysiwyg', EntityProviderRegistry::EVENT_COLLECT_PROVIDERS,
static function (Event $event): void {
$registry = $event->getParameter(0);
if (!$registry instanceof EntityProviderRegistry) return;
$adapter = new \Project\Editor\PublicCatalog();
$registry->register(new EntityProviderDefinition($adapter, $adapter, $adapter, $adapter));
}
);
}
Откройте в редакторе «Источник данных», выберите «Учебный каталог», найдите запись и примените выбор. Измените настройку показа заголовка, сохраните документ и откройте его снова. Отдельно проверьте страницу сайта.
Если каталога нет в списке, проверьте, вызывается ли обработчик регистрации и возвращает ли canDiscover() значение true для текущего контекста.
4. Подключите свой интерфейс выбора записей
Если вы создаёте свой диалог выбора, вызывайте globalit:wysiwyg.api.provider.queryJson — то же действие Битрикс, что использует редактор. Отправляйте POST-запрос в активной сессии с CSRF-токеном. Сервер проверит права на редактирование документа.
Отправляйте JSON, чтобы сохранить типы чисел, логических значений и null, а также пустые настройки. В contentUid передайте идентификатор открытого документа.
// contentUid берётся из открытого редактора, например через agent.v1.listEditors().
const response = await BX.ajax.runAction('globalit:wysiwyg.api.provider.queryJson', {
json: {payload: {operation: 'search', contentUid,
providerId: 'project.public-catalog', query: '', limit: 20, offset: 0}},
});
const result = response.data;
if (result.status !== 'ok') throw new Error(result.status);
console.log(result.records);
operation |
Поля помимо operation и contentUid |
Результат в response.data |
|---|---|---|
discover |
Дополнительные поля не нужны | {status, providers} |
search |
providerId, необязательные query, limit, offset |
{status, records} |
select |
providerId, entityIds, необязательные schemaVersion, displaySettings |
{status, records} |
preview |
Те же поля, что у select |
{status, html, stylesheets?} |
select проверяет выбранные записи, preview возвращает очищенный HTML предпросмотра. Результат нужно применить к блоку через редактор и затем сохранить документ. Эти запросы сами по себе блок не вставляют и страницу не публикуют.
Публичный вывод запускает сервер. Если из браузера передать editor: false, запрос не станет публичным и права пользователя не изменятся.
5. Подключите свой источник данных
Задайте в серверной конфигурации провайдера адреса подключения, идентификаторы инфоблоков и коды свойств.
В search() выбирайте активные записи нужного сайта с учётом прав и пагинации. В resolve() снова проверьте записи по сохранённым идентификаторам: после выбора они могли измениться или стать недоступными.
Для инфоблоков подключите модуль iblock и используйте его проверки прав. Значение editor лишь указывает, откуда вызван провайдер; доступ к данным проверяйте отдельно.
Общий HTML собирайте без опоры на права автора из $USER. Персональные данные отдавайте отдельно и только после проверки прав того, кто их получает.
Экранируйте текст и атрибуты. Модуль дополнительно очищает HTML провайдера. Для JavaScript используйте механизм расширений, а не скрипты внутри возвращаемого HTML.
6. Обновляйте страницы при изменении источника
Когда изменение источника уже записано в базу, отправьте уведомление:
$result = (new \GlobalIT\WYSIWYG\Service\ProviderNotificationService())->notify(
$eventUuid, 'project.public-catalog', ['intro'], $userId
);
Создайте для события UUID версии 4 и передайте его в $eventUuid. При повторной отправке сохраняйте этот UUID. Проверьте результат вызова: успешный ответ подтверждает приём уведомления; страницы обновляются через очередь заданий.
Чтобы уведомление не потерялось, запишите его в очередь outbox в одной транзакции с изменением данных. Для БД Битрикс используйте ModuleProviderOutbox. Для другой БД реализуйте ProviderOutboxInterface; его методы описаны в справочнике.
После приёма уведомления модуль находит связанные опубликованные документы и ставит их HTML на обновление. Кеш очищается в ходе обработки. Для внешнего кеша или CDN подключите обработчик очистки.
EntityProviderService::invalidate() очищает кеш вывода и кеш провайдера. Для обновления опубликованных страниц отправляйте уведомление об изменении источника, как показано выше.
Готовый пример с настройками отображения
Архив project.catalog содержит провайдер с JSON-источником, проверкой прав и двумя вариантами вывода: списком и карточками. Настройте пути и правила доступа под свой проект. Закрытый JSON-файл каталога храните вне публичного корня сайта.