612 lines
21 KiB
Markdown
612 lines
21 KiB
Markdown
# Справка по системе событий EventManager
|
||
|
||
## Общее описание
|
||
|
||
EventManager — это сервис для работы с событиями в системе «Бизнес.Точка». Он является обёрткой над встроенной системой событий CodeIgniter 4 и предоставляет два типа подписок на события:
|
||
|
||
- **moduleOn()** — обработчик выполняется только при наличии активной подписки на модуль
|
||
- **systemOn()** — обработчик выполняется всегда, без проверки статуса подписки
|
||
|
||
EventManager используется для создания интеграций между модулями, когда действия в одном модуле должны автоматически вызывать события в другом. Например, при создании клиента в CRM может автоматически создаваться задача в модуле Tasks.
|
||
|
||
---
|
||
|
||
## Подключение EventManager
|
||
|
||
EventManager подключается как сервис через `service('eventManager')`:
|
||
|
||
```php
|
||
$eventManager = service('eventManager');
|
||
```
|
||
|
||
Доступно в контроллерах через BaseController:
|
||
|
||
```php
|
||
// В контроллере:
|
||
$em = service('eventManager');
|
||
```
|
||
|
||
---
|
||
|
||
## Основные методы
|
||
|
||
### forModule() — привязка к модулю
|
||
|
||
Метод `forModule()` привязывает все последующие вызовы `moduleOn()` к указанному модулю. Это означает, что подписки на события будут создаваться только если организация имеет активную подписку на этот модуль.
|
||
|
||
```php
|
||
$em = service('eventManager');
|
||
|
||
// Привязываем события к модулю CRM
|
||
$em->forModule('crm');
|
||
```
|
||
|
||
После вызова `forModule()` все события, добавленные через `moduleOn()`, будут проверять подписку организации на модуль CRM. Если подписка не активна — обработчик не будет выполнен.
|
||
|
||
```php
|
||
// Цепочка вызовов
|
||
service('eventManager')
|
||
->forModule('crm')
|
||
->moduleOn('client.created', function($client) {
|
||
// Этот код выполнится только если подписка на CRM активна
|
||
});
|
||
```
|
||
|
||
**Важно:** Метод `forModule()` необходимо вызвать перед `moduleOn()`, иначе будет выброшено исключение.
|
||
|
||
---
|
||
|
||
### moduleOn() — подписка с проверкой модуля
|
||
|
||
Метод `moduleOn()` создаёт подписку на событие, которая выполняется только при соблюдении условий:
|
||
|
||
1. Модуль существует в конфигурации `BusinessModules`
|
||
2. Модуль глобально включён в настройках
|
||
3. Организация имеет активную подписку на модуль
|
||
|
||
```php
|
||
$em = service('eventManager');
|
||
$em->forModule('crm');
|
||
|
||
$em->moduleOn('client.created', function($client) {
|
||
// Обработчик выполнится только если CRM подписка активна
|
||
log_message('debug', 'Клиент создан: ' . $client['name']);
|
||
});
|
||
```
|
||
|
||
**Сигнатура метода:**
|
||
|
||
```php
|
||
public function moduleOn(
|
||
string $event, // Имя события
|
||
callable $callback, // Обработчик события
|
||
int $priority = 100 // Приоритет выполнения
|
||
): bool
|
||
```
|
||
|
||
**Возвращаемое значение:**
|
||
|
||
- `true` — подписка создана, обработчик будет выполнен
|
||
- `false` — подписка не создана (модуль не активен, отключён или не существует)
|
||
|
||
**Параметр `$callback`:**
|
||
|
||
Обработчик события получает параметры, переданные при вызове события:
|
||
|
||
```php
|
||
$em->forModule('crm');
|
||
$em->moduleOn('client.created', function($client, $userId) {
|
||
echo 'Создан клиент ' . $client['name'] . ' пользователем ' . $userId;
|
||
});
|
||
|
||
// Где-то в коде:
|
||
Events::trigger('client.created', $clientData, $currentUserId);
|
||
```
|
||
|
||
**Приоритет выполнения:**
|
||
|
||
Параметр `$priority` определяет порядок выполнения обработчиков. Меньшее значение — более высокий приоритет:
|
||
|
||
```php
|
||
// Выполнится раньше (приоритет 50)
|
||
$em->moduleOn('client.created', function($client) {
|
||
// Логирование
|
||
}, 50);
|
||
|
||
// Выполнится позже (приоритет 100, значение по умолчанию)
|
||
$em->moduleOn('client.created', function($client) {
|
||
// Отправка уведомлений
|
||
});
|
||
```
|
||
|
||
---
|
||
|
||
### systemOn() — подписка без проверки модуля
|
||
|
||
Метод `systemOn()` создаёт подписку на событие без проверки статуса подписки. Обработчик будет выполнен всегда, независимо от того, какие модули активированы у организации.
|
||
|
||
Используется для системных событий, которые должны работать для всех организаций:
|
||
|
||
```php
|
||
$em = service('eventManager');
|
||
|
||
// Этот обработчик выполнится для всех организаций
|
||
$em->systemOn('user.login', function($user) {
|
||
log_message('info', 'Пользователь вошёл: ' . $user['email']);
|
||
});
|
||
|
||
// Для отправки email-уведомлений при любом действии
|
||
$em->systemOn('email.send', function($to, $subject, $body) {
|
||
// Логирование отправки
|
||
});
|
||
```
|
||
|
||
**Сигнатура метода:**
|
||
|
||
```php
|
||
public function systemOn(
|
||
string $event,
|
||
callable $callback,
|
||
int $priority = 100
|
||
): void
|
||
```
|
||
|
||
---
|
||
|
||
### off() — отписка от события
|
||
|
||
Метод `off()` удаляет подписку на событие:
|
||
|
||
```php
|
||
$em = service('eventManager');
|
||
|
||
// Удаление всех обработчиков события
|
||
$em->off('client.created');
|
||
|
||
// Удаление конкретного обработчика
|
||
$em->off('client.created', $specificCallback);
|
||
```
|
||
|
||
---
|
||
|
||
### currentModuleActive() — проверка статуса модуля
|
||
|
||
Метод `currentModuleActive()` возвращает `true` если текущий модуль (установленный через `forModule()`) активен для организации.
|
||
|
||
Используется внутри обработчиков для проверки:
|
||
|
||
```php
|
||
$em->service('eventManager');
|
||
$em->forModule('tasks');
|
||
|
||
$em->moduleOn('deal.won', function($deal) {
|
||
// Проверяем, активен ли модуль Tasks
|
||
if ($em->currentModuleActive()) {
|
||
// Создаём задачу
|
||
createTaskForDeal($deal);
|
||
}
|
||
});
|
||
```
|
||
|
||
---
|
||
|
||
### getCurrentModuleCode() — получение кода модуля
|
||
|
||
Метод `getCurrentModuleCode()` возвращает код модуля, установленного через `forModule()`:
|
||
|
||
```php
|
||
$em = service('eventManager');
|
||
$em->forModule('crm');
|
||
|
||
$code = $em->getCurrentModuleCode(); // Вернёт 'crm'
|
||
```
|
||
|
||
---
|
||
|
||
## Встроенные события системы
|
||
|
||
### События пользователя
|
||
|
||
```php
|
||
// После успешной регистрации пользователя
|
||
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);
|
||
```
|
||
|
||
### События организации
|
||
|
||
```php
|
||
// При создании организации
|
||
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);
|
||
```
|
||
|
||
### События модуля Клиенты
|
||
|
||
```php
|
||
// При создании клиента
|
||
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: Создание задачи при создании клиента
|
||
|
||
```php
|
||
// В модуле 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: Автоматический переход сделки при завершении задачи
|
||
|
||
```php
|
||
// В модуле 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: Уведомление при записи на приём
|
||
|
||
```php
|
||
// В модуле 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 при выигрыше сделки
|
||
|
||
```php
|
||
// В модуле 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:
|
||
|
||
```php
|
||
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
|
||
<?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
|
||
<?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` для ошибок конфигурации.
|
||
|
||
---
|
||
|
||
## Тестирование событий
|
||
|
||
### Ручное тестирование в разработке
|
||
|
||
```php
|
||
// В контроллере для тестирования
|
||
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 'Событие вызвано, проверьте логи';
|
||
}
|
||
```
|
||
|
||
### Отладка подписок
|
||
|
||
```php
|
||
// Получение всех обработчиков события
|
||
$handlers = Events::listeners('client.created');
|
||
|
||
foreach ($handlers as $handler) {
|
||
log_message('debug', 'Handler: ' . print_r($handler, true));
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Типичные ошибки и их устранение
|
||
|
||
### Ошибка: "Module code not set"
|
||
|
||
```php
|
||
// Неправильно:
|
||
$em->moduleOn('client.created', $callback);
|
||
|
||
// Правильно:
|
||
$em->forModule('crm')->moduleOn('client.created', $callback);
|
||
```
|
||
|
||
### Событие не срабатывает
|
||
|
||
Возможные причины:
|
||
|
||
1. Модуль не активирован для организации
|
||
2. Модуль отключён глобально в конфигурации
|
||
3. Ошибка в имени события
|
||
4. Исключение в обработчике блокирует выполнение
|
||
|
||
Проверка:
|
||
|
||
```php
|
||
// Проверка статуса модуля
|
||
$em = service('eventManager');
|
||
$em->forModule('crm');
|
||
|
||
if ($em->currentModuleActive()) {
|
||
echo 'Модуль активен';
|
||
} else {
|
||
echo 'Модуль не активен';
|
||
}
|
||
```
|
||
|
||
### Конфликты приоритетов
|
||
|
||
При использовании нескольких обработчиков одного события убедитесь в корректном порядке выполнения:
|
||
|
||
```php
|
||
// Сначала сохраняем данные
|
||
$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` |
|