Архитектура Telegram-бота на Yii2: пишем надежный TelegramClient с DI, логированием и обработкой 429 ошибок

Разработка отказоустойчивого Telegram-бота требует большего, чем просто отправка HTTP-запросов через file_get_contents. В реальном продакшене вы столкнетесь с сетевыми сбоями, лимитами Telegram (rate limits) и необходимостью безопасно хранить учетные данные. Использование паттерна «Сервисный слой» (Service Layer) в связке с Dependency Injection (DI) в Yii2 позволяет изолировать логику общения с Telegram Bot API, сделать код тестируемым и расширяемым.

1. Вынос токена в params-local.php

Никогда не хардкодьте токен бота в теле классов. Приватные ключи не должны попадать в систему контроля версий (Git). В Yii2 для этого предусмотрен механизм разделения конфигурации на глобальную (коммитится в репозиторий) и локальную (игнорируется системой контроля версий).

Определим структуру конфигурации в файле config/params.php, задав дефолтные значения:

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

А реальный токен пропишем в локальном конфиге, который добавлен в .gitignore:

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

2. Проектирование компонента TelegramClient

Создадим класс TelegramClient. Он будет отвечать за отправку POST-запросов через cURL, валидацию ответов, логирование сетевых ошибок и обработку лимитов (HTTP-код 429). При получении ответа с кодом 429 клиент автоматически приостановит выполнение (sleep) на указанное Telegram количество секунд и повторит запрос.

<?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. Регистрация TelegramClient в DI-контейнере Yii2

Чтобы не создавать экземпляр класса вручную через оператор new и не зависеть от глобального состояния, зарегистрируем наш клиент в контейнере зависимостей (Dependency Injection) Yii2. Это делается в конфигурационном файле приложения (например, config/web.php или config/main.php для консоли).

<?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. Использование клиента в контроллерах и сервисах

Благодаря DI-контейнеру Yii2, мы можем внедрить TelegramClient напрямую в конструктор контроллера или консольной команды. Yii автоматически разрешит зависимости и передаст настроенный объект.

<?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. Настройка логирования ошибок Telegram API в Yii2

Все ошибки, перехваченные в нашем клиенте с помощью Yii::error() и категорийным тегом 'telegram', должны записываться в отдельный файл логов. Это упрощает отладку в продакшене. Добавьте новый таргет в конфигурацию компонента log в файле config/web.php:

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

Заключение

Использование выделенного сервисного слоя TelegramClient с интеграцией через DI-контейнер решает сразу несколько архитектурных проблем. Вы избавляетесь от дублирования кода cURL-запросов, обеспечиваете централизованное логирование ошибок, защищаете токены от утечки в публичные репозитории и делаете приложение устойчивым к временным блокировкам Telegram (HTTP 429) благодаря автоматическим повторным попыткам.

Если вам требуется профессиональная разработка сложных интеграций или чат-ботов под ключ, обратитесь к специалистам BotCreator.

Новые статьи — в Telegram

Разбираем, что автоматизировать в бизнесе и как это работает на практике. Без спама.