# Правила написания кода PHP-команды

> Битрикс24 · ORM · SOLID · DDD  
> Полное руководство — версия 2.0

**Платформа:** Битрикс24  
**PHP:** 8.1+ / 8.4  
**Версия документа:** 2.0 (исправленная и дополненная)  
**Год:** 2026

SOLID · DRY · KISS · YAGNI · ORM Bitrix · DTO · Repository · PSR-3 · PSR-12 · PHP 8.1+ · AbstractAgent

---

## Содержание

01. Именование и читаемость
02. Функции и методы
03. Классы и объекты (SOLID, DI, ServiceLocator)
04. Контроллеры
05. Работа с массивами и коллекциями
06. Условия и управляющие конструкции
07. Типизация и строгость
08. Обработка ошибок и Result Object
09. Безопасность
10. Базы данных и ORM Битрикс
11. Производительность: кеш и TaggedCache
12. Тестирование
13. Комментирование и документация
14. Общие практики (DRY, KISS, YAGNI)
15. События Bitrix (EventManager)
16. Логирование (PSR-3)
17. Агенты — AbstractAgent (полный паттерн)

---

## РАЗДЕЛ 01. Именование и читаемость

### Осмысленные имена переменных, методов, классов

Избегайте сокращений (кроме общепринятых: id, url, json), однобуквенных имён и абстрактных названий. Имя должно отражать суть сущности или действия.

**ПЛОХО**
```php
function calc($d) {
    $r = [];
    foreach ($d as $x) {
        if ($x['UF_ACTIVE']) {
            $r[] = $x['UF_NAME'];
        }
    }
    return $r;
}
```

**ХОРОШО**
```php
/**
 * Возвращает имена активных клиентов.
 * @param array $clients
 * @return string[]
 */
function getActiveClientNames(array $clients): array {
    $names = [];
    foreach ($clients as $client) {
        if ($client['UF_ACTIVE'] === 'Y') {
            $names[] = $client['UF_FULL_NAME'];
        }
    }
    return $names;
}
```

### Единый стиль именования (PSR-12 + Bitrix-соглашения)

| Сущность | Стиль | Пример |
|---|---|---|
| Классы | PascalCase | `ClientService` |
| Методы / функции | camelCase | `getActiveClients()` |
| Константы класса | UPPER_CASE | `MAX_RETRY_COUNT` |
| Свойства | camelCase | `$firstName` |
| Поля ORM Bitrix | snake_case (как в БД) | `STAGE_ID`, `UF_ACTIVE` |

Пользовательские поля `UF_*` — как есть: `UF_FULL_NAME`

---

## РАЗДЕЛ 02. Функции и методы

### Одна функция — одна ответственность (SRP)

**ПЛОХО**
```php
function processLeadAndSendNotification(array $data): void {
    // валидация, сохранение в CRM, отправка письма, логирование — всё в одном месте
}
```

**ХОРОШО**
```php
function validateLeadData(array $data): array { /* ... */ }
function createLead(array $validatedData): \Bitrix\Crm\Item { /* ... */ }
function sendLeadNotification(\Bitrix\Crm\Item $lead): void { /* ... */ }
```

### Избегайте флаговых аргументов (boolean trap)

**ПЛОХО**
```php
function addProductToDeal(int $dealId, array $productRow, bool $recalculate = true): void {
    // добавление товара
    if ($recalculate) {
        \CCrmDeal::Recalculate($dealId);
    }
}
```

**ХОРОШО — ДВА ЯВНЫХ МЕТОДА**
```php
function addProductToDeal(int $dealId, array $productRow): void {
    // только добавление товара
}

function addProductToDealAndRecalculate(int $dealId, array $productRow): void {
    $this->addProductToDeal($dealId, $productRow);
    \CCrmDeal::Recalculate($dealId);
}
```

### Количество параметров ≤ 3–4, иначе — DTO

**ПЛОХО**
```php
function updateCompany(int $id, string $title, string $phone, string $email, ?string $address, bool $isActive) { /* ... */ }
```

**ХОРОШО — READONLY DTO (PHP 8.2+)**
```php
readonly class UpdateCompanyDto {
    public function __construct(
        public string $title,
        public string $phone,
        public string $email,
        public ?string $address = null,
        public bool $isActive = true,
    ) {}
}

function updateCompany(int $id, UpdateCompanyDto $dto): void { /* ... */ }
```

### Ранний возврат (early return)

**ПЛОХО — ГЛУБОКАЯ ВЛОЖЕННОСТЬ**
```php
function getDiscountForDeal(\Bitrix\Crm\Item $deal): float {
    $discount = 0.0;
    if ($deal->getStageId() === 'EXECUTING') {
        $client = $deal->getClient();
        if ($client && $client->hasActiveSubscription()) {
            if ($deal->getOpportunity() > 100000) {
                $discount = 0.15;
            } else {
                $discount = 0.05;
            }
        }
    }
    return $discount;
}
```

**ХОРОШО — FLAT-СТРУКТУРА**
```php
function getDiscountForDeal(\Bitrix\Crm\Item $deal): float {
    if ($deal->getStageId() !== 'EXECUTING') {
        return 0.0;
    }
    $client = $deal->getClient();
    if (!$client || !$client->hasActiveSubscription()) {
        return 0.0;
    }
    return $deal->getOpportunity() > 100000 ? 0.15 : 0.05;
}
```

---

## РАЗДЕЛ 03. Классы и объекты (SOLID, DI, ServiceLocator)

### Принцип единственной ответственности (SRP)

**ПЛОХО — КЛАСС ДЕЛАЕТ ВСЁ СРАЗУ**
```php
class LeadManager {
    public function create(array $data): void { /* ... */ }
    public function sendEmail(string $message): void { /* ... */ }
    public function log(string $action): void { /* ... */ }
}
```

**ХОРОШО — РАЗДЕЛЕНИЕ ОТВЕТСТВЕННОСТЕЙ**
```php
class LeadCreationService {
    public function __construct(
        private \Bitrix\Crm\Service\Container $crmContainer,
        private LeadNotificationService $notificationService,
        private \Psr\Log\LoggerInterface $logger,
    ) {}
    
    public function createFromArray(array $data): \Bitrix\Crm\Item {
        // только логика создания лида
    }
}
```

### Предпочитайте композицию наследованию

**ПЛОХО — НАСЛЕДОВАНИЕ ОТ УСТАРЕВШЕГО КЛАССА**
```php
class CustomDealProcessor extends \CCrmDeal {
    public function process($dealId) {
        $this->Update($dealId, ['STAGE_ID' => 'WON']);
    }
}
```

**ПРАВИЛЬНО — ЧЕРЕЗ CONTAINER**
```php
class DealProcessor {
    public function __construct(
        private \Bitrix\Crm\Service\Container $container,
    ) {}
    
    public function markAsWon(int $dealId): void {
        $factory = $this->container->getFactory(\CCrmOwnerType::Deal);
        $deal = $factory->getItem($dealId);
        if ($deal === null) {
            throw new \Bitrix\Main\ObjectNotFoundException("Deal #{$dealId} not found");
        }
        $deal->setStageId('WON');
        $result = $factory->getUpdateOperation($deal)->launch();
        if (!$result->isSuccess()) {
            throw new \RuntimeException(implode(', ', $result->getErrorMessages()));
        }
    }
}
```

### ServiceLocator Bitrix для внедрения зависимостей

**ПЛОХО — РУЧНОЕ СОЗДАНИЕ ЗАВИСИМОСТЕЙ ПОВСЮДУ**
```php
// В каждом месте использования:
$logger = new FileLogger('/local/logs/app.log');
$factory = \Bitrix\Crm\Service\Container::getInstance()
    ->getFactory(\CCrmOwnerType::Lead);
$service = new LeadCreationService($factory, new LeadNotificationService(), $logger);
```

**ХОРОШО — РЕГИСТРАЦИЯ ОДИН РАЗ, ПОЛУЧЕНИЕ ВЕЗДЕ**
```php
use Bitrix\Main\DI\ServiceLocator;

// Один раз:
$serviceLocator = ServiceLocator::getInstance();
$serviceLocator->addInstanceLazy(
    LeadCreationService::class,
    static function () use ($serviceLocator): LeadCreationService {
        return new LeadCreationService(
            \Bitrix\Crm\Service\Container::getInstance(),
            $serviceLocator->get(LeadNotificationService::class),
            $serviceLocator->get(\Psr\Log\LoggerInterface::class),
        );
    }
);

// Получение в любом месте приложения:
$service = ServiceLocator::getInstance()->get(LeadCreationService::class);
```

### Избегайте статических методов для бизнес-логики

**ПЛОХО — ПРЯМОЙ ВЫЗОВ УСТАРЕВШЕГО API**
```php
$leadId = \CCrmLead::Add(['TITLE' => 'Новый лид']);
```

**ХОРОШО — ОБОРАЧИВАЕМ В СЕРВИС**
```php
class LeadService {
    public function __construct(private \Bitrix\Crm\Service\Factory $factory) {}
    
    public function create(string $title): \Bitrix\Crm\Item {
        $lead = $this->factory->createItem();
        $lead->setTitle($title);
        return $this->factory->getCreateOperation($lead)->launch()->getItem();
    }
}
```

---

## РАЗДЕЛ 04. Контроллеры

### Тонкие контроллеры — вся логика в сервисах

**ПЛОХО — ЛОГИКА ПРЯМО В КОМПОНЕНТЕ**
```php
// result_modifier.php
if ($arParams['USE_FILTER'] == 'Y') {
    $filter = ['ACTIVE' => 'Y'];
    if ($_REQUEST['search']) {
        $filter['%NAME'] = $_REQUEST['search'];
    }
    $res = \CIBlockElement::GetList([], $filter, false, false, ['ID','NAME']);
    while ($el = $res->Fetch()) {
        $arResult['ITEMS'][] = $el;
    }
}
```

**ХОРОШО — СЕРВИС С КОРРЕКТНЫМ ORM API**
```php
class NewsService {
    public function getFilteredNews(array $criteria): array {
        $query = \Bitrix\Iblock\Elements\ElementNewsTable::query()
            ->setFilter(['=ACTIVE' => 'Y'])
            ->setSelect(['ID', 'NAME']);
        
        if (!empty($criteria['search'])) {
            $query->whereLike('NAME', '%' . $criteria['search'] . '%');
        }
        return $query->exec()->fetchAll();
    }
}

// В компоненте:
$arResult['ITEMS'] = (new NewsService())->getFilteredNews([
    'search' => $_REQUEST['search'] ?? '',
]);
```

### DTO для валидации входящих данных

**ПЛОХО — СЫРАЯ ВАЛИДАЦИЯ В КОНТРОЛЛЕРЕ**
```php
$title = $_POST['TITLE'] ?? '';
if (strlen($title) < 3) {
    $error = 'Заголовок слишком короткий';
} else {
    \CCrmLead::Add(['TITLE' => $title]);
}
```

**ХОРОШО — DTO + КОНТРОЛЛЕР**
```php
readonly class CreateLeadDto {
    public function __construct(
        public string $title,
        public ?string $phone = null,
        public ?string $email = null,
    ) {}
    
    /** @throws \InvalidArgumentException */
    public static function fromArray(array $data): self {
        if (empty($data['TITLE']) || mb_strlen($data['TITLE']) < 3) {
            throw new \InvalidArgumentException('Заголовок — не менее 3 символов');
        }
        return new self(
            title: trim($data['TITLE']),
            phone: $data['PHONE'] ?? null,
            email: $data['EMAIL'] ?? null,
        );
    }
}

class LeadController {
    public function __construct(private LeadCreationService $leadService) {}
    
    public function createAction(\Bitrix\Main\Request $request): \Bitrix\Main\Response {
        try {
            $dto = CreateLeadDto::fromArray($request->getPostList()->toArray());
            $lead = $this->leadService->create($dto);
            return new \Bitrix\Main\Response(
                json_encode(['id' => $lead->getId()]),
                'application/json'
            );
        } catch (\InvalidArgumentException $e) {
            return new \Bitrix\Main\Response(
                json_encode(['error' => $e->getMessage()]),
                'application/json',
                400
            );
        }
    }
}
```

---

## РАЗДЕЛ 05. Работа с массивами и коллекциями

### Встроенные функции вместо foreach-циклов

**ПЛОХО**
```php
$activeClientIds = [];
foreach ($clients as $client) {
    if ($client['UF_ACTIVE'] === 'Y') {
        $activeClientIds[] = $client['ID'];
    }
}
```

**ХОРОШО**
```php
$activeClients = array_filter($clients, fn($c) => $c['UF_ACTIVE'] === 'Y');
$activeClientIds = array_column($activeClients, 'ID');
```

### Вложенные массивы — разворачивание через array_merge / array_column

**ПЛОХО — ВЛОЖЕННЫЕ ЦИКЛЫ**
```php
$allPhones = [];
foreach ($contacts as $contact) {
    foreach ($contact['PHONE'] as $phone) {
        $allPhones[] = $phone['VALUE'];
    }
}
```

**ХОРОШО**
```php
$allPhones = array_merge(...array_column($contacts, 'PHONE'));
$phoneNumbers = array_column($allPhones, 'VALUE');
```

### Коллекции объектов для сложных структур

**ПЛОХО — РУЧНОЙ ЦИКЛ ДЛЯ АГРЕГАЦИИ**
```php
$sum = 0;
foreach ($dealProducts as $row) {
    $sum += $row['PRICE'] * $row['QUANTITY'];
}
```

**ХОРОШО — ILLUMINATE/COLLECTIONS**
```php
use Illuminate\Support\Collection;

$totalSum = Collection::make($dealProducts)
    ->sum(fn($row) => $row['PRICE'] * $row['QUANTITY']);
```

---

## РАЗДЕЛ 06. Условия и управляющие конструкции

### Длинные условия — выносите в методы

**ПЛОХО — НЕЧИТАЕМОЕ УСЛОВИЕ В IF**
```php
if ($deal->getStageId() === 'EXECUTING' && $deal->getOpportunity() > 10000 && $deal->getAssignedById() === $currentUserId && !$deal->getClosed()) {
    // разрешить действие
}
```

**ХОРОШО — ИНКАПСУЛИРОВАННОЕ УСЛОВИЕ**
```php
private function canCurrentUserEditDeal(\Bitrix\Crm\Item $deal): bool {
    return $deal->getStageId() === 'EXECUTING'
        && $deal->getOpportunity() > 10000
        && $deal->getAssignedById() === \CCrmSecurityHelper::GetCurrentUserID()
        && !$deal->getClosed();
}

if ($this->canCurrentUserEditDeal($deal)) {
    // разрешить действие
}
```

### Именованные константы и Enum вместо магических значений

**ПЛОХО — МАГИЧЕСКИЕ ЧИСЛА И СТРОКИ**
```php
if ($lead->getStatusId() === '1') { // что за статус?
    $action = 'call';
}
if ($product['QUANTITY'] < 10) { /* неясно откуда 10 */ }
```

**ХОРОШО — PHP 8.1+ ENUM**
```php
enum LeadStatus: string {
    case NEW = '1';
    case IN_PROCESS = '2';
    case CONVERTED = '3';
}

const LOW_STOCK_THRESHOLD = 10;

if ($lead->getStatusId() === LeadStatus::NEW->value) {
    $action = 'call';
}
if ($product['QUANTITY'] < LOW_STOCK_THRESHOLD) { /* ... */ }
```

### Полиморфизм вместо switch / if-elseif

**ПЛОХО — РАЗРАСТАЮЩИЙСЯ SWITCH**
```php
class ActivityHandler {
    public function process($activityType, $data) {
        if ($activityType === 'CALL') {
            // логика звонка
        } elseif ($activityType === 'EMAIL') {
            // логика письма
        } elseif ($activityType === 'MEETING') {
            // логика встречи — и так далее...
        } else {
            throw new \Exception('Unknown type');
        }
    }
}
```

**ХОРОШО — ПАТТЕРН STRATEGY + REGISTRY**
```php
interface ActivityProcessor {
    public function process(array $data): void;
}

class CallProcessor implements ActivityProcessor { /* ... */ }
class EmailProcessor implements ActivityProcessor { /* ... */ }

class ActivityProcessorRegistry {
    private array $processors = [];
    
    public function register(string $type, ActivityProcessor $p): void {
        $this->processors[$type] = $p;
    }
    
    public function get(string $type): ActivityProcessor {
        return $this->processors[$type]
            ?? throw new \InvalidArgumentException("Unknown activity type: {$type}");
    }
}
```

### match вместо if-elseif (PHP 8+)

**ПЛОХО**
```php
if ($stage === 'NEW') {
    $label = 'Новый';
} elseif ($stage === 'IN_PROGRESS') {
    $label = 'В работе';
} else {
    $label = 'Завершён';
}
```

**ХОРОШО — MATCH СО СТРОГИМ СРАВНЕНИЕМ**
```php
$label = match ($stage) {
    'NEW' => 'Новый',
    'IN_PROGRESS' => 'В работе',
    default => 'Завершён',
};
```

---

## РАЗДЕЛ 07. Типизация и строгость

### declare(strict_types=1) — обязательно в каждом файле

**ПЛОХО — НЕЯВНОЕ ПРИВЕДЕНИЕ ТИПОВ**
```php
<?php
// нет strict_types — PHP молча приведёт "5" к int
function add(int $a, int $b): int {
    return $a + $b;
}
echo add("5", "10"); // 15 — ошибка замаскирована
```

**ХОРОШО — ЯВНЫЙ TYPEERROR ПРИ НЕВЕРНЫХ ТИПАХ**
```php
<?php
declare(strict_types=1);

function add(int $a, int $b): int {
    return $a + $b;
}
echo add("5", "10"); // TypeError — сразу видим проблему
```

### Типизация свойств, аргументов и возвращаемых значений

**ПЛОХО — PHPDOC ВМЕСТО НАТИВНОЙ ТИПИЗАЦИИ**
```php
class ClientInfo {
    /** @var string|null */
    private $name;
    
    public function setName($name) {
        $this->name = $name;
    }
    
    public function getName() {
        return $this->name;
    }
}
```

**ХОРОШО — НАТИВНАЯ ТИПИЗАЦИЯ PHP 8**
```php
class ClientInfo {
    private ?string $name = null;
    
    public function setName(string $name): void {
        $this->name = $name;
    }
    
    public function getName(): ?string {
        return $this->name;
    }
}
```

### readonly классы для DTO (PHP 8.2+)

**ПЛОХО — ИЗМЕНЯЕМЫЙ МАССИВ ВМЕСТО DTO**
```php
function updateClient(int $id, array $data): void {
    $client = $this->getById($id);
    $client['NAME'] = $data['NAME'] ?? $client['NAME'];
    // легко ошибиться с ключом, нет типобезопасности
}
```

**ХОРОШО — НЕИЗМЕНЯЕМЫЙ READONLY DTO**
```php
readonly class UpdateClientDto {
    public function __construct(
        public string $name,
        public ?string $phone = null,
        public ?string $email = null,
    ) {}
}

function updateClient(int $id, UpdateClientDto $dto): void {
    $client = $this->getById($id);
    $client->setName($dto->name);
}
```

---

## РАЗДЕЛ 08. Обработка ошибок и Result Object

### Исключения вместо возврата false/null

**ПЛОХО — СТАРЫЙ ПОДХОД BITRIX С ГЛОБАЛЬНЫМ СОСТОЯНИЕМ**
```php
$leadId = \CCrmLead::Add(['TITLE' => '']);
if (!$leadId) {
    $error = \CCrmLead::GetLastError(); // глобальное состояние
}
```

**ХОРОШО — ДОМЕННЫЕ ИСКЛЮЧЕНИЯ**
```php
class LeadCreationException extends \RuntimeException {}

try {
    $lead = $this->leadService->createFromDto($dto);
} catch (LeadCreationException $e) {
    $this->logger->error($e->getMessage());
}
```

### Иерархия доменных исключений

**ПЛОХО — БАЗОВЫЙ \EXCEPTION БЕЗ КОНТЕКСТА**
```php
throw new \Exception('Неверный email');
// caller не знает, какое именно исключение ловить
```

**ХОРОШО — ИЕРАРХИЯ ДОМЕННЫХ ИСКЛЮЧЕНИЙ**
```php
class MyModuleException extends \RuntimeException {}
class InvalidEmailException extends \DomainException {}
class LeadNotFoundException extends \Bitrix\Main\ObjectNotFoundException {}
class LeadValidationException extends MyModuleException {}

throw new InvalidEmailException('Email имеет неверный формат: ' . $email);
```

### Нативный Result Object Bitrix

Result Object — нативный паттерн Bitrix для операций уровня сервиса. Вы можете использовать его там, где нужно вернуть данные или набор ошибок без исключений.

**ПЛОХО — ВОЗВРАТ NULL ПРИ ОШИБКЕ, НЕТ ДЕТАЛЕЙ**
```php
public function create(CreateLeadDto $dto): ?int {
    $lead = $factory->createItem();
    $lead->setTitle($dto->title);
    $result = $factory->getCreateOperation($lead)->launch();
    if (!$result->isSuccess()) {
        return null; // caller не знает, что именно пошло не так
    }
    return $lead->getId();
}
```

**ХОРОШО — RESULT С ДЕТАЛЯМИ ОШИБОК**
```php
public function create(CreateLeadDto $dto): \Bitrix\Main\Result {
    $result = new \Bitrix\Main\Result();
    try {
        $factory = \Bitrix\Crm\Service\Container::getInstance()
            ->getFactory(\CCrmOwnerType::Lead);
        $lead = $factory->createItem();
        $lead->setTitle($dto->title);
        $opResult = $factory->getCreateOperation($lead)->launch();
        if (!$opResult->isSuccess()) {
            return $result->addErrors($opResult->getErrors());
        }
        $result->setData(['id' => $lead->getId()]);
    } catch (\Throwable $e) {
        $result->addError(new \Bitrix\Main\Error($e->getMessage(), $e->getCode()));
    }
    return $result;
}

// Использование:
$result = $leadService->create($dto);
if ($result->isSuccess()) {
    $leadId = $result->getData()['id'];
} else {
    foreach ($result->getErrors() as $error) {
        $logger->error($error->getMessage());
    }
}
```

---

## РАЗДЕЛ 09. Безопасность

### ORM вместо raw SQL — никаких переменных в строке запроса

**ПЛОХО — SQL-ИНЪЕКЦИЯ ДАЖЕ С FORSQL**
```php
global $DB;
$email = $_GET['email'];
$sql = "SELECT * FROM b_user WHERE EMAIL = '" . $DB->ForSql($email) . "'";
$res = $DB->Query($sql);
```

**ХОРОШО — ORM С ПАРАМЕТРИЗОВАННЫМ ЗАПРОСОМ**
```php
use Bitrix\Main\UserTable;

$email = $_GET['email'] ?? '';
$user = UserTable::query()
    ->setSelect(['ID', 'NAME', 'LAST_NAME'])
    ->where('EMAIL', $email) // параметр передаётся безопасно
    ->setLimit(1)
    ->exec()
    ->fetch();
```

### Экранирование вывода в HTML

**ПЛОХО — XSS УЯЗВИМОСТЬ**
```php
echo "Привет, " . $arUser['NAME'];
```

**ХОРОШО — HTMLFILTER::ENCODE()**
```php
use Bitrix\Main\Text\HtmlFilter;

echo "Привет, " . HtmlFilter::encode($arUser['NAME']);
```

### Валидация входящих данных

`FILTER_SANITIZE_SPECIAL_CHARS` и `FILTER_SANITIZE_STRING` удалены в PHP 8.1–8.2. Используйте явную санитизацию.

**ПЛОХО — УДАЛЁННЫЕ КОНСТАНТЫ PHP**
```php
// FILTER_SANITIZE_SPECIAL_CHARS удалён в PHP 8.2!
$phone = filter_input(INPUT_POST, 'PHONE', FILTER_SANITIZE_SPECIAL_CHARS);
```

**ХОРОШО — ЯВНАЯ САНИТИЗАЦИЯ ДЛЯ PHP 8.2+**
```php
$phone = trim((string)($_POST['PHONE'] ?? ''));
$phone = strip_tags($phone);
if (!preg_match('/^\+?\d{10,15}$/', $phone)) {
    throw new \InvalidArgumentException('Некорректный номер телефона');
}

// Для email — нативный PHP-фильтр (не удалён):
$email = filter_input(INPUT_POST, 'EMAIL', FILTER_VALIDATE_EMAIL);
if ($email === false || $email === null) {
    throw new \InvalidArgumentException('Некорректный email');
}
```

---

## РАЗДЕЛ 10. Базы данных и ORM Битрикс

### Репозиторий инкапсулирует работу с данными

**ПЛОХО — ORM-ЗАПРОСЫ РАЗБРОСАНЫ ПО СЕРВИСАМ**
```php
class DealReportService {
    public function getWonDealsSum(): float {
        // прямой вызов ORM из сервиса без абстракции
        $result = \Bitrix\Crm\DealTable::getList([
            'filter' => ['=STAGE_SEMANTIC_ID' => 'S'],
            'select' => ['OPPORTUNITY_SUM'],
            'runtime' => [
                new \Bitrix\Main\Entity\ExpressionField(
                    'OPPORTUNITY_SUM',
                    'SUM(%s)',
                    'OPPORTUNITY'
                )
            ]
        ])->fetch();
        return (float)($result['OPPORTUNITY_SUM'] ?? 0);
    }
}
```

**ХОРОШО — ПАТТЕРН REPOSITORY**
```php
interface DealRepository {
    public function getTotalWonOpportunity(): float;
}

class OrmDealRepository implements DealRepository {
    public function getTotalWonOpportunity(): float {
        $result = \Bitrix\Crm\DealTable::query()
            ->addSelect(
                new \Bitrix\Main\Entity\ExpressionField(
                    'SUM_OPP',
                    'SUM(%s)',
                    'OPPORTUNITY'
                )
            )
            ->where('STAGE_SEMANTIC_ID', 'S')
            ->exec()->fetch();
        return (float)($result['SUM_OPP'] ?? 0);
    }
}

class DealReportService {
    public function __construct(private DealRepository $dealRepo) {}
    
    public function getWonDealsSum(): float {
        return $this->dealRepo->getTotalWonOpportunity();
    }
}
```

### Транзакции для согласованности данных

**ПЛОХО — НЕТ ТРАНЗАКЦИИ, ДАННЫЕ МОГУТ РАССОГЛАСОВАТЬСЯ**
```php
\Bitrix\Crm\DealTable::update($dealId, ['STAGE_ID' => 'WON']);
\Bitrix\Crm\InvoiceTable::add([/* ... */]); // если упадёт — сделка уже обновлена
```

**ХОРОШО — АТОМАРНОЕ ОБНОВЛЕНИЕ С ROLLBACK**
```php
$connection = \Bitrix\Main\Application::getConnection();
$connection->startTransaction();
try {
    \Bitrix\Crm\DealTable::update($dealId, ['STAGE_ID' => 'WON']);
    \Bitrix\Crm\InvoiceTable::add([/* ... */]);
    $connection->commitTransaction();
} catch (\Exception $e) {
    $connection->rollbackTransaction();
    throw $e;
}
```

### Решение проблемы N+1 через JOIN в ORM

**ПЛОХО — N+1 ЗАПРОСОВ**
```php
$deals = \Bitrix\Crm\DealTable::getList(['select' => ['ID','CONTACT_ID']])->fetchAll();
foreach ($deals as $deal) {
    // N дополнительных запросов!
    $contact = \Bitrix\Crm\ContactTable::getByPrimary($deal['CONTACT_ID'])->fetch();
}
```

**ХОРОШО — ПРЕДЗАГРУЗКА ЧЕРЕЗ REGISTERRUNTIMEFIELD**
```php
$deals = \Bitrix\Crm\DealTable::query()
    ->setSelect(['ID', 'TITLE', 'CONTACT.NAME', 'CONTACT.LAST_NAME'])
    ->registerRuntimeField(
        new \Bitrix\Main\Entity\ReferenceField(
            'CONTACT',
            \Bitrix\Crm\ContactTable::class,
            \Bitrix\Main\ORM\Query\Join::on('this.CONTACT_ID', 'ref.ID'),
        )
    )
    ->where('STAGE_ID', 'EXECUTING')
    ->setOrder(['DATE_CREATE' => 'DESC'])
    ->exec()->fetchCollection();

foreach ($deals as $deal) {
    $name = $deal->get('CONTACT.NAME'); // уже загружен, доп. запросов нет
}
```

### fetchCollection() и работа с объектами

**ПЛОХО — НЕСУЩЕСТВУЮЩИЙ МЕТОД КОЛЛЕКЦИИ**
```php
$leadCollection = \Bitrix\Crm\LeadTable::getList(['select' => ['*']])->fetchCollection();
$titles = $leadCollection->getTitleList(); // метод не существует!
```

**ХОРОШО — ГЕТТЕРЫ ЧЕРЕЗ GETALL()**
```php
$leadCollection = \Bitrix\Crm\LeadTable::query()
    ->setSelect(['ID', 'TITLE'])
    ->exec()->fetchCollection();

$titles = array_map(
    fn($lead) => $lead->getTitle(),
    $leadCollection->getAll(),
);
```

---

## РАЗДЕЛ 11. Производительность: кеш и TaggedCache

### Кеширование через Bitrix\Main\Data\Cache

**ПЛОХО — ДОРОГОЙ ЗАПРОС БЕЗ КЕША**
```php
// Выполняется на каждый запрос страницы
$elements = \CIBlockElement::GetList(
    [], ['IBLOCK_ID' => $iblockId], false, false,
    ['ID','NAME']
)->FetchAll();
```

**ХОРОШО — РЕЗУЛЬТАТ КЕШИРУЕТСЯ**
```php
$cache = \Bitrix\Main\Data\Cache::createInstance();
$cacheId = 'iblock_elements_' . $iblockId;

if ($cache->initCache(3600, $cacheId, '/my_module/')) {
    $elements = $cache->getVars();
} elseif ($cache->startDataCache()) {
    $elements = \Bitrix\Iblock\Elements\ElementNewsTable::getList([
        'filter' => ['IBLOCK_ID' => $iblockId],
    ])->fetchAll();
    $cache->endDataCache($elements);
}
```

### Тегированный кеш (TaggedCache)

TaggedCache позволяет инвалидировать кеш при изменении конкретных сущностей. Предпочтительный подход для данных CRM и Iblock.

**ПЛОХО — СБРОС ВСЕГО КЕША ПРИ ИЗМЕНЕНИИ ОДНОГО ЛИДА**
```php
// При любом изменении сбрасываем весь кеш модуля
\Bitrix\Main\Data\Cache::createInstance()->cleanDir('/crm/leads/');
```

**ХОРОШО — ТОЧЕЧНАЯ ИНВАЛИДАЦИЯ ПО ТЕГУ**
```php
$taggedCache = \Bitrix\Main\Application::getInstance()->getTaggedCache();
$cache = \Bitrix\Main\Data\Cache::createInstance();

if ($cache->initCache(3600, 'lead_' . $leadId, '/crm/leads/')) {
    $data = $cache->getVars();
} elseif ($cache->startDataCache()) {
    $taggedCache->startTagCache('/crm/leads/');
    $taggedCache->registerTag('crm_lead_' . $leadId);
    $taggedCache->registerTag('crm_leads_list');
    $data = $this->leadRepository->getById($leadId);
    $taggedCache->endTagCache();
    $cache->endDataCache($data);
}

// При изменении лида — только его кеш:
$taggedCache->clearByTag('crm_lead_' . $leadId);

// При массовом изменении:
$taggedCache->clearByTag('crm_leads_list');
```

### Выбирайте только нужные поля

**ПЛОХО — SELECT \* БЕЗ НЕОБХОДИМОСТИ**
```php
$leads = \Bitrix\Crm\LeadTable::getList(['select' => ['*']])->fetchAll();
```

**ХОРОШО — ТОЛЬКО НЕОБХОДИМЫЕ ПОЛЯ**
```php
$leads = \Bitrix\Crm\LeadTable::query()
    ->setSelect(['ID', 'TITLE', 'STATUS_ID', 'DATE_CREATE'])
    ->setLimit(50)
    ->exec()->fetchAll();
```

---

## РАЗДЕЛ 12. Тестирование

### Код должен быть тестируемым — изолируйте зависимости

**ПЛОХО — ПРЯМОЙ ВЫЗОВ API, НЕЛЬЗЯ ПРОТЕСТИРОВАТЬ**
```php
class LeadService {
    public function create(array $data): int {
        return \CCrmLead::Add($data); // нельзя замокать
    }
}
```

**ХОРОШО — РЕПОЗИТОРИЙ ЧЕРЕЗ ИНТЕРФЕЙС**
```php
interface LeadRepository {
    public function save(Lead $lead): void;
}

class LeadService {
    public function __construct(private LeadRepository $repo) {}
    
    public function create(LeadDto $dto): Lead {
        $lead = new Lead($dto);
        $this->repo->save($lead);
        return $lead;
    }
}

// PHPUnit тест:
class LeadServiceTest extends \PHPUnit\Framework\TestCase {
    public function testCreateLead(): void {
        $mockRepo = $this->createMock(LeadRepository::class);
        $mockRepo->expects($this->once())->method('save');
        $service = new LeadService($mockRepo);
        $lead = $service->create(new LeadDto(title: 'Test Lead'));
        $this->assertSame('Test Lead', $lead->getTitle());
    }
}
```

### Data Providers для параметризованных тестов

**ПЛОХО — ДУБЛИРОВАНИЕ ТЕСТОВЫХ МЕТОДОВ**
```php
public function testSmallOrderDiscount(): void {
    $this->assertSame(0.0, (new DiscountCalculator())->calculate(5000.0));
}

public function testMediumOrderDiscount(): void {
    $this->assertSame(0.05, (new DiscountCalculator())->calculate(50000.0));
}
// и так далее...
```

**ХОРОШО — @DATAPROVIDER**
```php
class DiscountCalculatorTest extends \PHPUnit\Framework\TestCase {
    /** @dataProvider discountProvider */
    public function testCalculate(float $amount, float $expected): void {
        $this->assertSame($expected, (new DiscountCalculator())->calculate($amount));
    }
    
    public static function discountProvider(): array {
        return [
            'small order' => [5000.0, 0.0],
            'medium order' => [50000.0, 0.05],
            'large order' => [150000.0, 0.15],
        ];
    }
}
```

---

## РАЗДЕЛ 13. Комментирование и документация

### Код должен быть самодокументированным

Комментарии объясняют *«почему»*, а не *«что»*. Используйте выразительные имена.

**ПЛОХО — КОММЕНТАРИЙ ОПИСЫВАЕТ ОЧЕВИДНОЕ**
```php
// умножаем цену на количество
$sum = $price * $qty;
```

**ХОРОШО — ИМЯ ГОВОРИТ САМО ЗА СЕБЯ, КОММЕНТАРИЙ ОБЪЯСНЯЕТ БИЗНЕС-ПРАВИЛО**
```php
$totalPrice = $product->getPrice() * $product->getQuantity();
// Скидка для VIP-клиентов введена с Q1 2024 (см. задачу PROJ-1234)
if ($client->isVip()) {
    $totalPrice *= (1 - self::VIP_DISCOUNT_RATE);
}
```

### PHPDoc для публичных API

**ПЛОХО — НЕТ ДОКУМЕНТАЦИИ, НЕЯСНЫ ТИПЫ И ИСКЛЮЧЕНИЯ**
```php
public function find($id) {
    // ...
}
```

**ХОРОШО — ПОЛНЫЙ PHPDOC**
```php
/**
 * Находит сделку по идентификатору.
 *
 * @param positive-int $id Идентификатор сделки в CRM
 * @return \Bitrix\Crm\Item
 *
 * @throws \Bitrix\Main\ObjectNotFoundException если сделка не найдена
 * @throws \Bitrix\Main\SystemException при ошибке базы данных
 */
public function find(int $id): \Bitrix\Crm\Item { /* ... */ }
```

---

## РАЗДЕЛ 14. Общие практики (DRY, KISS, YAGNI, Закон Деметры)

### DRY — не дублируйте знания

**ПЛОХО — ОДНА И ТА ЖЕ ЛОГИКА В ДВУХ МЕСТАХ**
```php
// В компоненте A:
$lead = \Bitrix\Crm\LeadTable::getByPrimary($id)->fetchObject();
if (!$lead) {
    throw new \Exception('Лид не найден');
}

// В компоненте B:
$lead = \Bitrix\Crm\LeadTable::getByPrimary($id)->fetchObject();
if (!$lead) {
    throw new \Exception('Лид не найден');
}
```

**ХОРОШО — ОБЩАЯ ЛОГИКА В LEADPROVIDER**
```php
class LeadProvider {
    public function getById(int $id): \Bitrix\Crm\Item {
        $lead = \Bitrix\Crm\LeadTable::getByPrimary($id)->fetchObject();
        if (!$lead) {
            throw new \Bitrix\Main\ObjectNotFoundException("Лид #{$id} не найден");
        }
        return $lead;
    }
}
```

### Закон Деметры — не разговаривай с незнакомцами

**ПЛОХО — ЦЕПОЧКА ЧЕРЕЗ ГРАНИЦЫ ОБЪЕКТОВ**
```php
$city = $deal->getContact()->getCompany()->getAddress()->getCity();
```

**ХОРОШО — ДЕЛЕГИРУЮЩИЙ МЕТОД В DEAL**
```php
// Вызов:
$city = $deal->getContactCompanyCity();

// Реализация внутри класса Deal:
public function getContactCompanyCity(): ?string {
    return $this->getContact()?->getCompany()?->getCity();
}
```

### KISS — простое решение лучше сложного

Не усложняйте без необходимости. Если задачу можно решить тремя строками — не пишите тридцать.

### YAGNI — не добавляйте функциональность «на будущее»

Реализуйте только то, что требуется прямо сейчас. Код, написанный «на перспективу», часто не используется и засоряет проект.

---

## РАЗДЕЛ 15. События Bitrix (EventManager)

События — ключевой механизм расширения Битрикс без изменения ядра. Тонкие подписчики и делегирование в сервис — обязательный стандарт.

### Тонкий обработчик — логика в сервисе

**ПЛОХО — ВСЯ ЛОГИКА В ТЕЛЕ ОБРАБОТЧИКА**
```php
AddEventHandler('crm', 'OnAfterCrmLeadAdd', function(array &$fields): void {
    // 50+ строк: отправка писем, запись в лог, вызов внешних API...
    $mailer = new \PHPMailer\PHPMailer\PHPMailer();
    $mailer->setFrom(['no-reply@example.com']);
    $mailer->addAddress($fields['EMAIL']);
    // ...
});
```

**ХОРОШО — ПОДПИСЧИК ДЕЛЕГИРУЕТ В СЕРВИС**
```php
class LeadEventSubscriber {
    /** Обработчик намеренно тонкий — вся логика в сервисе. */
    public static function onAfterLeadAdd(array &$fields): void {
        \Bitrix\Main\DI\ServiceLocator::getInstance()
            ->get(LeadNotificationService::class)
            ->notifyOnLeadCreated((int)$fields['ID']);
    }
}

// Регистрация в init.php:
\Bitrix\Main\EventManager::getInstance()->addEventHandler(
    'crm',
    'OnAfterCrmLeadAdd',
    [LeadEventSubscriber::class, 'onAfterLeadAdd'],
);
```

### Современный API событий (Bitrix\Main\Event)

**ОБЪЕКТНО-ОРИЕНТИРОВАННЫЙ EVENT API**
```php
\Bitrix\Main\EventManager::getInstance()->addEventHandlerCompatible(
    'crm',
    '\Bitrix\Crm\Service\Operation\Add::onAfterRun',
    static function (\Bitrix\Main\Event $event): \Bitrix\Main\EventResult {
        $lead = $event->getParameter('ITEM');
        if ($lead instanceof \Bitrix\Crm\Item\Lead) {
            \Bitrix\Main\DI\ServiceLocator::getInstance()
                ->get(LeadNotificationService::class)
                ->notifyOnLeadCreated($lead->getId());
        }
        return new \Bitrix\Main\EventResult(\Bitrix\Main\EventResult::SUCCESS);
    }
);
```

---

## РАЗДЕЛ 16. Логирование (PSR-3)

Инжектируйте PSR-3 LoggerInterface. Глобальные функции `AddMessage2Log()` — устаревший подход без уровней и контекста.

### PSR-3 логирование через Bitrix\Main\Diag\Logger

**ПЛОХО — УСТАРЕВШИЕ ФУНКЦИИ БЕЗ УРОВНЕЙ**
```php
AddMessage2Log('Ошибка создания лида: ' . $message, 'my_module');
// нет уровней severity, нет структурированного контекста, сложно искать
```

**ХОРОШО — PSR-3 С УРОВНЯМИ И КОНТЕКСТОМ**
```php
$logger->info('Lead created', ['leadId' => $lead->getId(), 'userId' => \CUser::GetID()]);

$logger->error('Lead creation failed', [
    'message' => $e->getMessage(),
    'code' => $e->getCode(),
    'trace' => $e->getTraceAsString(),
]);
```

### Инжектируйте LoggerInterface, а не конкретную реализацию

**ПЛОХО — КОНКРЕТНАЯ РЕАЛИЗАЦИЯ В КОНСТРУКТОРЕ**
```php
class LeadCreationService {
    private \Bitrix\Main\Diag\Logger $logger;
    
    public function __construct() {
        // нельзя подменить в тестах, привязка к реализации
        $this->logger = \Bitrix\Main\Diag\Logger::create('crm', '/local/logs/crm.log');
    }
}
```

**ХОРОШО — PSR-3 ИНТЕРФЕЙС ЧЕРЕЗ DI**
```php
use Psr\Log\LoggerInterface;

class LeadCreationService {
    public function __construct(
        private \Bitrix\Crm\Service\Factory $factory,
        private LoggerInterface $logger,
    ) {}
    
    public function create(CreateLeadDto $dto): \Bitrix\Main\Result {
        $this->logger->debug('Creating lead', ['title' => $dto->title]);
        // ...
    }
}

// Регистрация в ServiceLocator:
ServiceLocator::getInstance()->addInstanceLazy(
    LoggerInterface::class,
    fn() => \Bitrix\Main\Diag\Logger::create(
        'app',
        '/local/logs/app_' . date('Y-m-d') . '.log'
    )
);
```

---

## РАЗДЕЛ 17. Агенты — AbstractAgent (полный паттерн)

В проекте используется абстрактный базовый класс **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(); // НЕ удаляем агент из очереди
    }
}
```