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

Developing a production-grade Telegram bot in Yii2 requires architectural separation of concerns. Invoking the API via ad-hoc file_get_contents calls or cURL requests scattered across controllers leads to code duplication, the lack of a single logging point, and application crashes when hitting Telegram Bot API rate limits (HTTP 429 Too Many Requests).

In this article, we will build an isolated service layer: create a TelegramClient component, configure its injection via DI-контейнер Yii2, ensure secure token storage, and implement automatic retries when receiving a 429 Too Many Requests response.

Extracting Configuration and Tokens into params-local.php

The Telegram-бот token is sensitive information. It must strictly never be stored in a VCS repository (Git). In the Yii2 framework, environment-specific secrets are configured using the config/params-local.php file, which is added to .gitignore.

Let's create the configuration parameters and register our service in the Dependency Injection Container.

// config/params-local.php
<?php

return [
'telegram.botToken' => getenv('TELEGRAM_BOT_TOKEN') ?: '1234567890:ABCdefGHIjklMNOpqrsTUVwxyz',
'telegram.apiBaseUrl' => 'https://api.telegram.org/bot',
];

Next, let's configure the DI container in the application configuration file config/web.php (or config/main.php for the advanced template) so that the TelegramClient service is injected automatically with the required arguments.

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

$config = [
'id' => 'app-bot',
'basePath' => dirname(__DIR__),
'container' => [
'definitions' => [
\app\components\TelegramClient::class => [
'class' => \app\components\TelegramClient::class,
'botToken' => $params['telegram.botToken'],
'baseUrl' => $params['telegram.apiBaseUrl'],
'timeout' => 10,
'maxRetries' => 3,
],
],
],
'params' => $params,
];

return $config;

Creating the TelegramClient Service Component with Retries

The service class should be responsible for executing HTTP requests to the Telegram API via cURL, validating response status codes, парсинг JSON, handling network errors, and responding correctly to rate limits. Telegram returns HTTP status code 429 when request rate limits are exceeded, providing a parameters.retry_after parameter in seconds.

Below is the complete implementation of the TelegramClient component with a retry loop for 429 errors and structured logging via Yii::error() and Yii::warning().

<?php

namespace app\components;

use Yii;
use yii\base\Component;
use yii\base\InvalidArgumentException;

class TelegramClient extends Component
{
public string $botToken = '';
public string $baseUrl = 'https://api.telegram.org/bot';
public int $timeout = 10;
public int $maxRetries = 3;

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

/**
* Выполнение запроса к Telegram Bot API
*
* @param string $method Имя метода (например, sendMessage, sendPhoto)
* @param array $payload Массив параметров метода
* @return array
* @throws \RuntimeException
*/
public function sendRequest(string $method, array $payload = []): array
{
$url = rtrim($this->baseUrl, '/') . $this->botToken . '/' . $method;
$attempt = 0;

while ($attempt < $this->maxRetries) {
$attempt++;
$ch = curl_init();

$jsonPayload = json_encode($payload, JSON_UNESCAPED_UNICODE);

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

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

if ($curlErrno !== 0) {
Yii::error("cURL error [{$curlErrno}] при вызове {$method}: {$curlError}", 'telegram');
if ($attempt >= $this->maxRetries) {
throw new \RuntimeException("Ошибка сети Telegram API: {$curlError}");
}
sleep(1);
continue;
}

$response = json_decode($rawResponse, true);
$jsonError = json_last_error();

if ($jsonError !== JSON_ERROR_NONE) {
Yii::error("Некорректный JSON от Telegram API [{$method}]: {$rawResponse}", 'telegram');
throw new \RuntimeException("Некорректный формат ответа Telegram API.");
}

// Обработка лимита вызовов (429 Too Many Requests)
if ($httpCode === 429 || (isset($response['ok']) && $response['ok'] === false && ($response['error_code'] ?? 0) === 429)) {
$retryAfter = $response['parameters']['retry_after'] ?? 1;
Yii::warning("Telegram 429 Too Many Requests на методе {$method}. Попытка {$attempt}/{$this->maxRetries}. Пауза {$retryAfter} сек.", 'telegram');

if ($attempt < $this->maxRetries) {
sleep((int)$retryAfter);
continue;
}
}

// Обработка остальных ошибок API
if (!isset($response['ok']) || $response['ok'] !== true) {
$description = $response['description'] ?? 'Неизвестная ошибка';
$errorCode = $response['error_code'] ?? $httpCode;

Yii::error("Telegram API Error [{$errorCode}] на методе {$method}: {$description}. Payload: " . json_encode($payload, JSON_UNESCAPED_UNICODE), 'telegram');

return [
'ok' => false,
'error_code' => $errorCode,
'description' => $description,
];
}

return $response;
}

throw new \RuntimeException("Превышено максимальное число попыток запроса к Telegram API [{$method}].");
}
}

Using TelegramClient in Services and Controllers

Thanks to integration with the Yii2 DI container, the TelegramClient component can be requested directly in the constructors of controllers, console commands, or domain services. This makes the code easily testable and independent of global state.

Let's consider an example of business logic for registering a customer lead with unique identifier generation, saving to the database, and notifying an administrator via our TelegramClient.

<?php

namespace app\services;

use app\components\TelegramClient;
use Yii;
use yii\db\Connection;

class LeadProcessor
{
private TelegramClient $client;
private Connection $db;

public function __construct(TelegramClient $client, Connection $db)
{
$this->client = $client;
$this->db = $db;
}

/**
* Создание лида и отправка уведомления в Telegram
*/
public function createLead(int $chatId, string $customerName, string $phone): bool
{
// Генерация безопасного ID лида (не используем сессионные ключи)
$leadId = bin2hex(random_bytes(7));

$transaction = $this->db->beginTransaction();
try {
$this->db->createCommand()->insert('{{%leads}}', [
'lead_id' => $leadId,
'chat_id' => $chatId,
'name' => $customerName,
'phone' => $phone,
'created_at' => time(),
])->execute();

$transaction->commit();
} catch (\Throwable $e) {
$transaction->rollBack();
Yii::error("Ошибка сохранения лида {$leadId}: " . $e->getMessage(), 'leads');
return false;
}

$text = "<b>Новая заявка #" . htmlspecialchars($leadId) . "</b>
";
$text .= "Имя: " . htmlspecialchars($customerName) . "
";
$text .= "Телефон: " . htmlspecialchars($phone);

$response = $this->client->sendRequest('sendMessage', [
'chat_id' => $chatId,
'text' => $text,
'parse_mode' => 'HTML',
'reply_markup' => [
'inline_keyboard' => [
[
['text' => 'Подтвердить', 'callback_data' => 'confirm_lead:' . $leadId],
]
]
]
]);

return isset($response['ok']) && $response['ok'] === true;
}
}

Telegram Bot API Limits and Architectural Checklist

When building the service layer, it is essential to consider official limits and Telegram infrastructure specifics:

  • Send limits: No more than 30 messages per second total across all chats and no more than 1 message per second to a single specific chat/channel. When sending broadcasts, always push tasks into a queue (for example, via yii2-queue) and use rate limiting.
  • callback_data length: The callback_data field in inline-кнопка is limited to 64 bytes. Do not pass data arrays or long tokens inside it — pass a short ID (for example, bin2hex(random_bytes(7))) and fetch the data from the database.
  • Response logging: Be sure to configure the telegram log category in config/web.php or config/console.php with a dedicated target file (@runtime/logs/telegram.log) so that API call errors are not lost in the general log stream.
  • Webhook idempotency: The incoming webhook controller should check the X-Telegram-Bot-Api-Secret-Token header and log processed update_id values in a storage engine (Redis or DB) to prevent duplicate processing of the same event.

Creating a dedicated TelegramClient component with dependency injection and error handling makes your Yii2 project resilient to network failures and rate limit enforcement by the Telegram Bot API.

If you need turnkey development of a fault-tolerant Telegram bot infrastructure integrated with a CRM, the BotCreator team can help you implement a solution of any complexity.

New articles on Telegram

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