ЧТО МЫ МОЖЕМ СОЗДАТЬ
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 и мини-приложения; приведенные выше шаблоны - это основа, которую мы повторно используем в разных проектах.