Разработка продуктового 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 поможет реализовать решение любой сложности.