При разработке коммерческих Telegram-ботов на фреймворке Yii2 частой ошибкой становится прямая вызов функций отправки сообщений из бизнес-логики или прямо из контроллера Webhook. Это приводит к дублированию кода, трудностям при тестировании и полной незащищенности приложения от лимитов Telegram Bot API (ошибка HTTP 429 Too Many Requests).
Грамотная архитектура требует выделения отдельного сервисного слоя. В этой статье мы создадим изолированный компонент TelegramClient, настроим его внедрение через Dependency Injection (DI) контейнер Yii2, обработаем ошибочные статусы API и реализуем безопасную работу с токенами.
Зачем нужен сервисный слой TelegramClient в Yii2
Использование сторонних библиотек не всегда оправдано: они часто перегружены лишними абстракциями, содержат устаревшие зависимости или не позволяют точечно управлять HTTP-запросами. Нативный компонент, написанный на cURL внутри Yii2, решает сразу несколько задач:
- Изоляция конфигурации: секретный токен бота хранятся в
params-local.phpи не попадают в систему контроля версий (Git). - Централизованная обработка ошибок: все случаи сбоев сети, некорректного JSON или статусов
ok: falseобрабатываются в одном месте и записываются в лог-файлы Yii2. - Контроль лимитов (Rate Limiting): автоматическая пауза при получении HTTP-кода 429 на основе заголовка
retry_after. - Удобство тестирования: объект клиента подменяется моком через DI-контейнер
Yii::$container.
Конфигурация Yii2: вынос токенов в params-local.php
Никогда не храните токены в коде контроллеров или основном web.php. Создайте параметры в файле config/params-local.php, который добавлен в .gitignore.
<?php
return [
'telegram' => [
'botToken' => getenv('TELEGRAM_BOT_TOKEN') ?: '123456789:AAXxxXxxXxxXxxXxxXxxXxxXxxXxxXxxXxx',
'secretToken' => getenv('TELEGRAM_SECRET_TOKEN') ?: 'c8a3f890e72b4d1198e1f02a3a5d8b92',
],
];
Далее зарегистрируем компонент в сервисы DI-контейнера. Это делается в конфигурационном файле приложения (config/web.php или config/main.php) в секции container:
<?php
$config = [
'id' => 'basic',
'basePath' => dirname(__DIR__),
'container' => [
'singletons' => [
\app\components\TelegramClient::class => function ($container) {
$params = Yii::$app->params['telegram'] ?? [];
if (empty($params['botToken'])) {
throw new \invalidConfigException('Telegram botToken is not configured in params-local.php');
}
return new \app\components\TelegramClient($params['botToken']);
},
],
],
// ... остальная конфигурация
];
return $config;
Реализация компонента TelegramClient c ретраями 429
Создадим класс app\components\TelegramClient. Клиент будет выполнять отправку через cURL (использование file_get_contents недопустимо из-за отсутствия гибкой настройки таймаутов и HTTP-заголовков). В случае ответа с кодом 429 компонент считывает параметр retry_after и делает повторную попытку.
<?php
namespace app\components;
use Yii;
use yii\base\Component;
use yii\base\InvalidConfigException;
class TelegramClient extends Component
{
private string $botToken;
private int $maxRetries = 3;
private int $timeout = 10;
public function __construct(string $botToken, array $config = [])
{
$this->botToken = $botToken;
parent::__construct($config);
}
/**
* Выполнение запроса к Telegram Bot API
*
* @param string $method Метод API (например, sendMessage)
* @param array $params Параметры метода
* @return array Ответ от Telegram API
* @throws \Exception
*/
public function request(string $method, array $params = []): array
{
$url = "https://api.telegram.org/bot{$this->botToken}/{$method}";
$attempt = 0;
while ($attempt < $this->maxRetries) {
$attempt++;
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($params),
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => $this->timeout,
CURLOPT_CONNECTTIMEOUT => 5,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);
if ($response === false) {
Yii::error("cURL Error on method {$method}: {$curlError}", 'telegram');
throw new \RuntimeException("cURL transport error: {$curlError}");
}
$data = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
Yii::error("Invalid JSON from Telegram API [{$method}]: " . json_last_error_msg(), 'telegram');
throw new \RuntimeException("Failed to parse Telegram response JSON");
}
// Обработка лимита запросов HTTP 429
if ($httpCode === 429) {
$retryAfter = $data['parameters']['retry_after'] ?? 2;
Yii::warning("Rate limit 429 hit for method {$method}. Retry after {$retryAfter}s. Attempt {$attempt}", 'telegram');
sleep((int)$retryAfter);
continue;
}
// Проверка бизнес-ошибок Telegram (ok: false)
if ($httpCode !== 200 || empty($data['ok'])) {
$description = $data['description'] ?? 'Unknown Telegram Error';
$errorCode = $data['error_code'] ?? $httpCode;
Yii::error("Telegram API Error [{$method}]: Code {$errorCode} - {$description}", 'telegram');
return $data;
}
return $data;
}
throw new \RuntimeException("Max retries ({$this->maxRetries}) reached for Telegram API method {$method}");
}
}
Использование в Webhook-контроллере: защита, идемпотентность и генерация лидов
Теперь интегрируем созданный компонент в контроллер вебхука. Важно предусмотреть три рубежа защиты:
- Валидация заголовка
X-Telegram-Bot-Api-Secret-Token. - Идемпотентность обработки на основе
update_id(чтобы избежать дублей при повторных отправках со стороны Telegram). - Безопасная генерация идентификатора лида через
bin2hex(random_bytes(7)).
<?php
namespace app\controllers;
use Yii;
use yii\web\Controller;
use yii\web\Response;
use yii\web\BadRequestHttpException;
use app\components\TelegramClient;
class WebhookController extends Controller
{
public $enableCsrfValidation = false;
public function actionIndex(TelegramClient $telegram): array
{
Yii::$app->response->format = Response::FORMAT_JSON;
// 1. Проверка Secret Token
$secretHeader = Yii::$app->request->getHeaders()->get('X-Telegram-Bot-Api-Secret-Token');
$expectedSecret = Yii::$app->params['telegram']['secretToken'] ?? '';
if (empty($expectedSecret) || !hash_equals($expectedSecret, $secretHeader ?? '')) {
throw new BadRequestHttpException('Access denied: Invalid Secret Token');
}
// 2. Парсинг входящего Update
$rawPayload = Yii::$app->request->getRawBody();
$update = json_decode($rawPayload, true);
if (!isset($update['update_id'])) {
return ['status' => 'ignored'];
}
// 3. Дедупликация (идемпотентность) по update_id
$updateId = (int)$update['update_id'];
$cacheKey = "tg_update_processed_" . $updateId;
if (Yii::$app->cache->get($cacheKey)) {
return ['status' => 'already_processed'];
}
Yii::$app->cache->set($cacheKey, true, 86400);
// 4. Обработка команды /start
if (isset($update['message']['text'])) {
$chatId = $update['message']['chat']['id'];
$text = trim($update['message']['text']);
if (str_starts_with($text, '/start')) {
// Создаем уникальный lead_id без использования сессий
$leadId = bin2hex(random_bytes(7)); // 14 символов hex
Yii::$app->db->createCommand()->insert('leads', [
'lead_code' => $leadId,
'chat_id' => $chatId,
'status' => 'new',
'created_at' => date('Y-m-d H:i:s'),
])->execute();
$deepLink = "https://t.me/MyCompanyBot?start=ref_" . $leadId;
// Ограничение callback_data до 64 байт
$callbackData = 'confirm_' . substr($leadId, 0, 8);
$telegram->request('sendMessage', [
'chat_id' => $chatId,
'text' => "Заявка <strong>#" . htmlspecialchars($leadId) . "</strong> зарегистрирована!
"
. "Ваша ссылка: " . htmlspecialchars($deepLink),
'parse_mode' => 'HTML',
'reply_markup' => [
'inline_keyboard' => [[
['text' => 'Подтвердить', 'callback_data' => $callbackData]
]]
]
]);
}
}
return ['status' => 'ok'];
}
}
Настройка ротации логов для категории telegram
Чтобы ошибки взаимодействия с Telegram Bot API не затерялись в общем логе приложения, настройте отдельный таргетинг в config/web.php:
'components' => [
'log' => [
'traceLevel' => YII_DEBUG ? 3 : 0,
'targets' => [
[
'class' => 'yii\log\FileTarget',
'levels' => ['error', 'warning'],
'categories' => ['telegram'],
'logFile' => '@runtime/logs/telegram.log',
'maxFileSize' => 10240, // 10MB
'maxLogFiles' => 5,
],
],
],
],
Построение изолированного сервисного слоя в Yii2 с автоматической обработкой 429 ошибок и логгированием защищает проект от потери сообщений и сбоев при нагрузке.
Если вам требуется профессиональная помощь в архитектуре или комплексная разработка ботов и Mini Apps под ключ, обратитесь к команде BotCreator.