Managing a Webhook manually via a browser or third-party HTTP clients is a frequent source of production issues. With manual configuration, it is easy to forget to pass secret_token, miss changing the URL when switching environments, or lose control over accumulated pending updates. The only correct approach for commercial development is to move the bot's service tasks into framework CLI commands.
In this article, we will cover creating a console controller in Yii2 responsible for API health checks, secure registration and removal of Webhooks, as well as automatic cleanup of idempotency tables and update logs.
Why move Telegram API maintenance to the Yii2 console
Using Yii2 console commands (classes extending yii\console\Controller) provides several engineering advantages at once:
- Integration into CI/CD pipelines: On every deploy, the pipeline can automatically call the health check and update the Webhook URL with the actual
secret_token. - Single configuration: Tokens and parameters are pulled from the environment (
getenv()) orYii::$app->params, eliminating token leaks into cURL command history. - Safe deletion of accumulated messages: The
drop_pending_updatesflag when resetting the webhook allows clearing the queue of accumulated messages after a server failure. - Automatic database maintenance: Tables storing
update_idfor idempotency grow over time to millions of rows, requiring regular scheduled cleanup via Cron.
CLI Controller Implementation: getMe, setWebhook, and deleteWebhook
Let's create консольный контроллер commands/TelegramController.php. To interact with the Telegram Bot API, we use pure cURL with mandatory HTTP status code validation, checks for json_last_error, and the ok field in the response body.
<?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;
}
}Update log rotation and idempotency table cleanup
When an update is received from Telegram at the Webhook controller, its update_id is saved to the database (for example, in the telegram_update_log table) to prevent duplicate processing. Over time, this table grows to hundreds of thousands of records, consuming disk space and reducing index search performance.
Let's add a method to our console controller for background cleanup of outdated records, retaining only data for the last N days.
/**
* Очистка старых логов входящих апдейтов (для поддержания идемпотентности)
* Пример вызова: 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;
}
}Automation: Usage in CI/CD and Crontab
The developed console command can easily be integrated into a deployment script (for example, in a bash script when deploying a new app version) and into the Linux system task scheduler (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>&1Error handling and common pitfalls
When using CLI commands in a production environment, keep the following nuances in mind:
- Exit codes (ExitCode): Commands should return
ExitCode::OK(0) on success and non-zero codes (for example,ExitCode::UNSPECIFIED_ERRORor1) on failures. This is necessary so that CI/CD scripts fail the build in a timely manner when API errors occur. - Secret token (Secret Token): The
secret_tokenparameter passed when callingsetWebhookmust contain 1 to 256 characters (allowed characters areA-Z,a-z,0-9,_, and-). Validate theX-Telegram-Bot-Api-Secret-Tokenheader when receiving requests on the web controller side. - Drop pending updates: Be careful with the
dropPending = trueflag when deleting or reinstalling a webhook. Use it only when an outage has occurred and the accumulated queue of outdated commands could crash your business logic or lead to duplicate actions.
If you need qualified assistance in designing a high-load bot architecture or integrating webhooks with CRM systems, contact the experts at BotCreator.