При розробці Telegram-ботів на Yii2 частою помилкою стає розмивання викликів Telegram Bot API по контролерах або фонових задачах. Використання розрізнених викликів file_get_contents або прямих HTTP-клієнтів без централізованої обробки призводить до втрати логів, витоку токенів у репозиторій і падіння сервісу при отриманні лімітів від Telegram (HTTP 429).
Правильний підхід — винесення взаємодії з HTTP API в сервісний шар application component з реєстрацією в DI-контейнері (Dependency Injection) Yii2. Це забезпечує єдину точку відповідальності за авторизацію, сетевые таймауты, парсинг відповідей та повторні спроби запитів при тимчасових збоях.
Безпечна конфігурація токена та DI-контейнера
Токен бота не можна зберігати в контролерах або версіонованому файлі config/web.php. У Yii2 оптимальним місцем для секретів є файл config/params-local.php, який виключений з Git-репозиторію, або читання глобальних змінних оточення через getenv().
Створимо конфігураційний масив параметрів і пропишемо ініціалізацію компонента через внедрение зависимостей у конфігурації Yii2.
<?php
// config/params-local.php
return [
'telegram' => [
'botToken' => getenv('TELEGRAM_BOT_TOKEN') ?: '123456789:AA_EXAMPLE_TOKEN_DO_NOT_COMMIT',
'timeout' => 10,
'maxRetries' => 3]];
Тепер зареєструємо клас TelegramClient у DI-контейнері додатка через config/web.php або config/main.php, щоб Yii2 міг автоматично впроваджувати його в контролери та консольні команди:
<?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;
Реалізація компонента TelegramClient з cURL та ретраями
Сервісний компонент має використовувати бібліотеку cURL для повного контролю над заголовками, таймаутами та кодами відповіді HTTP. Функцію file_get_contents категорично не рекомендується використовувати для роботи з Telegram API, оскільки вона погано обробляє мережеві помилки і фатально падає при відповідях з HTTP-статусами 4xx/5xx.
При отриманні відповіді 429 Too Many Requests Telegram повертає в JSON корисне навантаження parameters.retry_after — кількість секунд, яку необхідно почекати перед повторним відправленням. Реалізуємо цю логіку всередині циклу ретраїв.
<?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'];
}
}
Впровадження сервісу через DI у Webhook-контролері
Завдяки автозв'язуванню (autowiring) в Yii2, створений компонент TelegramClient впроваджується прямо в конструктор контролера або сервісу бізнес-логики. Контролер займається тільки валідацією вхідного запиту і передає сформовану відповідь клієнту.
<?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]);
}
}
Налаштування категорії логування для Telegram
Для того щоб помилки викликів Telegram API не губилися в загальному потоці системного логу, налаштуємо окрему категорію логів у config/web.php. Це дозволить скидати помилки взаємодії з Bot API в окремий файл 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' => []]]],
Архітектурні рекомендації щодо роботи з API
- Розділення на підсистеми: якщо бот надсилає масові розсилки або важкі медіафайли, виклики
TelegramClientслід переносити з веб-контролера у фонові завдання (Yii2 Queue). - Контроль
callback_data: обсяг даних у inline-кнопка строго обмежений 64 байтами. Передавайте тільки короткі ключі або UUID, зберігаючи основні параметри в БД. - Мережеві тайм-аути: при відправленні фото та документів збільшуйте параметр
CURLOPT_TIMEOUTдо 30–60 секунд, щоб cURL не обривав з'єднання до завершення завантаження файлового потока.
Делегувати розробку високонавантаженого сервісного шару та безпечних інтеграцій будь-якої складності ви можете команді BotCreator.