Керування Webhooks вручну через браузер або сторонні HTTP-клієнти є частим джерелом проблем у виробництві. Завдяки ручному налаштуванню легко забути про перенесення secret_token, пропустіть зміну URL-адреси, коли змінюється середовище, або втратьте контроль над накопиченими оновленнями. Єдино правильним підходом для комерційної розробки є передача сервісних завдань бота командам CLI фреймворку.
У цій статті ми обговоримо створення консольного контролера в Yii2, який відповідає за перевірку доступності API (health-check), безпечну реєстрацію та видалення Webhook, а також автоматичне очищення таблиць ідемпотентності та журналів оновлень.
Навіщо переносити обслуговування API Telegram на консоль Yii2
Використовуючи консольні команди Yii2 (класи, успадковані від yii\console\controller) дає відразу кілька інженерних переваг:
- Інтеграція конвеєра CI/CD: Під час кожного розгортання конвеєр може автоматично викликати перевірку здоров 'я та оновлювати URL-адресу Webhook з поточною
secret_token. - Одинарна конфігурація: Токени та параметри беруться з навколишнього середовища. (
getenv()АБОYii::$app->params, що виключає витік токенів в історію команд cURL. - Безпечно видаляйте накопичені повідомлення: Прапор
drop_pending_updatesпри скиданні вебхука, дозволяє очистити чергу накопичених повідомлень після збою сервера. - Автоматичне запобігання БД: Столи, в яких зберігаються
update_idщоб забезпечити ідемпотентність, вони з часом зростають до мільйонів рядків, що вимагає регулярного планового прибирання через Cron.
Реалізація контролера CLI: getMe, setWebhook та deleteWebhook
Створити консольный контроллер commands/TelegramController.php. Для взаємодії з Telegram Bot API використовуйте чистий cURL з обов 'язковою валідацією http коду відповіді, верифікацією json_last_error та поля Гаразд в тілі реагування.
<?php
namespace app\commands;
use Yii;
use yii\console\Controller;
use yii\console\ExitCode;
class TelegramController extends Controller
{
/**
* Отправка POST-запроса к Telegram Bot API через cURL
*/
private function sendApiRequest(string $method, array $params = []): array
{
$token = getenv('TELEGRAM_BOT_TOKEN') ?: (Yii::$app->params['telegramBotToken'] ?? null);
if (!$token) {
$this->stderr("Ошибка: Не задан TELEGRAM_BOT_TOKEN в getenv() или params.php\n");
return ['ok' => false, 'error' => 'Missing bot token'];
}
$url = "https://api.telegram.org/bot{$token}/{$method}";
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($params),
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_TIMEOUT => 10,
CURLOPT_CONNECTTIMEOUT => 5,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);
if ($response === false) {
$this->stderr("cURL Error: {$curlError}\n");
return ['ok' => false, 'error' => $curlError];
}
$data = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
$this->stderr("JSON Error: " . json_last_error_msg() . "\n");
return ['ok' => false, 'error' => 'Invalid JSON response'];
}
if ($httpCode !== 200 || empty($data['ok'])) {
$description = $data['description'] ?? 'Unknown API error';
$this->stderr("Telegram API Error [HTTP {$httpCode}]: {$description}\n");
}
return $data;
}
/**
* Health-check доступности бота (getMe)
*/
public function actionHealth(): int
{
$res = $this->sendApiRequest('getMe');
if (!empty($res['ok'])) {
$botUsername = $res['result']['username'] ?? 'unknown_bot';
$botName = $res['result']['first_name'] ?? 'Bot';
$this->stdout("Health-check пройден успешно. Имя: {$botName} (@{$botUsername})\n");
return ExitCode::OK;
}
$this->stderr("Health-check провален! Проверьте валидность токена.\n");
return ExitCode::UNSPECIFIED_ERROR;
}
/**
* Установка Webhook с привязкой Secret Token
*/
public function actionSetWebhook(string $url): int
{
$secretToken = getenv('TELEGRAM_SECRET_TOKEN') ?: (Yii::$app->params['telegramSecretToken'] ?? '');
$params = [
'url' => $url,
'allowed_updates' => ['message', 'callback_query', 'my_chat_member'],
];
if (!empty($secretToken)) {
$params['secret_token'] = $secretToken;
}
$res = $this->sendApiRequest('setWebhook', $params);
if (!empty($res['ok'])) {
$this->stdout("Webhook успешно зарегистрирован на URL: {$url}\n");
return ExitCode::OK;
}
return ExitCode::UNSPECIFIED_ERROR;
}
/**
* Удаление Webhook с опциональным сбросом очереди накопившихся апдейтов
*/
public function actionDeleteWebhook(bool $dropPending = false): int
{
$params = ['drop_pending_updates' => $dropPending];
$res = $this->sendApiRequest('deleteWebhook', $params);
if (!empty($res['ok'])) {
$status = $dropPending ? "с очисткой входящей очереди" : "без очистки очереди";
$this->stdout("Webhook успешно удален ({$status}).\n");
return ExitCode::OK;
}
return ExitCode::UNSPECIFIED_ERROR;
}
}Обертання журналів оновлень та очищення таблиць ідемпотентності
При отриманні оновлення від Telegram до контролера Webhook, його update_id зберігається в базі даних (наприклад, в таблиці telegram_update_log), щоб уникнути дублювання обробки. З часом ця таблиця розростається до сотень тисяч записів, займаючи дисковий простір і знижуючи швидкість пошуку індексу.
Додамо метод до нашого консольного контролера для фонового очищення застарілих записів, зберігаючи лише дані за останні N днів.
/**
* Очистка старых логов входящих апдейтов (для поддержания идемпотентности)
* Пример вызова: php yii telegram/clean-logs 14
*/
public function actionCleanLogs(int $days = 14): int
{
if ($days < 1) {
$this->stderr("Параметр days должен быть больше или равен 1.\n");
return ExitCode::DATAERR;
}
$thresholdDate = date('Y-m-d H:i:s', strtotime("-{$days} days"));
try {
$deletedCount = Yii::$app->db->createCommand()
->delete('{{%telegram_update_log}}', ['<', 'created_at', $thresholdDate])
->execute();
$this->stdout("Успешно удалено {$deletedCount} записей update_id старше {$days} дней ({$thresholdDate}).\n");
return ExitCode::OK;
} catch (\Exception $e) {
$this->stderr("Ошибка при выполнении очистки БД: " . $e->getMessage() . "\n");
return ExitCode::UNSPECIFIED_ERROR;
}
}Автоматизація: використання в CI/CD та Crontab
Розроблена консольна команда може бути легко реалізована в скрипті розгортання (наприклад, в скрипті bash при розгортанні нової версії програми) та в системному планувальнику завдань Linux (crontab).
# 1. Скрипт деплоя приложения (deploy.sh)
#!/usr/bin/env bash
set -e
APP_ENV="prod"
WEBHOOK_URL="https://bot.example.com/telegram/webhook"
echo "Проверка подключения к Telegram API..."
php yii telegram/health
echo "Регистрация целевого Webhook..."
php yii telegram/set-webhook "$WEBHOOK_URL"
# 2. Задача в /etc/cron.d/telegram-bot
# Запуск очистки устаревших логов update_id каждое воскресенье в 03:00 ночи (хранить 14 дней)
0 3 * * 0 www-data /usr/bin/php /var/www/app/yii telegram/clean-logs 14 >> /var/www/app/runtime/logs/cron-telegram.log 2>&1Обробка помилок і типові підводні камені
Використовуючи команди CLI в середовищі продукту, враховуйте наступні нюанси:
- Коди повернення (ExitCode): Команди повинні повернутися
ExitCode::OK(0) щодо кодів успіху та ненульових кодів (наприклад,ExitCode::unspecified_ERRORвід кулі1) при збоях. Це вимагає, щоб сценарії CI/CD переривали збирання в той час, коли виникають помилки API. - Секретний токен: Переданий параметр
secret_tokenпри викликуsetWebhookмає містити від 1 до 256 символів (дозволені символиA-Z,a-z,0-9,_і-). Підтвердьте назвуX-Telegram-Bot-Api-Secret-Tokenпри отриманні запитів на стороні веб-контролера. - Відкинути оновлення, що очікують на розгляд: Будьте обережні з прапором
dropPending = trueпри видаленні або перевстановленні веб-гачка. Використовуйте його лише тоді, коли сталася помилка, а накопичена черга застарілих команд може поставити вашу бізнес-логіку або призвести до дублювання дій.
Якщо вам потрібна кваліфікована допомога у розробці архітектури бота з високим навантаженням або інтеграції веб-перехоплювачів із CRM-системами, зверніться до BotCreator.