bp/docs/EVENTS.md

21 KiB
Raw Blame History

Справка по системе событий EventManager

Общее описание

EventManager — это сервис для работы с событиями в системе «Бизнес.Точка». Он является обёрткой над встроенной системой событий CodeIgniter 4 и предоставляет два типа подписок на события:

  • moduleOn() — обработчик выполняется только при наличии активной подписки на модуль
  • systemOn() — обработчик выполняется всегда, без проверки статуса подписки

EventManager используется для создания интеграций между модулями, когда действия в одном модуле должны автоматически вызывать события в другом. Например, при создании клиента в CRM может автоматически создаваться задача в модуле Tasks.


Подключение EventManager

EventManager подключается как сервис через service('eventManager'):

$eventManager = service('eventManager');

Доступно в контроллерах через BaseController:

// В контроллере:
$em = service('eventManager');

Основные методы

forModule() — привязка к модулю

Метод forModule() привязывает все последующие вызовы moduleOn() к указанному модулю. Это означает, что подписки на события будут создаваться только если организация имеет активную подписку на этот модуль.

$em = service('eventManager');

// Привязываем события к модулю CRM
$em->forModule('crm');

После вызова forModule() все события, добавленные через moduleOn(), будут проверять подписку организации на модуль CRM. Если подписка не активна — обработчик не будет выполнен.

// Цепочка вызовов
service('eventManager')
    ->forModule('crm')
    ->moduleOn('client.created', function($client) {
        // Этот код выполнится только если подписка на CRM активна
    });

Важно: Метод forModule() необходимо вызвать перед moduleOn(), иначе будет выброшено исключение.


moduleOn() — подписка с проверкой модуля

Метод moduleOn() создаёт подписку на событие, которая выполняется только при соблюдении условий:

  1. Модуль существует в конфигурации BusinessModules
  2. Модуль глобально включён в настройках
  3. Организация имеет активную подписку на модуль
$em = service('eventManager');
$em->forModule('crm');

$em->moduleOn('client.created', function($client) {
    // Обработчик выполнится только если CRM подписка активна
    log_message('debug', 'Клиент создан: ' . $client['name']);
});

Сигнатура метода:

public function moduleOn(
    string $event,       // Имя события
    callable $callback,  // Обработчик события
    int $priority = 100  // Приоритет выполнения
): bool

Возвращаемое значение:

  • true — подписка создана, обработчик будет выполнен
  • false — подписка не создана (модуль не активен, отключён или не существует)

Параметр $callback:

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

$em->forModule('crm');
$em->moduleOn('client.created', function($client, $userId) {
    echo 'Создан клиент ' . $client['name'] . ' пользователем ' . $userId;
});

// Где-то в коде:
Events::trigger('client.created', $clientData, $currentUserId);

Приоритет выполнения:

Параметр $priority определяет порядок выполнения обработчиков. Меньшее значение — более высокий приоритет:

// Выполнится раньше (приоритет 50)
$em->moduleOn('client.created', function($client) {
    // Логирование
}, 50);

// Выполнится позже (приоритет 100, значение по умолчанию)
$em->moduleOn('client.created', function($client) {
    // Отправка уведомлений
});

systemOn() — подписка без проверки модуля

Метод systemOn() создаёт подписку на событие без проверки статуса подписки. Обработчик будет выполнен всегда, независимо от того, какие модули активированы у организации.

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

$em = service('eventManager');

// Этот обработчик выполнится для всех организаций
$em->systemOn('user.login', function($user) {
    log_message('info', 'Пользователь вошёл: ' . $user['email']);
});

// Для отправки email-уведомлений при любом действии
$em->systemOn('email.send', function($to, $subject, $body) {
    // Логирование отправки
});

Сигнатура метода:

public function systemOn(
    string $event,
    callable $callback,
    int $priority = 100
): void

off() — отписка от события

Метод off() удаляет подписку на событие:

$em = service('eventManager');

// Удаление всех обработчиков события
$em->off('client.created');

// Удаление конкретного обработчика
$em->off('client.created', $specificCallback);

currentModuleActive() — проверка статуса модуля

Метод currentModuleActive() возвращает true если текущий модуль (установленный через forModule()) активен для организации.

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

$em->service('eventManager');
$em->forModule('tasks');

$em->moduleOn('deal.won', function($deal) {
    // Проверяем, активен ли модуль Tasks
    if ($em->currentModuleActive()) {
        // Создаём задачу
        createTaskForDeal($deal);
    }
});

getCurrentModuleCode() — получение кода модуля

Метод getCurrentModuleCode() возвращает код модуля, установленного через forModule():

$em = service('eventManager');
$em->forModule('crm');

$code = $em->getCurrentModuleCode(); // Вернёт 'crm'

Встроенные события системы

События пользователя

// После успешной регистрации пользователя
Events::trigger('user.registered', $user);

// После подтверждения email
Events::trigger('user.verified', $user);

// При каждом входе в систему
Events::trigger('user.login', $user);

// При выходе из системы
Events::trigger('user.logout', $user);

// При смене пароля
Events::trigger('user.passwordChanged', $user);

// При изменении профиля
Events::trigger('user.profileUpdated', $user, $oldData);

События организации

// При создании организации
Events::trigger('organization.created', $organization);

// При изменении данных организации
Events::trigger('organization.updated', $organization, $changes);

// При удалении организации (до удаления)
Events::trigger('organization.deleting', $organization);

// После удаления организации
Events::trigger('organization.deleted', $organizationId);

// При присоединении пользователя к организации
Events::trigger('organization.userJoined', $organization, $user, $role);

// При выходе пользователя из организации
Events::trigger('organization.userLeft', $organization, $user);

// При изменении роли пользователя
Events::trigger('organization.userRoleChanged', $organization, $user, $oldRole, $newRole);

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

// При создании клиента
Events::trigger('client.created', $client, $userId);

// При обновлении клиента
Events::trigger('client.updated', $client, $changes, $userId);

// При удалении клиента
Events::trigger('client.deleted', $clientId, $userId);

// При импорте клиентов
Events::trigger('client.imported', $clients, $userId);

Примеры интеграции модулей

Пример 1: Создание задачи при создании клиента

// В модуле Tasks — файл bootstrap или Config/Events.php

service('eventManager')
    ->forModule('tasks')
    ->moduleOn('client.created', function($client, $userId) {
        // Создаём задачу на первичный контакт
        $taskModel = new \App\Modules\Tasks\Models\TaskModel();
        
        $taskModel->insert([
            'organization_id' => $client['organization_id'],
            'title' => 'Первый контакт с клиентом: ' . $client['name'],
            'description' => 'Необходимо связаться с клиентом для уточнения потребностей',
            'assigned_to' => $userId,
            'status' => 'todo',
            'priority' => 'medium',
            'due_at' => date('Y-m-d H:i:s', strtotime('+1 day')),
            'created_by' => $userId,
        ]);
    });

Пример 2: Автоматический переход сделки при завершении задачи

// В модуле CRM
service('eventManager')
    ->forModule('crm')
    ->moduleOn('task.completed', function($task, $userId) {
        if ($task['related_type'] === 'deal' && $task['related_id']) {
            $dealModel = new \App\Modules\CRM\Models\DealModel();
            
            // Получаем сделку
            $deal = $dealModel->find($task['related_id']);
            
            if ($deal && $deal['stage'] === 'proposal') {
                // Переводим сделку на следующий этап
                $dealModel->update($deal['id'], [
                    'stage' => 'negotiation',
                    'updated_at' => date('Y-m-d H:i:s'),
                ]);
                
                // Логируем переход
                log_message('info', 'Сделка #' . $deal['id'] . ' переведена на этап переговоров после завершения задачи');
            }
        }
    });

Пример 3: Уведомление при записи на приём

// В модуле Booking
service('eventManager')
    ->forModule('booking')
    ->moduleOn('booking.created', function($booking, $client, $userId) {
        // Отправляем уведомление клиенту
        $emailService = service('email');
        
        $emailService->send(
            $client['email'],
            'Подтверждение записи',
            'Уважаемый ' . $client['name'] . ', 
            Ваша запись на ' . $booking['service_name'] . ' подтверждена на ' . 
            date('d.m.Y в H:i', strtotime($booking['starts_at']))
        );
        
        // Создаём задачу для менеджера
        $taskModel = new \App\Modules\Tasks\Models\TaskModel();
        $taskModel->insert([
            'organization_id' => $booking['organization_id'],
            'title' => 'Подготовка к записи: ' . $client['name'],
            'description' => 'Клиент записан на услугу ' . $booking['service_name'],
            'assigned_to' => $booking['staff_id'],
            'status' => 'todo',
            'priority' => 'normal',
            'due_at' => $booking['starts_at'],
        ]);
    });

Пример 4: Создание проекта Proof при выигрыше сделки

// В модуле CRM
service('eventManager')
    ->forModule('crm')
    ->moduleOn('deal.won', function($deal, $userId) {
        // Проверяем, активен ли модуль Proof
        if (service('moduleSubscription')->isModuleActive('proof')) {
            $projectModel = new \App\Modules\Proof\Models\ProjectModel();
            
            $projectModel->insert([
                'organization_id' => $deal['organization_id'],
                'client_id' => $deal['client_id'],
                'title' => 'Проект по сделке #' . $deal['id'],
                'description' => 'Автоматически создан при выигрыше сделки',
                'status' => 'active',
                'created_by' => $userId,
            ]);
        }
    });

Правила именования событий

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

Формат: сущность.действие

Сущность Действия Пример события
user registered, verified, login, logout, passwordChanged, profileUpdated user.login
organization created, updated, deleting, deleted, userJoined, userLeft, userRoleChanged organization.created
client created, updated, deleted, imported client.created
deal created, updated, deleted, won, lost deal.won
booking created, updated, cancelled, completed booking.created
task created, updated, deleted, started, completed task.completed
project created, updated, deleted, archived project.created
file uploaded, deleted, approved, rejected file.uploaded
email send, sent, failed email.send

Группы событий модулей

  • CRM: client.*, deal.*, pipeline.*
  • Booking: booking.*, service.*, staff.*
  • Proof: project.*, file.*, comment.*
  • Tasks: task.*, board.*, comment.*

Вызов событий в коде

Для вызова события используйте Events::trigger() из CodeIgniter 4:

use CodeIgniter\Events\Events;

// Простой вызов
Events::trigger('client.created', $clientData);

// С несколькими параметрами
Events::trigger('deal.won', $deal, $userId);

// С именованными параметрами (начиная с CI 4.3+)
Events::trigger('client.updated', [
    'client' => $clientData,
    'changes' => $changes,
    'userId' => $userId,
]);

Порядок инициализации событий

События модулей должны инициализироваться в файле app/Config/Events.php:

<?php

use CodeIgniter\Events\Events;
use CodeIgniter\Shield\Exceptions\RuntimeException;

/*
 * --------------------------------------------------------------------
 * Application Events
 * --------------------------------------------------------------------
 * Events::on() methods to register listeners for application events.
 */

// Загрузка событий модулей
if (is_file(APPPATH . 'Modules/Tasks/Config/Events.php')) {
    require_once APPPATH . 'Modules/Tasks/Config/Events.php';
}

if (is_file(APPPATH . 'Modules/CRM/Config/Events.php')) {
    require_once APPPATH . 'Modules/CRM/Config/Events.php';
}

if (is_file(APPPATH . 'Modules/Booking/Config/Events.php')) {
    require_once APPPATH . 'Modules/Booking/Config/Events.php';
}

if (is_file(APPPATH . 'Modules/Proof/Config/Events.php')) {
    require_once APPPATH . 'Modules/Proof/Config/Events.php';
}

Каждый модуль создаёт свой файл Config/Events.php:

<?php

// app/Modules/CRM/Config/Events.php

use CodeIgniter\Events\Events;
use App\Services\EventManager;

if (!function_exists('register_crm_events')) {
    function register_crm_events(): void
    {
        $em = service('eventManager');
        
        // Интеграция CRM → Tasks
        $em->forModule('crm');
        $em->moduleOn('client.created', function($client, $userId) {
            // Логика создания задачи
        });
        
        // Интеграция CRM → Proof
        $em->moduleOn('deal.won', function($deal, $userId) {
            // Логика создания проекта
        });
    }
}

register_crm_events();

Логирование событий

EventManager автоматически логирует информацию о подписках и выполнении событий:

  • Подписка создана: "EventManager: Subscribed to event 'client.created' for module 'crm'"
  • Модуль отключён: "EventManager: Module 'crm' is disabled globally"
  • Подписка не активна: "EventManager: Organization subscription not active for module 'crm'"
  • Системное событие: "EventManager: System event subscribed: 'user.login'"

Уровень логирования — debug для информации и error для ошибок конфигурации.


Тестирование событий

Ручное тестирование в разработке

// В контроллере для тестирования
public function testEvent()
{
    $testClient = [
        'id' => 999,
        'name' => 'Тестовый клиент',
        'email' => 'test@example.com',
        'organization_id' => session()->get('active_org_id'),
    ];
    
    // Вызываем событие напрямую
    Events::trigger('client.created', $testClient, session()->get('user_id'));
    
    return 'Событие вызвано, проверьте логи';
}

Отладка подписок

// Получение всех обработчиков события
$handlers = Events::listeners('client.created');

foreach ($handlers as $handler) {
    log_message('debug', 'Handler: ' . print_r($handler, true));
}

Типичные ошибки и их устранение

Ошибка: "Module code not set"

// Неправильно:
$em->moduleOn('client.created', $callback);

// Правильно:
$em->forModule('crm')->moduleOn('client.created', $callback);

Событие не срабатывает

Возможные причины:

  1. Модуль не активирован для организации
  2. Модуль отключён глобально в конфигурации
  3. Ошибка в имени события
  4. Исключение в обработчике блокирует выполнение

Проверка:

// Проверка статуса модуля
$em = service('eventManager');
$em->forModule('crm');

if ($em->currentModuleActive()) {
    echo 'Модуль активен';
} else {
    echo 'Модуль не активен';
}

Конфликты приоритетов

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

// Сначала сохраняем данные
$em->moduleOn('deal.won', function($deal) {
    saveDealToArchive($deal);
}, 10);  // Выполнится первым

// Затем отправляем уведомления
$em->moduleOn('deal.won', function($deal) {
    sendDealWonNotification($deal);
}, 100); // Выполнится вторым

Сводка методов

Метод Описание Возвращает
forModule($code) Привязать к модулю $this
moduleOn($event, $callback, $priority) Подписка с проверкой bool
systemOn($event, $callback, $priority) Системная подписка void
off($event, $callback) Отписка void
currentModuleActive() Проверка модуля bool
getCurrentModuleCode() Код модуля string|null