# 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(); // НЕ удаляем агент из очереди
    }
}
```