bp/docs/EVENTS.md

612 lines
21 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Справка по системе событий 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` |