Сервісний рівень 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

Розбираємо, що автоматизувати в бізнесі та як це працює на практиці. Без спаму.