Сервисный слой бота в Yii2: компонент TelegramClient, DI и обработка 429

Разработка продуктового Telegram-бота на Yii2 требует архитектурного разделения ответственности. Вызов API через разовые вызовы file_get_contents или размазанные по контроллерам cURL-запросы приводит к дублированию кода, отсутствию единой точки логирования и падению приложения при превышении лимитов Telegram Bot API (HTTP 429 Too Many Requests).

В этой статье мы построим изолированный сервисный слой: создадим компонент TelegramClient, настроим его внедрение через DI-контейнер Yii2, обеспечим безопасное хранение токенов и реализуем автоматические ретраи при ответе 429 Too Many Requests.

Вынос конфигурации и токенов в params-local.php

Токен Telegram-бота — это чувствительное знание. Его категорически нельзя хранить в репозитории VCS (Git). В фреймворке Yii2 для конфигурирования секретов под конкретную среду применяется файл config/params-local.php, который добавлен в .gitignore.

Создадим параметры конфигурации и зарегистрируем наш сервис в контейнере зависимостей (Dependency Injection Container).

// config/params-local.php
<?php

return [
'telegram.botToken' => getenv('TELEGRAM_BOT_TOKEN') ?: '1234567890:ABCdefGHIjklMNOpqrsTUVwxyz',
'telegram.apiBaseUrl' => 'https://api.telegram.org/bot',
];

Далее настроим DI-контейнер в конфигурационном файле приложения config/web.php (или config/main.php для advanced-шаблона), чтобы сервис TelegramClient внедрялся автоматически с нужными аргументами.

// config/web.php
$params = array_merge(
require __DIR__ . '/params.php',
require __DIR__ . '/params-local.php'
);

$config = [
'id' => 'app-bot',
'basePath' => dirname(__DIR__),
'container' => [
'definitions' => [
\app\components\TelegramClient::class => [
'class' => \app\components\TelegramClient::class,
'botToken' => $params['telegram.botToken'],
'baseUrl' => $params['telegram.apiBaseUrl'],
'timeout' => 10,
'maxRetries' => 3,
],
],
],
'params' => $params,
];

return $config;

Создание сервисного компонента TelegramClient с ретраями

Сервисный класс должен отвечать за выполнения HTTP-запросов к API Telegram через cURL, проверку кодов ответа, парсинг JSON, обработку ошибок сети и корректную реакцию на лимиты. Telegram отправляет HTTP status code 429 при превышении частоты запросов, возвращая параметр parameters.retry_after в секундах.

Ниже представлена полная реализация компонента TelegramClient с циклом повторных попыток при 429 ошибке и структурированным логированием через Yii::error() и Yii::warning().

<?php

namespace app\components;

use Yii;
use yii\base\Component;
use yii\base\InvalidArgumentException;

class TelegramClient extends Component
{
public string $botToken = '';
public string $baseUrl = 'https://api.telegram.org/bot';
public int $timeout = 10;
public int $maxRetries = 3;

public function init(): void
{
parent::init();
if (empty($this->botToken)) {
throw new InvalidArgumentException('Параметр botToken не может быть пустым.');
}
}

/**
* Выполнение запроса к Telegram Bot API
*
* @param string $method Имя метода (например, sendMessage, sendPhoto)
* @param array $payload Массив параметров метода
* @return array
* @throws \RuntimeException
*/
public function sendRequest(string $method, array $payload = []): array
{
$url = rtrim($this->baseUrl, '/') . $this->botToken . '/' . $method;
$attempt = 0;

while ($attempt < $this->maxRetries) {
$attempt++;
$ch = curl_init();

$jsonPayload = json_encode($payload, JSON_UNESCAPED_UNICODE);

curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $jsonPayload,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Accept: application/json',
],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => $this->timeout,
CURLOPT_CONNECTTIMEOUT => 5,
]);

$rawResponse = curl_exec($ch);
$curlErrno = curl_errno($ch);
$curlError = curl_error($ch);
$httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($curlErrno !== 0) {
Yii::error("cURL error [{$curlErrno}] при вызове {$method}: {$curlError}", 'telegram');
if ($attempt >= $this->maxRetries) {
throw new \RuntimeException("Ошибка сети Telegram API: {$curlError}");
}
sleep(1);
continue;
}

$response = json_decode($rawResponse, true);
$jsonError = json_last_error();

if ($jsonError !== JSON_ERROR_NONE) {
Yii::error("Некорректный JSON от Telegram API [{$method}]: {$rawResponse}", 'telegram');
throw new \RuntimeException("Некорректный формат ответа Telegram API.");
}

// Обработка лимита вызовов (429 Too Many Requests)
if ($httpCode === 429 || (isset($response['ok']) && $response['ok'] === false && ($response['error_code'] ?? 0) === 429)) {
$retryAfter = $response['parameters']['retry_after'] ?? 1;
Yii::warning("Telegram 429 Too Many Requests на методе {$method}. Попытка {$attempt}/{$this->maxRetries}. Пауза {$retryAfter} сек.", 'telegram');

if ($attempt < $this->maxRetries) {
sleep((int)$retryAfter);
continue;
}
}

// Обработка остальных ошибок API
if (!isset($response['ok']) || $response['ok'] !== true) {
$description = $response['description'] ?? 'Неизвестная ошибка';
$errorCode = $response['error_code'] ?? $httpCode;

Yii::error("Telegram API Error [{$errorCode}] на методе {$method}: {$description}. Payload: " . json_encode($payload, JSON_UNESCAPED_UNICODE), 'telegram');

return [
'ok' => false,
'error_code' => $errorCode,
'description' => $description,
];
}

return $response;
}

throw new \RuntimeException("Превышено максимальное число попыток запроса к Telegram API [{$method}].");
}
}

Использование TelegramClient в сервисах и контроллерах

Благодаря интеграции с DI-контейнером Yii2, компонент TelegramClient можно запрашивать непосредственно в конструкторах контроллеров, консольных команд или доменных сервисов. Это делает код легко тестируемым и независимым от глобального состояния.

Рассмотрим пример бизнес-логики регистрации заявки клиента (лида) с генерацией уникального идентификатора, сохранением в базу данных и уведомлением администратора через наш TelegramClient.

<?php

namespace app\services;

use app\components\TelegramClient;
use Yii;
use yii\db\Connection;

class LeadProcessor
{
private TelegramClient $client;
private Connection $db;

public function __construct(TelegramClient $client, Connection $db)
{
$this->client = $client;
$this->db = $db;
}

/**
* Создание лида и отправка уведомления в Telegram
*/
public function createLead(int $chatId, string $customerName, string $phone): bool
{
// Генерация безопасного ID лида (не используем сессионные ключи)
$leadId = bin2hex(random_bytes(7));

$transaction = $this->db->beginTransaction();
try {
$this->db->createCommand()->insert('{{%leads}}', [
'lead_id' => $leadId,
'chat_id' => $chatId,
'name' => $customerName,
'phone' => $phone,
'created_at' => time(),
])->execute();

$transaction->commit();
} catch (\Throwable $e) {
$transaction->rollBack();
Yii::error("Ошибка сохранения лида {$leadId}: " . $e->getMessage(), 'leads');
return false;
}

$text = "<b>Новая заявка #" . htmlspecialchars($leadId) . "</b>
";
$text .= "Имя: " . htmlspecialchars($customerName) . "
";
$text .= "Телефон: " . htmlspecialchars($phone);

$response = $this->client->sendRequest('sendMessage', [
'chat_id' => $chatId,
'text' => $text,
'parse_mode' => 'HTML',
'reply_markup' => [
'inline_keyboard' => [
[
['text' => 'Подтвердить', 'callback_data' => 'confirm_lead:' . $leadId],
]
]
]
]);

return isset($response['ok']) && $response['ok'] === true;
}
}

Лимиты Telegram Bot API и архитектурный чеклист

При построении сервисного слоя необходимо учитывать официальные лимиты и особенности инфраструктуры Telegram:

  • Лимиты отправки: Не более 30 сообщений в секунду суммарно по всем чатам и не более 1 сообщения в секунду в один конкретный чат/канал. При рассылках обязательно отправляйте задачи в очередь (например, через yii2-queue) и используйте задержки (rate-limiting).
  • Длина callback_data: Поле callback_data в inline-кнопках ограничено 64 байтами. Не передавайте в нем массив данных или длительные токены — передавайте короткий ID (например, bin2hex(random_bytes(7))) и запрашивайте данные из БД.
  • Логирование ответов: Обязательно настраивайте категорию логов telegram в config/web.php или config/console.php с отдельным target-файлом (@runtime/logs/telegram.log), чтобы ошибки вызовов API не терялись в общем потоке.
  • Идемпотентность вебхуков: Входной контроллер вебхука должен проверять заголовок X-Telegram-Bot-Api-Secret-Token и фиксировать проработанные update_id в хранилище (Redis или БД) для исключения повторной обработки одного события.

Создание специализированного компонента TelegramClient с внедрением зависимостей и обработкой ошибок делает ваш проект на Yii2 устойчивым к сбоям сети и изменению лимитов со стороны Telegram Bot API.

Если вам требуется разработка отказоустойчивой инфраструктуры Telegram-бота под ключ с интеграцией в CRM, команда BotCreator поможет реализовать решение любой сложности.

Новые статьи — в Telegram

Разбираем, что автоматизировать в бизнесе и как это работает на практике. Без спама.