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

При разработке Telegram-ботов на Yii2 частой ошибкой становится размазывание вызовов Telegram Bot API по контроллерам или фоновым задачам. Использование разрозненных вызовов file_get_contents или прямых HTTP-клиентов без централизованной обработки приводит к потере логов, утечке токенов в репозиторий и падению сервиса при получении лимитов от Telegram (HTTP 429).

Правильный подход — вынесение взаимодействия с HTTP API в сервисный слой application component с регистрацией в DI-контейнере (Dependency Injection) Yii2. Это обеспечивает единственную точку ответственности за авторизацию, сетевые таймауты, парсинг ответов и повторные попытки запросов при временных сбоях.

Безопасная конфигурация токена и DI-контейнера

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

Создадим конфигурационный массив параметров и пропишем инициализацию компонента через внедрение зависимостей в конфигурации Yii2.

<?php
// config/params-local.php
return [
'telegram' => [
'botToken' => getenv('TELEGRAM_BOT_TOKEN') ?: '123456789:AA_EXAMPLE_TOKEN_DO_NOT_COMMIT',
'timeout' => 10,
'maxRetries' => 3]];

Теперь зарегистрируем класс TelegramClient в DI-контейнере приложения через config/web.php или config/main.php, чтобы Yii2 мог автоматически внедрять его в контроллеры и консольные команды:

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

$config = [
'id' => 'app-telegram',
'basePath' => dirname(__DIR__),
'components' => [
// Прочая конфигурация компонентов...
],
'container' => [
'definitions' => [
\app\components\TelegramClient::class => function ($container, $params, $config) {
$tgParams = Yii::$app->params['telegram'] ?? [];
return new \app\components\TelegramClient([
'botToken' => $tgParams['botToken'] ?? '',
'timeout' => $tgParams['timeout'] ?? 10,
'maxRetries' => $tgParams['maxRetries'] ?? 3]);
}]]];

return $config;

Реализация компонента TelegramClient с cURL и ретраями

Сервисный компонент должен использовать библиотеку cURL для полного контроля над заголовками, таймаутами и кодами ответа HTTP. Функцию file_get_contents категорически не рекомендуется использовать для работы с Telegram API, так как она плохо обрабатывает сетевые ошибки и фатально падает при ответах с HTTP-статусами 4xx/5xx.

При получении ответа 429 Too Many Requests Telegram возвращает в JSON полезную нагрузку parameters.retry_after — количество секунд, которое необходимо подождать перед повторной отправкой. Реализуем эту логику внутри цикла ретраев.

<?php

namespace app\components;

use Yii;
use yiiase\Component;
use yiiase\InvalidConfigException;

class TelegramClient extends Component
{
public string $botToken = '';
public int $timeout = 10;
public int $maxRetries = 3;

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

/**
* Выполнение запроса к Telegram Bot API
*
* @param string $method Название метода API (например, sendMessage)
* @param array $params Массив параметров запроса
* @return array Массив ответа от Telegram API
*/
public function request(string $method, array $params = []): array
{
$url = "https://api.telegram.org/bot{$this->botToken}/{$method}";
$attempt = 0;

while ($attempt <= $this->maxRetries) {
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($params),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => $this->timeout,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_SSL_VERIFYPEER => true]);

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

if ($rawResponse === false) {
Yii::error("cURL error during Telegram API call {$method}: {$curlError}", 'telegram');
$attempt++;
usleep(500000); // 500ms пауза перед повтором cURL
continue;
}

$data = json_decode($rawResponse, true);
if (json_last_error() !== JSON_ERROR_NONE) {
Yii::error("Invalid JSON from Telegram API {$method}: " . json_last_error_msg(), 'telegram');
return ['ok' => false, 'description' => 'Invalid JSON response'];
}

// Обработка ограничения по частоте запросов (Rate Limit)
if ($httpCode === 429) {
$retryAfter = $data['parameters']['retry_after'] ?? 1;
Yii::warning("Telegram 429 for method {$method}. Retry after {$retryAfter}s (attempt {$attempt})", 'telegram');
sleep((int)$retryAfter);
$attempt++;
continue;
}

// Логирование ошибок API (код не 200 или ok: false)
if ($httpCode !== 200 || !isset($data['ok']) || $data['ok'] !== true) {
$desc = $data['description'] ?? 'Unknown Telegram error';
Yii::error("Telegram API Error [HTTP {$httpCode}] on {$method}: {$desc}", 'telegram');
}

return $data;
}

return ['ok' => false, 'description' => 'Max execution retries exceeded'];
}
}

Внедрение сервиса через DI в Webhook-контроллере

Благодаря автосвязыванию (autowiring) в Yii2, созданный компонент TelegramClient внедряется прямо в конструктор контроллера или сервиса бизнес-логики. Контроллер занимается только валидацией входного запроса и передает сформированный ответ клиенту.

<?php

namespace app\controllers;

use Yii;
use yii\web\Controller;
use yii\web\Response;
use app\components\TelegramClient;

class WebhookController extends Controller
{
public $enableCsrfValidation = false;
private TelegramClient $telegram;

// Внедрение зависимости через конструктор контроллера
public function __construct($id, $module, TelegramClient $telegram, $config = [])
{
$this->telegram = $telegram;
parent::__construct($id, $module, $config);
}

public function actionIndex(): Response
{
$secretToken = Yii::$app->request->getHeaders()->get('X-Telegram-Bot-Api-Secret-Token');
$expectedToken = getenv('TELEGRAM_WEBHOOK_SECRET');

if (!empty($expectedToken) && $secretToken !== $expectedToken) {
Yii::warning('Unauthorized webhook call: invalid secret token', 'telegram');
return $this->asJson(['ok' => false, 'error' => 'Unauthorized'])->setStatusCode(403);
}

$update = json_decode(Yii::$app->request->getRawBody(), true);
if (json_last_error() !== JSON_ERROR_NONE || !is_array($update)) {
return $this->asJson(['ok' => false, 'error' => 'Invalid JSON']);
}

if (isset($update['message']['chat']['id'])) {
$chatId = $update['message']['chat']['id'];

$this->telegram->request('sendMessage', [
'chat_id' => $chatId,
'text' => 'Ваш запрос принят и успешно обработан в сервисе Yii2.',
'parse_mode' => 'HTML']);
}

return $this->asJson(['ok' => true]);
}
}

Настройка категории логирования для Telegram

Для того чтобы ошибки вызовов Telegram API не терялись в общем потоке системного лога, настроим отдельную категорию логов в config/web.php. Это позволит сбрасывать ошибки взаимодействия с Bot API в отдельный файл runtime/logs/telegram.log.

<?php
// config/web.php (раздел components -> log -> targets)
'log' => [
'traceLevel' => YII_DEBUG ? 3 : 0,
'targets' => [
[
'class' => 'yii\log\FileTarget',
'levels' => ['error', 'warning'],
'categories' => ['telegram'],
'logFile' => '@runtime/logs/telegram.log',
'logVars' => []]]],

Архитектурные рекомендации по работе с API

  • Разделение на подсистемы: если бот отправляет массовые рассылки или тяжелые медиафайлы, вызовы TelegramClient следует переносить из веб-контроллера в фоновые задачи (Yii2 Queue).
  • Контроль callback_data: объём данных в inline-кнопках строго ограничен 64 байтами. Передавайте только короткие ключи или UUID, храня основные параметры в БД.
  • Сетевые тайм-ауты: при отправке фото и документов увеличивайте параметр CURLOPT_TIMEOUT до 30–60 секунд, чтобы cURL не обрывал соединение до завершения загрузки файлового потока.

Делегировать разработку высоконагруженного сервисного слоя и безопасных интеграций любой сложности вы можете команде BotCreator.

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

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