# 17. Агенты — AbstractAgent (полный паттерн)
[← Оглавление](./index.md)
---
В проекте используется абстрактный базовый класс **AbstractAgent**, который инкапсулирует всю инфраструктуру агентов: регистрацию, запуск, обработку ошибок и повторный вызов. Конкретные агенты реализуют только метод `execute()`.
## Архитектура: AbstractAgent
Базовый класс решает все инфраструктурные задачи — конкретный агент содержит только бизнес-логику.
**ABSTRACTAGENT — БАЗОВЫЙ КЛАСС (APP\AGENTS)**
```php
<?php
declare(strict_types=1);
namespace App\Agents;
use App\Logger;
use Bitrix\Main\SystemException;
use Bitrix\Main\Type\DateTime;
use CAgent;
use DateTimeZone;
use Exception;
use Psr\Log\LoggerInterface;
use RuntimeException;
/**
* Абстрактный класс для создания агентов Битрикс24.
* Конкретный агент обязан реализовать только метод execute().
*/
abstract class AbstractAgent
{
protected const DEFAULT_PRIORITY = 100;
protected const DEFAULT_INTERVAL = 86400; // 24 часа
protected const MODULE_ID = 'main';
protected const EXEC_DATETIME_HOUR = 4;
protected const EXEC_DATETIME_MINUTE = 0;
protected const IS_PERIODICAL_AGENT = 'Y';
protected LoggerInterface $logger;
/**
* Выполнение основной логики агента.
* Должен вернуть строку следующего вызова.
*
* @throws Exception
*/
abstract public function execute(): string;
/** Интервал выполнения в секундах (можно переопределить). */
protected function getInterval(): int {
return static::DEFAULT_INTERVAL;
}
/** Приоритет агента. */
protected function getPriority(): int {
return static::DEFAULT_PRIORITY;
}
/** Модуль, к которому привязан агент. */
protected function getModuleId(): string {
return static::MODULE_ID;
}
/** Тип агента (периодический / разовый). */
protected function getType(): string {
return static::IS_PERIODICAL_AGENT;
}
/** Время первого запуска агента (по московскому времени). */
protected function getExecDateTime(): int
{
$dateTime = new DateTime();
$dateTime->setTimeZone(new DateTimeZone('Europe/Moscow'));
return $dateTime
->setTime(static::EXEC_DATETIME_HOUR, static::EXEC_DATETIME_MINUTE)
->getTimestamp();
}
/**
* Проверка предусловий.
* Переопределите в конкретном агенте при необходимости.
*
* @throws RuntimeException
*/
protected function checkConditions(): void {}
/** Логирование ошибки выполнения. */
protected function handleError(Exception $e): void
{
$this->logger = Logger::getInstance(module: $this->getModuleId());
$this->logger->error($e->getMessage(), $e->getTrace());
}
/** Строка повторного вызова агента (регистрируется в Битрикс). */
protected function createNextCall(): string
{
return static::class . '::agent();';
}
/**
* Статический точка входа агента — именно эта строка регистрируется в CAgent.
* Не содержит бизнес-логики: только запуск и перехват исключений.
*/
public static function agent(): string
{
$instance = new static();
try {
$instance->checkConditions();
return $instance->execute();
} catch (Exception $e) {
$instance->handleError($e);
return $instance->createNextCall();
}
}
/**
* Регистрация агента в системе Битрикс.
* Вызывается при установке модуля (DoInstall).
*
* @throws SystemException
*/
public static function register(): bool
{
$instance = new static();
$execTime = $instance->getExecDateTime();
static::unregister(); // удаляем дубли
$agentId = CAgent::AddAgent(
$instance->createNextCall(),
$instance->getModuleId(),
$instance->getType(),
$instance->getInterval(),
ConvertTimeStamp($execTime, 'FULL'),
'Y',
ConvertTimeStamp($execTime, 'FULL'),
$instance->getPriority(),
);
if (!$agentId) {
throw new SystemException(
'Ошибка регистрации агента: ' . static::class
);
}
return true;
}
/** Удаление агента из системы (вызывается при деинсталляции). */
public static function unregister(): void
{
$instance = new static();
CAgent::RemoveAgent($instance->createNextCall(), $instance->getModuleId());
}
/** Проверка, зарегистрирован ли агент. */
public static function isRegistered(): bool
{
$instance = new static();
$agent = CAgent::GetList(
[],
['NAME' => $instance->createNextCall(), 'MODULE_ID' => $instance->getModuleId()]
)->Fetch();
return $agent !== false;
}
}
```
---
## Пример конкретного агента
Конкретный агент наследует AbstractAgent и реализует только метод `execute()`. Вся инфраструктура — регистрация, перехват ошибок, повторный вызов — унаследована.
**ПЛОХО — ПРОЦЕДУРНЫЙ АГЕНТ, ВСЯ ЛОГИКА В ОДНОЙ ФУНКЦИИ**
```php
// agents.php в модуле
function DaSkud_ImportAgent(): string {
// 80 строк: создание клиентов, запросы к API, запись в БД...
$skudClient = new SkudV2Client(/* параметры хардкодом */);
$end = new DateTime();
$start = (clone $end)->modify('-1 day');
$res = $skudClient->getAttendance($start->format('Y-m-d'), $end->format('Y-m-d'));
foreach ($res as $record) {
\CIBlockElement::Add([/* ... */]);
}
return __FUNCTION__ . '();';
}
```
**ХОРОШО — КОНКРЕТНЫЙ АГЕНТ С МИНИМАЛЬНОЙ РЕАЛИЗАЦИЕЙ**
```php
<?php
declare(strict_types=1);
namespace Da\Skud\Agents;
use App\Agents\AbstractAgent;
use Bitrix\Main\DI\ServiceLocator;
use Da\Skud\ImportCommand;
use Da\Skud\IntegrationInvoker;
use Da\Skud\Services\BitrixService;
use Da\Skud\Services\SkudV2Service;
use Da\Skud\SkudV2Client;
use DateTime;
class SkudV2Agent extends AbstractAgent
{
// Переопределяем MODULE_ID для привязки к конкретному модулю
public const MODULE_ID = 'da.skud';
/**
* Вся бизнес-логика инкапсулирована в сервисах.
* Агент только оркестрирует вызов.
*/
public function execute(): string
{
/** @var SkudV2Client $skudClient */
$skudClient = ServiceLocator::getInstance()->get('da.skud.client');
$end = new DateTime();
$start = (clone $end)->modify('-1 day');
$skudService = new SkudV2Service(
$skudClient,
$start->format('Y-m-d'),
$end->format('Y-m-d'),
);
$bitrixService = new BitrixService($start->format('Y-m-d'));
$command = new ImportCommand($skudService, $bitrixService);
$invoker = new IntegrationInvoker();
$invoker->submit($command);
return $this->createNextCall(); // ОБЯЗАТЕЛЬНО
}
}
```
---
## Регистрация и управление агентом
**ПЛОХО — РУЧНАЯ РЕГИСТРАЦИЯ С ДУБЛИРОВАНИЕМ ПАРАМЕТРОВ**
```php
// В DoInstall() — параметры хардкодом, легко ошибиться
\CAgent::AddAgent(
'DaSkud_ImportAgent();',
'da.skud',
'Y',
86400,
'',
'Y',
\ConvertTimeStamp(time() + 86400, 'FULL'),
100
);
```
**ХОРОШО — ЧЕРЕЗ ABSTRACTAGENT::REGISTER()**
```php
// install/index.php — метод DoInstall():
use Da\Skud\Agents\SkudV2Agent;
// Все параметры определены константами в классе агента
SkudV2Agent::register();
// Проверка перед регистрацией:
if (!SkudV2Agent::isRegistered()) {
SkudV2Agent::register();
}
// install/index.php — метод DoUninstall():
SkudV2Agent::unregister();
```
---
## Обработка исключений в агентах — критическое правило
> ⚠️ **Агент НЕ должен пробрасывать исключение наружу.** Если `execute()` бросит необработанное исключение, Битрикс удалит агент из очереди и он перестанет выполняться.
AbstractAgent перехватывает все исключения в методе `agent()` и логирует их — конкретный агент должен либо обрабатывать их внутри `execute()`, либо позволить AbstractAgent сделать это.
**КАК ABSTRACTAGENT ЗАЩИЩАЕТ ОЧЕРЕДЬ АГЕНТОВ**
```php
// AbstractAgent::agent() — точка входа:
public static function agent(): string
{
$instance = new static();
try {
$instance->checkConditions();
return $instance->execute();
} catch (Exception $e) {
$instance->handleError($e);
return $instance->createNextCall(); // НЕ удаляем агент из очереди
}
}
```