Управление Webhook вручную через браузер или сторонние HTTP-клиенты — частый источник проблем в продакшене. При ручной настройке легко забыть передать secret_token, пропустить смену URL при смене окружения или потерять контроль над зависшими накопившимися апдейтами. Единственный верный подход для коммерческой разработки — выносить сервисные задачи бота в CLI-команды фреймворка.
В этой статье мы разберем создание консольного контроллера в Yii2, который отвечает за проверку доступности API (health-check), безопасную регистрацию и удаление Webhook, а также автоматическую очистку таблиц идемпотентности и логов апдейтов.
Зачем выносить обслуживание Telegram API в консоль Yii2
Использование консольных команд Yii2 (классы, наследуемые от yii\console\Controller) дает сразу несколько инженерных преимуществ:
- Интеграция в CI/CD pipeline: При каждом деплое пайплайн может автоматически вызывать health-check и обновлять Webhook URL с актуальным
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 и поля ok в теле ответа.
<?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): Передаваемый параметр
secret_tokenпри вызовеsetWebhookдолжен содержать от 1 до 256 символов (допустимы символыA-Z,a-z,0-9,_и-). Валидируйте заголовокX-Telegram-Bot-Api-Secret-Tokenпри приеме запросов на стороне веб-контроллера. - Drop pending updates: Будьте осторожны с флагом
dropPending = trueпри удалении или переустановке вебхука. Используйте его только тогда, когда произошел сбой и накопившаяся очередь устаревших команд может положить ваш бизнес-логику или привести к дублированию действий.
Если вам необходима квалифицированная помощь в проектировании высоконагруженной архитектуры бота или интеграции вебхуков с CRM-системами, обратитесь к специалистам компании BotCreator.