Створіть клієнтський компонент Telegram для Yii2: DI, Token через params, 429 Retries, Error Logging

ЩО МИ МОЖЕМО СТВОРИТИ

Призначений для багаторазового використання 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. Він розкриває один публічний метод, виклик($method, $params), а також декілька налаштовуваних параметрів: значення тайм-ауту, максимальна кількість повторних спроб та маркер бота. Ми реєструємо його як компонент програми, тому контейнер Yii DI дає нам налаштований екземпляр, куди б ми його не ввели - підказка.

<?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.or g';
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 використання обробки Перез' єднуватись після: поле повернуто в параметри. 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 реєструватиме його як структуровані дані, які легше зібрати, ніж плоский рядок.

Виробничі примітки

- Ідемпотентність для вебхуків. Обробники вебхуків повинні зберігати update_id і пропускати дублікати. Один курсор «останній оброблений ідентифікатор» небезпечний при перезавантаженні; використовуйте унікальний індекс на update_id в базі даних, або SETNX в Redis. - Секретний код вебхука. При реєстрації вебхука через setWebhook, завжди проходити секретный токен. Перевірте його всередині контролера, перш ніж розбирати корпус. - Уникнення HTML. Якщо ви встановили parse_mode в HTML запустіть користувацькі значення через htmlspecialchars($value, ENT_QUOTES | ENT_REPLACE, 'UTF-8'). Telegram суворо посилається на unescaped < і >. - Тайм-аути П 'ять секунд - це щедро для SendMessage; перетягніть до двох секунд для вбудованих запитів, де користувач чекає. - Повторні спроби та черги. 429/5xx повторює спробу всередині коротких компонентних ручок. Для тривалого простою поставте дзвінок у чергу (Yii2 має yii\queue\Черга інтерфейси) і дозволяють працівнику повторити спробу з експоненційним відключенням та чергою з мертвою буквою. - httpclient vs. raw cURL. Компонент вище використовує необроблений cURL для підтримки нульових додаткових залежностей. Якщо ви вже залежите від yiisoft/yii2-httpclient ви можете замінити httpPostJson() де новий клієнт(['transport' => 'yii\httpclient\CurlTransport'])->допис($url, $payload)->відправити() - Читал? "statusCode": 0 і ДАНІ. Логіка повторення залишається ідентичною. - Тестування. Запровадити підробку TelegramClient в тестах; загальнодоступна поверхня просто виклик($method, $params), який легко приглушити.

Цей компонент навмисно невеликий: один загальнодоступний метод, один HTTP-шлях, відсутність кешування, відсутність SDK. Якщо проект потребує answerCallbackQuery, editMessageTextабо вбудовані клавіатури, всі вони проходять через один і той же викликStencils з відповідними масивами параметрів, тому їх додавання є питанням документації, а не коду.

Якщо вам потрібна допомога з повним набором методів Bot API та їх параметрами, офіційна документація Telegram - правильна відправна точка.

Цей шаблон рівня обслуговування є таким самим, який ми використовуємо при відправці роботів для виробництва. BotCreator - це студія, яка створює наскрізні Telegram-боти та віджети; наведені вище шаблони є основою, яку ми повторно використовуємо в різних проєктах.

Нові статті — у Telegram

Розбираємо, що автоматизувати в бізнесі та як це працює на практиці. Без спаму.