Сервісний шар бота в Yii2: компонент TelegramClient, DI, логування та ретраї 429

Під час розробки комерційних 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 з ретраями 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-контролері: захист, ідемпотентність та генерація лідів

Тепер інтегруємо створений компонент у контролер вебхука. Важливо передбачити три рубежі захисту:

  1. Валідація заголовка X-Telegram-Bot-Api-Secret-Token.
  2. Идемпотентность обработки на основі update_id (щоб уникнути дублів при повторних надсиланнях з боку Telegram).
  3. Безпечна генерація ідентифікатора ліда через 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.

Нові статті — у Telegram

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