Bot Service Layer in Yii2: TelegramClient Component, DI, Logging, and 429 Retries

When developing commercial Telegram bots using the Yii2 framework, a common mistake is calling message sending functions directly from business logic or straight from the Webhook controller. This leads to code duplication, testing difficulties, and total lack of protection against Telegram Bot API limits (HTTP 429 Too Many Requests error).

Proper architecture requires isolating a dedicated service layer. In this article, we will create an isolated TelegramClient component, configure its injection via the Yii2 Dependency Injection (DI) container, handle error API statuses, and implement secure handling of tokens.

Why You Need a TelegramClient Service Layer in Yii2

Using third-party libraries is not always justified: they are often overloaded with unnecessary abstractions, contain outdated dependencies, or do not allow fine-grained HTTP request management. A native component written with cURL inside Yii2 solves several problems at once:

  • Configuration isolation: bot секретный токен are stored in params-local.php and are not committed to the version control system (Git).
  • Centralized error handling: all instances of network failures, invalid JSON, or ok: false statuses are handled in a single place and recorded in Yii2 log files.
  • Rate limiting control: automatic pause upon receiving HTTP status code 429 based on the retry_after header.
  • Ease of testing: the client object can be mocked via the Yii::$container DI container.

Yii2 Configuration: Moving Tokens to params-local.php

Never store tokens directly in controller code or the main web.php file. Create parameters in the config/params-local.php file, which should be added to .gitignore.

<?php

return [
'telegram' => [
'botToken' => getenv('TELEGRAM_BOT_TOKEN') ?: '123456789:AAXxxXxxXxxXxxXxxXxxXxxXxxXxxXxxXxx',
'secretToken' => getenv('TELEGRAM_SECRET_TOKEN') ?: 'c8a3f890e72b4d1198e1f02a3a5d8b92',
],
];

Next, we will register the component in the DI container services. This is done in the application configuration file (config/web.php or config/main.php) inside the container section:

<?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;

Implementing the TelegramClient Component with 429 Retries

Let's create the app\components\TelegramClient class. The client will send requests via cURL (using file_get_contents is unacceptable due to the lack of flexible timeout and HTTP header configuration). If a 429 response is received, the component reads the retry_after parameter and retries the request.

<?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}");
}
}

Usage in the Webhook Controller: Security, Idempotency, and Lead Generation

Now, let's integrate the created component into the webhook controller. It is important to implement three lines of defense:

  1. Validation of the X-Telegram-Bot-Api-Secret-Token header.
  2. Идемпотентность обработки based on update_id (to avoid duplicate processing during retries from Telegram).
  3. Secure generation of lead IDs using 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'];
}
}

Configuring Log Rotation for the Telegram Category

To ensure errors during Telegram Bot API interaction aren't lost in the general application log, configure a dedicated target in 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,
],
],
],
],

Building an isolated service layer in Yii2 with automatic handling of 429 errors and logging protects your project from lost messages and crashes under load.

If you need professional architecture assistance or turnkey разработка ботов and Mini Apps, contact the BotCreator team.

New articles on Telegram

We explain what to automate in your business and how it works in practice. No spam.