Виджеты в админке или как добавить свою кнопку

В этой заметке хочу поделится вариантом того, как можно добавить кнопку в админку для выполнения какого-либо действия по запросу администратора. Для этого не потребуется ExtJS и в целом, в зависимости от задачи, можно будет обойтись минимумом кода. В процессе мы создадим виджет, подключим очереди и Server Sent Events.

Общаяя задача:
Выполнять полную синхронизацию каталога товаров с Битрикс24 по нажатию на кнопки.

Решение будет иметь 3 уровня сложности. Для первого уровня предположим, что каталог небольшой и скрипт синхронизации будет работать недолго, до 30 секунд.

Создадим виджет.
Для этого переходим в «Панели управления»


Выбираем вкладку «Виджеты» и жмём кнопку «Создать»


Указываем название (оно будет отображаться в шапке), тип виджета — файл, в содержимом указываем путь к файлу виджета.


Затем сохраняем виджет и переходим обратно в «Панели управления» и открываем панель, в которую хотим добавить наш виджет, на редактирование


Добавляем наш виджет и сохраняем изменения


Чтобы виджет начал работать создадим файл core/elements/widgets/sync_bitrix.php со следующим содержимым
<?php

class modDashboardWidgetSyncBitrix extends modDashboardWidgetInterface
{
    public function render()
    {
        $pdoTools = $this->modx->getService('pdoTools');
        return $pdoTools->getChunk('@FILE chunks/widgets/sync_bitrix.tpl', []);
    }
}

return 'modDashboardWidgetSyncBitrix';

Название класса может быть любым, главное, чтобы он наследовал modDashboardWidgetInterface и реализовывал метод render(). Как видите реализация крайне проста: я просто получаю сервис pdoTools и обрабатываю с его помощью чанк widgets/sync_bitrix.tpl. Код чанка такой
<style>
    [data-si-preset="sync_bitrix"]:disabled{
      opacity: 0.5;
      pointer-events: none;
    }
</style>
<form action="" data-si-form>
    <button type="button" data-si-event="click" data-si-preset="sync_bitrix"
            class="x-btn x-btn-small x-btn-icon-small-left primary-button x-btn-noicon">
        Выполнить синхронизацию
    </button>
</form>

Я, конечно же, использую SendIt для отправки запроса на сервер, вы можете написать свою реализацию. Если же будете использовать SendIt, то убедитесь, что у вас установлена версия не ниже 2.1.7. Пресет sync_bitrix выглядит так:
<?php

return [
    'sync_bitrix' => [
        'hooks' => '',
        'method' => 'addToQueueSync',
        'snippet' => '@FILE snippets/sync_bitrix.php',
    ]
];

Также нужно подключить JavaScript компонента SendIt, скрипты обработки ответа и ещё скрипт для работы с SSE, который понадобится если вы дойдёте до третьего уровня сложности. Подключать скрипты можно в чанке, но, поскольку для моего решение скрипты нужны на всех страницах админки, я подключаю их в плагине
<?php
/**
 * @var modX $modx
 */

require_once MODX_CORE_PATH . 'services/vendor/autoload.php';
require_once MODX_CORE_PATH . 'components/sendit/services/sendit.class.php';

switch ($modx->event->name) {
    case 'OnManagerPageInit':
        $jsConfigPath = $modx->getOption('si_js_config_path', '', './sendit.inc.js');
        $cookies = !empty($_COOKIE['SendIt']) ? json_decode($_COOKIE['SendIt'], 1) : [];

        $data = [
            'sitoken' => md5($_SERVER['REMOTE_ADDR'] . time()),
            'sitrusted' => '0',
            'sijsconfigpath' => $jsConfigPath
        ];
        SendIt::setSession($modx, [
            'sitoken' => $data['sitoken'],
            'sendingLimits' => []
        ]);

        $data = array_merge($cookies, $data);
        setcookie('SendIt', json_encode($data), 0, '/');

        $modx->regClientStartupHTMLBlock(
            '            
            <script type="module" src="/assets/project_files/js/mgr/sse.js"></script>
            '
        );
        $modx->regClientStartupHTMLBlock(
            '
            <script type="module" src="/assets/components/sendit/js/web/sendit.js"></script>            
            '
        );
        $modx->regClientStartupHTMLBlock(
            '            
            <script type="module" src="/assets/project_files/js/mgr/admin.js"></script>
            '
        );
        break;
}

Таким образом, при нажатии на кнопку «Выполнить синхронизацию» будет отправлен запрос на сервер, который запустит сниппет snippets/sync_bitrix.php. В сниппете у меня вот такой код
<?php
use CustomServices\SyncBitrix;

require_once MODX_CORE_PATH . 'services/vendor/autoload.php';
/**
 * @var modX $modx
 * @var array $scriptProperties
 * @var SendIt $SendIt
 * @var string $method
 */
$ManageUsers = new SyncBitrix($modx);
$method = $scriptProperties['method'];
if(!method_exists($ManageUsers, $method)){
    return $SendIt->error('Метод '.$method.' не найден', []);
}
$result = $ManageUsers->$method($_POST, $scriptProperties);

if($SendIt){
    if($result['success']){
        return $SendIt->success($result['message'], $result['data']);
    }else{
        return $SendIt->error($result['message'], $result['data']);
    }
}
return $result;

Тут мы подключаем класс SyncBitrix и вызываем переданный в пресете метод addToQueueSync. Однако, вы можете сделать проще и написать код синхронизации прямо в сниппете без подключения классов. На этом первый уровень сложности заканчивается.
Второй уровень сложности подразумевает, что синхронизация длится дольше 30 секунд. Можно было бы добавить лимит и оффсет и отправлять запросы на сервер пока оффсет не превысит общее количество товаров, но у меня уже был код синхронизации, который запускался по крону, а так же был код для работы с очередями, поэтому я решил ничего не переписывать, а использовать, что есть.
Всё, что мы делали до этого, остаётся без изменений, но вместо того, чтобы сразу запускать синхронизацию, мы будем добавлять задачу в очередь в методе addToQueueSync
public function addToQueueSync(array $data, array $properties = []): array
{
    $queueData = [
        'className' => 'CustomServices\SyncBitrix',
        'method' => 'syncSiteProductsWithBitrix',
        'session_id' => session_id(),
    ];
    $this->qm->addToQueue('CustomServices', $queueData);

    return ['success' => true, 'message' => 'Синхронизация начата!', 'data' => []];
}

Затем нужно добавить задание в планировщик, в котором мы будем читать данные из очереди раз в минуту. Если в очереди есть задание на синхронизацию, будет вызван метод syncSiteProductsWithBitrix класса CustomServices\SyncBitrix, потому что именно их мы передавали в качестве сообщения очереди.

На этом можно было бы остановиться, но у нас нет никаких ограничений на количество нажатий на кнопку, т.е. нервный админ может заспамить очередь. Также у нас нет уведомления о том, что синхронизация завершена. Если для ограничения количества нажатий, можно просто блокировать кнопку на N минут, то вот чтобы сообщить пользователю об окончании синхронизации, нам понадобятся Server Sent Events. Тут и начинается третий уровень сложности.
После окончания синхронизации, мы должны добавить сообщение в очередь, при этом имя очереди равно session_id текущего пользователя, его мы так же передавали в сообщении с задачей на запуск синхронизации.
public function syncSiteProductsWithBitrix(?array $data = []): array
{
    // код синхронизации

    $queueData = [
        'eventName' => 'sync:bitrix:finished',
        'message' => 'Синхронизация данных курсов с Б24 завершена!',
    ];
    $this->qm->addToQueue($data['session_id'], $queueData);
}
Для работы с этим типом очередей, т.е. с персональными уведомлениями для конкретного пользовтаеля, создадим новый обработчик
<?php

/**
 * @var \modX $modx
 */

header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
echo "retry: 5000" . "\n\n";

$basePath = dirname(__FILE__, 4);
$path = $basePath . '/core/services/vendor/autoload.php';
if (!file_exists($path)) {
    echo 'data: {"error":"File `autoload.php` not found"}' . "\n\n";
}

define('MODX_API_MODE', true);
require_once $basePath . '/index.php';
require_once $path;

$modx->getService('error', 'error.modError');
$modx->setLogLevel(\modX::LOG_LEVEL_ERROR);
$qm = new CustomServices\QueueManager($modx);
$token = $_COOKIE['PHPSESSID'] ?? '';

/**
 * @param array $messages
 */
function sendMessages(array $messages): void
{
    global $modx;
    foreach ($messages as $id => $message) {
        echo "data: " . $message . "\n\n";
        echo "id: " . $id . "\n\n";
        ob_flush();
        flush();
    }
}

if ($messages = $qm->getMessages($token, true)) {
    sendMessages($messages);
}

В результате виджет будет выглядеть так


В итоге, после нажатия на кнопку в очередь 'CustomServices' добавляется сообщение с указанием класса и метода, который следует вызвать, а так же с session_id пользователя инициировавшего синхронизацию. После обработки этого сообщения, будет запущена синхронизация, по завершению которой в другую очередь будет добавлено сообщение об окончании синхронизации. Это сообщение будет показано тому, кто запустил синхронизацию вне зависимости от того, на какой странице админки он будет находится. А находиться админ может где угодно, так как после нажатия на кнопку 'Выполнить синхронизацию' админка не блокируется и можно выполнять другие задачи.

Весь код можно найти в репозитории

Спасибо за внимание!
Артур Шевченко
02 февраля 2025, 12:25
modx.pro
5
406
+14
Поблагодарить автора Отправить деньги

Комментарии: 4

Николай Савин
02 февраля 2025, 13:37
0
Очень хороший комплексный материал. Низкий поклон. Давненько такого не было.
    Павел Гвоздь
    02 февраля 2025, 17:25
    0
    Есть скрины шагов, но нет итогового скрина, что получилось. С демо картинкой веселее.
    Sergey (Sentinel)
    03 февраля 2025, 17:26
    +1
    Есть еще Quickstart Buttons 😉
    modmore.com/extras/quickstartbuttons/

    modx.pro/reviews/22829
      Авторизуйтесь или зарегистрируйтесь, чтобы оставлять комментарии.
      4