Telegram Bot Architecture on Yii2: Writing a Reliable TelegramClient with DI, Logging, and 429 Error Handling

Developing a fault-tolerant Telegram bot requires more than just sending HTTP requests via file_get_contents. In real production, you will encounter network failures, Telegram rate limits, and the need to securely store credentials. Using the "Service Layer" pattern in conjunction with Dependency Injection (DI) in Yii2 allows you to isolate the communication logic with Telegram Bot API, making the code testable and extensible.

1. Moving the Token to params-local.php

Never hardcode токен бота in class bodies. Private keys must not end up in version control systems (Git). Yii2 provides a mechanism for this by separating configuration into global (committed to the repository) and local (ignored by the version control system).

Let's define the configuration structure in the config/params.php file, setting default values:

<?php
// config/params.php
return [
'telegram' => [
'botToken' => null, // Переопределяется локально
'maxRetries' => 3, // Максимальное количество попыток при ошибке 429
],
];

And we will specify the actual token in the local config, which is added to .gitignore:

<?php
// config/params-local.php
return [
'telegram' => [
'botToken' => '123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ',
],
];

2. Designing the TelegramClient Component

Let's create the TelegramClient class. It will be responsible for sending POST requests via cURL, validating responses, logging network errors, and handling rate limits (HTTP code 429). Upon receiving a response with code 429, the client will automatically pause execution (sleep) for the number of seconds specified by Telegram and retry the request.

<?php

namespace app\components;

use Yii;
use yii\base\Component;
use yii\base\InvalidConfigException;

class TelegramClient extends Component
{
private string $token;
private int $maxRetries;
private string $apiUrl = 'https://api.telegram.org/bot';

public function __construct(array $config = [])
{
if (empty($config['token'])) {
throw new InvalidConfigException('Параметр Telegram bot token должен быть настроен.');
}
$this->token = $config['token'];
$this->maxRetries = $config['maxRetries'] ?? 3;
parent::__construct([]);
}

/**
* Отправка запроса к Telegram Bot API
* @param string $method Метод API (например, sendMessage)
* @param array $params Параметры запроса
* @param int $attempt Текущая попытка (для ретраев)
* @return array
* @throws \RuntimeException
*/
public function sendRequest(string $method, array $params = [], int $attempt = 1): array
{
$url = $this->apiUrl . $this->token . '/' . $method;
$ch = curl_init();

curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($params),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
]);

$response = curl_exec($ch);
$curlError = curl_error($ch);
$curlErrno = curl_errno($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

// Обработка транспортных ошибок cURL
if ($curlErrno !== 0) {
Yii::error("cURL error calling {$method}: {$curlError} ({$curlErrno})", 'telegram');
throw new \RuntimeException("Сбой сети при обращении к Telegram API: {$curlError}");
}

$data = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
Yii::error("Некорректный JSON от Telegram API в методе {$method}. Ответ: {$response}", 'telegram');
throw new \RuntimeException("Ошибка декодирования JSON от Telegram API");
}

// Обработка логических ошибок Telegram Bot API
if (isset($data['ok']) && $data['ok'] === false) {
$errorCode = $data['error_code'] ?? 0;
$description = $data['description'] ?? 'No description';

// Обработка лимитов (429 Too Many Requests)
if ($errorCode === 429 && $attempt <= $this->maxRetries) {
$retryAfter = $data['parameters']['retry_after'] ?? 1;
Yii::warning("Лимит запросов превышен (429). Ожидание {$retryAfter} сек. Попытка {$attempt}/{$this->maxRetries}", 'telegram');
sleep($retryAfter);
return $this->sendRequest($method, $params, $attempt + 1);
}

Yii::error("Telegram API вернул ошибку {$errorCode}: {$description} (Метод: {$method})", 'telegram');
}

return $data;
}
}

3. Registering TelegramClient in the Yii2 DI Container

To avoid creating a class instance manually using the new operator and to prevent dependency on global state, let's register our client in the Yii2 Dependency Injection (DI) container. This is done in the application configuration file (for example, config/web.php or config/main.php for the console).

<?php
// config/web.php
$params = require __DIR__ . '/params.php';
if (file_exists(__DIR__ . '/params-local.php')) {
$params = yii\helpers\ArrayHelper::merge($params, require __DIR__ . '/params-local.php');
}

$config = [
'id' => 'basic',
'basePath' => dirname(__DIR__),
'bootstrap' => ['log'],
'container' => [
'definitions' => [
app\components\TelegramClient::class => function () use ($params) {
return new app\components\TelegramClient([
'token' => $params['telegram']['botToken'] ?? null,
'maxRetries' => $params['telegram']['maxRetries'] ?? 3,
]);
},
],
],
// ... остальные настройки компонента log и db
];

return $config;

4. Using the Client in Controllers and Services

Thanks to the Yii2 DI container, we can inject TelegramClient directly into the constructor of a controller or a console command. Yii will automatically resolve the dependencies and pass the configured object.

<?php

namespace app\controllers;

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

class BotController extends Controller
{
private TelegramClient $telegram;

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

/**
* Пример отправки сообщения пользователю
*/
public function actionNotifyUser()
{
$chatId = Yii::$app->request->post('chat_id');
$message = Yii::$app->request->post('message');

if (!$chatId || !$message) {
return $this->asJson(['success' => false, 'error' => 'Missing parameters']);
}

try {
$result = $this->telegram->sendRequest('sendMessage', [
'chat_id' => $chatId,
'text' => $message,
'parse_mode' => 'HTML',
]);

if (isset($result['ok']) && $result['ok'] === true) {
return $this->asJson(['success' => true, 'message_id' => $result['result']['message_id']]);
}

return $this->asJson([
'success' => false,
'error' => $result['description'] ?? 'Неизвестная ошибка Telegram API'
]);

} catch (\Exception $e) {
return $this->asJson(['success' => false, 'error' => $e->getMessage()]);
}
}
}

5. Configuring Telegram API Error Logging in Yii2

All errors caught in our client using Yii::error() with the category tag 'telegram' should be written to a separate log file. This simplifies debugging in production. Add a new target to the log component configuration in the config/web.php file:

'components' => [
'log' => [
'targets' => [
[
'class' => 'yii\log\FileTarget',
'levels' => ['error', 'warning'],
'categories' => ['telegram'],
'logFile' => '@runtime/logs/telegram.log',
'logVars' => [], // Отключаем дамп глобальных переменных для приватности
],
],
],
]

Conclusion

Using a dedicated service layer like TelegramClient integrated via the DI container solves several architectural problems at once. You eliminate cURL request code duplication, ensure centralized логирование ошибок, protect tokens from leaking into public repositories, and make the application resilient to temporary Telegram blocks (HTTP 429) thanks to automatic retries.

If you need professional development of complex integrations or turnkey chatbots, contact the specialists at BotCreator.

New articles on Telegram

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