Сборка клиентского компонента Telegram для Yii2: DI, Token Via params, 429 Retries, Error Logging

ЧТО МЫ МОЖЕМ СОЗДАТЬ

A многоразового использования TelegramClient компонент для приложения Yii2, которое оборачивает API Telegram Bot по протоколу HTTPS. Цель - это уровень службы, который вы можете вызвать из контроллеров, консольных команд или рабочих очереди, не разбрасывая HTTP-код по всему проекту. Мы держим токен вне контроля источника, громко выходим из строя при проблемах с транспортом и автоматически отступаем, когда Telegram возвращается Слишком много запросов.

Эта статья не претендует на охват каждого метода API бота. Он фокусируется на транспортно-интеграционном уровне: внедрение зависимостей в Yii2, конфигурация через params-local.php, настройка cURL, ведение журнала ошибок и минимальный цикл повтора.

Макет проекта

Примите стандартный базовый или расширенный шаблон Yii2. Компонент живет в app/components/TelegramClient.php и подключается через конфигурацию приложения.

app/
components/
TelegramClient.php
config/
web.php
params.php
params-local.php // not committed

Компонент

TelegramClient extends yii\base\Component. Он раскрывает один публичный метод, call($method, $params), а также несколько настраиваемых параметров: значения тайм-аута, максимальное количество повторных попыток и маркер бота. Мы регистрируем его как компонент приложения, поэтому контейнер Yii DI дает нам настроенный экземпляр, где бы мы его ни ввели - hint.

<?php
namespace app\components;

use Yii;
use yii\base\Component;
use yii\base\InvalidConfigException;
use yii\httpclient\Client as HttpClient; // optional, see notes below

class TelegramClient extends Component
{
public string $botToken = '';
public string $apiBase = 'https://api.telegram.org';
public int $timeout = 5;
public int $connectTimeout = 3;
public int $maxRetries = 3;

public function init(): void
{
parent::init();
if ($this->botToken === '') {
$token = getenv('TELEGRAM_BOT_TOKEN');
if (is_string($token) && $token !== '') {
$this->botToken = $token;
}
}
if ($this->botToken === '') {
throw new InvalidConfigException('TelegramClient: botToken is not configured.');
}
}

/**
* @param string $method Bot API method, e.g. 'sendMessage'
* @param array $params request body
* @return array decoded JSON response
* @throws \RuntimeException on transport failure after retries
*/
public function call(string $method, array $params = []): array
{
$url = $this->apiBase . '/bot' . $this->botToken . '/' . $method;
$attempt = 0;
$delaySeconds = 1;

while (true) {
$attempt++;
$response = $this->httpPostJson($url, $params);

if ($response['status'] === 200 && $response['json']['ok'] === true) {
return $response['json'];
}

// 429: respect Retry-After when present
if ($response['status'] === 429) {
$retryAfter = (int)($response['json']['parameters']['retry_after'] ?? $delaySeconds);
Yii::warning(
"Telegram 429 on {$method}, sleeping {$retryAfter}s (attempt {$attempt})",
__METHOD__
);
if ($attempt >= $this->maxRetries) {
break;
}
sleep(max(1, $retryAfter));
continue;
}

// 5xx: retry with linear backoff
if ($response['status'] >= 500 && $response['status'] < 600) {
Yii::warning(
"Telegram {$response['status']} on {$method} (attempt {$attempt})",
__METHOD__
);
if ($attempt >= $this->maxRetries) {
break;
}
sleep($delaySeconds);
$delaySeconds++;
continue;
}

// Hard failure: 4xx (except 429), parse errors, etc.
Yii::error([
'method' => $method,
'status' => $response['status'],
'body' => $response['raw'],
], __METHOD__);
throw new \RuntimeException("Telegram API call failed: {$method}");
}

Yii::error([
'method' => $method,
'status' => $response['status'] ?? 0,
'note' => 'retries exhausted',
], __METHOD__);
throw new \RuntimeException("Telegram API call failed after {$attempt} attempts: {$method}");
}

private function httpPostJson(string $url, array $payload): array
{
$ch = curl_init($url);
$json = json_encode($payload, JSON_UNESCAPED_UNICODE);

curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $json,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => $this->connectTimeout,
CURLOPT_TIMEOUT => $this->timeout,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
]);

$body = curl_exec($ch);
$errno = curl_errno($ch);
$errstr = curl_error($ch);
$status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($body === false) {
Yii::error([
'curl_errno' => $errno,
'curl_error' => $errstr,
'url' => $url,
], __METHOD__);
return ['status' => 0, 'json' => ['ok' => false], 'raw' => ''];
}

$decoded = json_decode($body, true);
return [
'status' => $status,
'json' => is_array($decoded) ? $decoded : ['ok' => false],
'raw' => $body,
];
}
}

Две детали, на которые стоит обратить внимание.

Сначала мы проверяем json['ok'] === true. Telegram всегда возвращает 200 http при успешных вызовах и сигнализирует о сбое бизнес-уровня с помощью ok = false; плюс error_code и описание. Мы отделяем транспортные ошибки (коды состояния, сбои cURL) от ошибок API (ok = false;).

Во-вторых, 429 handling использует Повторное соединение после: поле, возвращенное в параметры. Telegram отправляет это, когда вступают в силу глобальные ограничения скорости для каждого бота. Мы также повторим попытку на 5xx с линейным возвратом. Все остальное регистрируется и выбрасывается.

Подключение в Yii2

В config/web.php зарегистрируйте компонент. Мы позволяем Yii собрать его через контейнер DI, чтобы любой сервис, который зависит от TelegramClient получает тот же настроенный экземпляр.

'components' => [
'telegram' => [
'class' => \app\components\TelegramClient::class,
'botToken' => Yii::$app->params['telegramBotToken'] ?? '',
'timeout' => 5,
'connectTimeout' => 3,
'maxRetries' => 3,
],
// ...
],

config/params.php объявляет ключ с местозаполнителем, чтобы приложение все еще загружалось без локального файла.

return [
'telegramBotToken' => '',
];

config/params-local.php загружается поверх params.php и является правильным местом для секретов. Добавьте его в .gitignore.

<?php
return [
'telegramBotToken' => '123456:AA...replace-me',
];

В качестве запасного варианта, init показания TELEGRAM_BOT_TOKEN из окружающей среды. В производстве это значение обычно поступает из секретного хранилища вашего хостинг-провайдера, а не из файла.

Использование компонента

С контроллера:

public function actionPing($chatId)
{
$telegram = Yii::$app->telegram;
$telegram->call('sendMessage', [
'chat_id' => (int)$chatId,
'text' => 'pong',
]);
return 'ok';
}

Из команды консоли, которая создает отведение перед отправкой:

public function actionLead($chatId)
{
$id = bin2hex(random_bytes(7));
Yii::$app->db->createCommand()
->insert('lead', ['id' => $id, 'chat_id' => $chatId, 'created_at' => time()])
->execute();

Yii::$app->telegram->call('sendMessage', [
'chat_id' => $chatId,
'text' => "Lead #{$id} created",
]);
}

Сначала вставьте строку, а затем отправьте. Таким образом, сбой Telegram не может создать фантомные лиды; если сообщение не удается, вызывающий абонент может повторить попытку и найти существующую запись.

Регистрация ошибок

Yii::предупреждение и Yii::error запись в сконфигурированную цель журнала. Для бота, который может работать в долгоживущих работниках, отправьте предупреждения в вращающийся файл и ошибки в ваш бэкэнд мониторинга. Минимальный лог Конфигурация компонента Vim

'log' => [
'targets' => [
[
'class' => \yii\log\FileTarget::class,
'levels' => ['warning', 'error'],
'logFile' => '@runtime/logs/telegram.log',
'maxFileSize' => 10 * 1024 * 1024,
'maxLogFiles' => 5,
],
[
'class' => \yii\log\SyslogTarget::class,
'levels' => ['error'],
],
],
],

Передайте массив в качестве второго аргумента Yii::error и Yii будет регистрировать его как структурированные данные, что проще grep, чем плоская строка.

Производственные заметки

- Идемпотентность для вебхуков. Обработчики веб-перехватчиков должны хранить update_id и пропускать дубликаты. Один курсор «последний обработанный идентификатор» небезопасен при перезагрузке; используйте уникальный индекс на update_id в базе данных, или SETNX в Redis. - Секретный код webhook-а. При регистрации веб-перехватчика через setWebhook, всегда проходить секретный токен. Проверьте его внутри контроллера перед разбором корпуса. - Экранирование HTML. Если вы установили parse_mode в HTML, запускать пользовательские значения через htmlspecialchars($value, ENT_QUOTES | ENT_REPLUTE, 'UTF-8'). Telegram строго относится к unescaped < и >. - Timeouts (Тайм-ауты) Пять секунд - это щедро для SendMessage; затяните до двух секунд для встроенных запросов, где пользователь ждет. - Повторные попытки и очереди. 429/5xx повторяет попытку внутри коротких ручек компонентов. Для длительных простоев переведите вызов в очередь (Yii2 имеет yii\queue\Queue интерфейсы) и позволить рабочему повторить попытку с экспоненциальным бэк-оффом и очередью мертвых букв. - httpclient против необработанного cURL. Компонент выше использует необработанный cURL для сохранения нулевых дополнительных зависимостей. Если вы уже зависите от yiisoft/yii2-httpclient вы можете заменить httpPostJson() где новый клиент(['transport' => 'yii\httpclient\CurlTransport'])->Post($url, $payload)->send() Читал. "statusCode": 0 и ДАННЫЕ. Логика повтора остается идентичной. - Проведение испытания. Ввести подделку TelegramClient в тестах; общественная поверхность просто call($method, $params), который легко заглушить.

Этот компонент намеренно мал: один публичный метод, один HTTP-путь, без кэширования, без SDK. Если проект нуждается в answerCallbackQuery, editMessageText, или встроенные клавиатуры, все они проходят через одну и ту же ВызовStencils с соответствующими массивами параметров, поэтому их добавление является вопросом документации, а не кода.

Если вам нужна справка по полному набору методов Bot API и их параметрам, официальная документация Telegram - правильная отправная точка.

Этот шаблон сервисного уровня - тот же, который мы используем при отгрузке производственных ботов. BotCreator - это студия, которая создает сквозных ботов Telegram и мини-приложения; приведенные выше шаблоны - это основа, которую мы повторно используем в разных проектах.

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

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