Telegram Bot Service Layer in Yii2: TelegramClient, DI, 429 Retries, and Token Storage

When developing Telegram bots on Yii2, a common mistake is scattering Telegram Bot API calls across controllers or background tasks. Using scattered file_get_contents calls or direct HTTP clients without centralized processing leads to lost logs, token leaks into the repository, and service crashes when hitting Telegram rate limits (HTTP 429).

The correct approach is to move interaction with the HTTP API to the application component service layer, registering it in the Yii2 DI (Dependency Injection) container. This ensures a single point of responsibility for authorization, сетевые таймауты, response parsing, and request retries during temporary failures.

Secure Token and DI Container Configuration

The bot token must not be stored in controllers or the version-controlled config/web.php file. In Yii2, the optimal place for secrets is the config/params-local.php file, which is excluded from the Git repository, or reading global environment variables via getenv().

Let's create a configuration array of parameters and define the component initialization via внедрение зависимостей in the Yii2 configuration.

<?php
// config/params-local.php
return [
'telegram' => [
'botToken' => getenv('TELEGRAM_BOT_TOKEN') ?: '123456789:AA_EXAMPLE_TOKEN_DO_NOT_COMMIT',
'timeout' => 10,
'maxRetries' => 3]];

Now, let's register the TelegramClient class in the application's DI container via config/web.php or config/main.php, so that Yii2 can automatically inject it into controllers and console commands:

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

Implementing the TelegramClient Component with cURL and Retries

The service component should use the cURL library for full control over headers, timeouts, and HTTP response codes. Using the file_get_contents function is strongly discouraged for working with the Telegram API, as it handles network errors poorly and crashes fatally on responses with HTTP 4xx/5xx statuses.

Upon receiving a 429 Too Many Requests response, Telegram returns a parameters.retry_after payload in JSON — the number of seconds to wait before retrying. Let's implement this logic inside a retry loop.

<?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'];
}
}

Injecting the Service via DI in a Webhook Controller

Thanks to autowiring in Yii2, the created TelegramClient component is injected directly into the constructor of the controller or business logic service. The controller only handles incoming request validation and passes the prepared response to the client.

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

Configuring a Logging Category for Telegram

To prevent Telegram API call errors from getting lost in the general system log stream, let's configure a separate log category in config/web.php. This will allow routing Bot API interaction errors to a separate file, 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' => []]]],

Architectural Recommendations for Working with the API

  • Subsystem separation: if the bot sends bulk mailings or heavy media files, TelegramClient calls should be moved from the web controller to background tasks (Yii2 Queue).
  • callback_data control: the volume of data in inline-кнопка is strictly limited to 64 bytes. Pass only short keys or UUIDs, storing the main parameters in the database.
  • Network timeouts: when sending photos and documents, increase the CURLOPT_TIMEOUT parameter to 30–60 seconds so that cURL does not drop the connection before the file stream upload is complete.

You can delegate the development of a high-load service layer and secure integrations of any complexity to the BotCreator team.

New articles on Telegram

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