Skip to content
MiniShop3
MiniShop3
Современный компонент интернет-магазина для MODX 3
  1. Компоненты
  2. MiniShop3
  3. Разработка
  4. События
  5. Уведомления

События уведомлений

События системы уведомлений: отправка, модификация, регистрация каналов.

Встроенные каналы

MiniShop3 включает два канала уведомлений из коробки:

КаналКлассОписание
emailEmailChannelОтправка через MODX modMail
telegramTelegramChannelОтправка через Telegram Bot API

Email канал

Использует стандартный MODX modMail. Шаблоны настраиваются через чанки.

Telegram канал

Отправляет сообщения через Telegram Bot API. Требует настройки:

  • ms3_telegram_bot_token — токен бота
  • ms3_telegram_manager — Chat ID менеджеров (через запятую)

Ограничение Telegram

Telegram-бот не может инициировать диалог с пользователем. Клиент должен сам написать боту первое сообщение. Поэтому Telegram-уведомления работают только для менеджеров, которые заранее настроили Chat ID.

Реализация уведомлений клиентам через Telegram

Для отправки Telegram-уведомлений клиентам необходимо:

  1. Получить согласие клиента на получение уведомлений
  2. Привязать Telegram-аккаунт клиента к его профилю в магазине
  3. Сохранить Chat ID клиента

Шаг 1: Добавить поле для Chat ID

Создайте дополнительное поле в msCustomer для хранения Chat ID:

sql
ALTER TABLE modx_ms3_customers ADD COLUMN telegram_chat_id VARCHAR(50) NULL;

Или используйте Extra Fields в настройках MiniShop3.

Шаг 2: Создать бота с Deep Linking

Используйте Telegram Deep Linking для привязки аккаунта:

php
<?php
// Генерация уникальной ссылки для привязки
$customerId = $msCustomer->get('id');
$token = hash('sha256', $customerId . $modx->getOption('ms3_snippet_token_secret'));
$linkCode = base64_encode($customerId . ':' . substr($token, 0, 16));

$botUsername = 'YourShopBot'; // Имя вашего бота
$telegramLink = "https://t.me/{$botUsername}?start={$linkCode}";

Шаг 3: Обработка команды /start на стороне бота

Бот должен обрабатывать параметр start и сохранять Chat ID:

php
<?php
// Webhook обработчик бота (упрощённый пример)
$update = json_decode(file_get_contents('php://input'), true);
$message = $update['message'] ?? null;

if ($message && str_starts_with($message['text'], '/start ')) {
    $linkCode = substr($message['text'], 7);
    $decoded = base64_decode($linkCode);
    [$customerId, $tokenPart] = explode(':', $decoded);

    // Проверка токена
    $expectedToken = hash('sha256', $customerId . $modx->getOption('ms3_snippet_token_secret'));
    if (substr($expectedToken, 0, 16) === $tokenPart) {
        // Сохраняем Chat ID в профиле клиента
        $customer = $modx->getObject(\MiniShop3\Model\msCustomer::class, $customerId);
        if ($customer) {
            $customer->set('telegram_chat_id', $message['chat']['id']);
            $customer->save();

            // Отправляем подтверждение
            sendTelegramMessage($message['chat']['id'], '✅ Уведомления подключены!');
        }
    }
}

Шаг 4: Плагин для отправки уведомлений клиенту

php
<?php
/**

 * Плагин: Telegram уведомления клиентам
 * События: msOnChangeOrderStatus

 */

switch ($modx->event->name) {
    case 'msOnChangeOrderStatus':
        $order = $scriptProperties['order'];
        $newStatus = $scriptProperties['status'];

        // Получаем клиента
        $customer = $order->getOne('Customer');
        if (!$customer) {
            return;
        }

        $chatId = $customer->get('telegram_chat_id');
        if (empty($chatId)) {
            return; // Клиент не привязал Telegram
        }

        // Формируем сообщение
        $statusName = $modx->lexicon($newStatus->get('name'));
        $orderNum = $order->get('num');

        $message = "📦 Заказ #{$orderNum}\n";
        $message .= "Статус изменён на: {$statusName}";

        // Отправляем через Telegram API
        $botToken = $modx->getOption('ms3_telegram_bot_token');
        $url = "https://api.telegram.org/bot{$botToken}/sendMessage";

        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => [
                'chat_id' => $chatId,
                'text' => $message,
                'parse_mode' => 'HTML',
            ],
            CURLOPT_RETURNTRANSFER => true,
        ]);
        curl_exec($ch);
        curl_close($ch);
        break;
}

Шаг 5: Кнопка привязки в личном кабинете

Добавьте в шаблон профиля клиента:

fenom
{if $customer.telegram_chat_id}
    <div class="alert alert-success">
        <i class="bi bi-telegram"></i> Telegram уведомления подключены
    </div>
{else}
    <a href="{$telegramLink}" class="btn btn-primary" target="_blank">
        <i class="bi bi-telegram"></i> Подключить Telegram уведомления
    </a>
{/if}

Альтернативный подход

Вместо собственного бота можно интегрироваться с существующими сервисами рассылок (SendPulse, Unisender и др.), которые предоставляют API для Telegram и берут на себя работу с подписками.

msOnBeforeSendNotification

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

Параметры

ПараметрТипОписание
notificationNotificationОбъект уведомления (абстрактный класс, конкретный подкласс — например StatusChangedNotification)
recipientarrayДанные получателя — см. структуру ниже
recipientTypestringТип получателя: customer или manager
channelsstring[]Список каналов отправки (например, ['email', 'telegram'])

Структура recipient

Для customer (начиная с MiniShop3 1.12.0) recipient собирается в OrderStatusService::getCustomerRecipient() со следующим приоритетом источников:

  1. msOrderAddress заказа — email/phone из формы оформления именно этого заказа.
  2. msCustomer — fallback по email/phone + источник для telegram_chat_id и payload customer.
  3. modUserProfile — последний fallback для email/phone/telegram, когда первые два пусты.

Резолвленные email/phone дополнительно зеркалятся в recipient['customer']['email']/['phone'] для плагинов, читающих контакт оттуда.

КлючТипИсточникОписание
typestringcustomer или manager (дублирует recipientType)
email`string\null`address → customer → profileРезолвленный email получателя
phone`string\null`address → customer → profileРезолвленный телефон
telegram_chat_id`string\null`customer.extended → profileID чата Telegram, если задан
addressarraymsOrderAddress->toArray()Полный адрес заказа (доступен только для customer-получателя)
customerarraymsCustomer->toArray()Объект клиента (доступен только для customer-получателя); поля email/phone уже синхронизированы с резолвленными значениями

Зачем разделять адрес и клиента

До 1.12.0 уведомления слались по контактам из msCustomer, которые могли отличаться от контактов конкретного заказа (например, если в одной сессии оформили два заказа с разными email — оба уходили на email из msCustomer).

Прерывание операции

Отмена отправки — return false или return 'cancel', а также классический $modx->event->output(...) (тоже трактуется как отмена). Событие идёт через EventGate::invokeRaw(), не через Utils::invokeEvent.

php
<?php
switch ($modx->event->name) {
    case 'msOnBeforeSendNotification':
        $recipient = $scriptProperties['recipient'];

        $hour = (int) date('G');
        if ($hour >= 23 || $hour < 8) {
            return false;
        }

        if (!empty($recipient['email'])) {
            $domain = substr($recipient['email'], strpos($recipient['email'], '@') + 1);
            if (in_array($domain, ['tempmail.com', 'throwaway.com'], true)) {
                return 'cancel';
            }
        }
        break;
}

Модификация через returnedValues

Параллельно работает мутация $recipient / $channels по ссылке. Явный контракт — ключи в returnedValues:

php
<?php
switch ($modx->event->name) {
    case 'msOnBeforeSendNotification':
        $values = &$modx->event->returnedValues;

        $values['recipient'] = array_replace(
            $scriptProperties['recipient'],
            ['email' => 'ops@example.com']
        );

        // list заменяет список каналов целиком
        $values['channels'] = ['email'];
        break;
}

msOnAfterSendNotification

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

Параметры

ПараметрТипОписание
notificationNotificationОбъект уведомления (абстрактный класс, конкретный подкласс — например StatusChangedNotification)
recipientarrayДанные получателя — см. структуру recipient выше
recipientTypestringТип получателя: customer или manager
resultsarray<string,bool>Результат отправки по каналам, например ['email' => true, 'telegram' => false]

Пример использования

php
<?php
switch ($modx->event->name) {
    case 'msOnAfterSendNotification':
        $notification = $scriptProperties['notification'];
        $recipient = $scriptProperties['recipient'];
        $results = $scriptProperties['results']; // ['email' => true, 'telegram' => false]

        foreach ($results as $channelName => $success) {
            $contact = $recipient['email'] ?? $recipient['phone'] ?? 'unknown';
            $level = $success ? modX::LOG_LEVEL_INFO : modX::LOG_LEVEL_ERROR;
            $modx->log($level, sprintf(
                '[Notification] %s → %s (%s): %s',
                $notification::class,
                $contact,
                $channelName,
                $success ? 'ok' : 'fail'
            ));
        }
        break;
}

Повторная отправка при ошибке

php
<?php
switch ($modx->event->name) {
    case 'msOnAfterSendNotification':
        $results = $scriptProperties['results'];

        if (in_array(false, $results, true)) {
            // Ваша очередь повторов — в пакете нет msNotificationQueue
            $modx->log(modX::LOG_LEVEL_WARN, '[Notification] Partial failure: ' . json_encode($results));
        }
        break;
}

msOnRegisterNotificationChannels

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

Параметры

ПараметрТипОписание
managerNotificationManagerМенеджер уведомлений

Регистрация кастомного канала

NotificationManager::registerChannel() принимает объект ChannelInterface, не callback.

php
<?php
switch ($modx->event->name) {
    case 'msOnRegisterNotificationChannels':
        /** @var \MiniShop3\Notifications\NotificationManager $manager */
        $manager = $scriptProperties['manager'];

        $manager->registerChannel(new MyComponent\Notifications\SmsChannel($modx));
        break;
}

Пример кастомного канала

php
<?php
namespace MyComponent\Notifications;

use MiniShop3\Model\msOrder;
use MiniShop3\Notifications\ChannelInterface;
use MiniShop3\Notifications\Notification;
use MODX\Revolution\modX;

class SmsChannel implements ChannelInterface
{
    public function __construct(protected modX $modx) {}

    public function getName(): string
    {
        return 'sms';
    }

    public function isAvailable(): bool
    {
        return (bool) $this->modx->getOption('my_sms_api_key');
    }

    public function getRequirements(): array
    {
        return ['my_sms_api_key'];
    }

    public function send(Notification $notification, array $recipient, msOrder $order): bool
    {
        $phone = $recipient['phone'] ?? null;
        if (!$phone) {
            return false;
        }

        // Вызов вашего SMS API
        return true;
    }
}

Полный пример: аналитика уведомлений

php
<?php
/**

 * Плагин: Аналитика уведомлений
 * События: msOnBeforeSendNotification, msOnAfterSendNotification

 */

switch ($modx->event->name) {

    case 'msOnBeforeSendNotification':
        $notification = $scriptProperties['notification'];

        // Сохраняем время начала для расчёта длительности
        $modx->eventData['notification_start'] = microtime(true);
        break;

    case 'msOnAfterSendNotification':
        $notification = $scriptProperties['notification'];
        $recipient = $scriptProperties['recipient'];
        $results = $scriptProperties['results'];

        $startTime = $modx->eventData['notification_start'] ?? microtime(true);
        $duration = round((microtime(true) - $startTime) * 1000, 2);

        foreach ($results as $channelName => $success) {
            $modx->log(
                $success ? modX::LOG_LEVEL_INFO : modX::LOG_LEVEL_ERROR,
                sprintf(
                    '[Notification] %s → %s (%s): %s in %sms',
                    $notification::class,
                    $recipient['email'] ?? $recipient['phone'] ?? 'unknown',
                    $channelName,
                    $success ? 'ok' : 'fail',
                    $duration
                )
            );
        }
        break;
}