При высокой нагрузке или нестабильной работе внешних сервисов синхронная обработка входящих запросов от Telegram Bot API неизбежно приводит к сбоям. Если ваш сервер не ответит кодом HTTP 200 OK в течение нескольких секунд, Telegram начнет повторно отправлять те же самые события (Update). Это создает лавинообразный рост нагрузки и повторные дублирующие действия в вашей системе.
В этом руководстве мы разберем создание архитектурно правильного контроллера на Yii2, который мгновенно принимает Webhook, проверяет подлинность заголовков, блокирует повторные запросы по update_id и передает обработку задачи в фоновое окружение через компонент yii2-queue.
1. Почему важно отвечать 200 OK мгновенно
Telegram Bot API ожидает от вебхука быстрый ответ с кодом 200 OK. Если время ожидания превышает допустимый таймаут, сервер Telegram считает доставку неудачной. В результате повторных попыток ваш бот может несколько раз записать одного и того же клиента в базу или отправить ему дублирующие уведомления.
Чтобы этого избежать, контроллер должен выполнять минимум работы: проверить секретный токен, зарегистрировать факт получения update_id, положить сырые данные массива в очередь и сразу вернуть статус успешного приема.
2. Отключение CSRF и валидация X-Telegram-Bot-Api-Secret-Token
По умолчанию yii\web\Controller проверяет CSRF-токен для всех POST-запросов. Telegram не знает ничего про Yii2 CSRF cookie, поэтому валидацию нужно выключить для action-вебхука. Для защиты от поддельных запросов используется механизм secret_token, передаваемый при настройке setWebhook.
Сервер Telegram отправляет этот секрет в заголовке X-Telegram-Bot-Api-Secret-Token. Сравнение строк необходимо проводить через бета-безопасную функцию hash_equals() для исключения атак по времени (timing attacks).
3. Контроллер WebhookController для Yii2
Ниже представлен полноценный код контроллера. Мы отключаем CSRF, валидируем заголовок в методе beforeAction, проверяем уникальность update_id в базе данных и отправляем задачу в очередь.
<?php
namespace app\controllers;
use Yii;
use yii\web\Controller;
use yii\web\BadRequestHttpException;
use yii\web\Response;
use app\jobs\ProcessTelegramUpdateJob;
class WebhookController extends Controller
{
public $enableCsrfValidation = false;
public function beforeAction($action)
{
if ($action->id === 'index') {
$secret = getenv('TELEGRAM_SECRET_TOKEN') ?: (Yii::$app->params['telegram_secret_token'] ?? null);
$headerToken = Yii::$app->request->headers->get('X-Telegram-Bot-Api-Secret-Token');
if (!$secret || !$headerToken || !hash_equals($secret, (string)$headerToken)) {
Yii::warning('Невалидный X-Telegram-Bot-Api-Secret-Token', 'telegram');
throw new BadRequestHttpException('Access denied');
}
}
return parent::beforeAction($action);
}
public function actionIndex()
{
Yii::$app->response->format = Response::FORMAT_JSON;
$rawBody = Yii::$app->request->getRawBody();
$update = json_decode($rawBody, true);
if (!$update || !isset($update['update_id'])) {
return ['status' => 'error', 'message' => 'Invalid payload'];
}
$updateId = (int)$update['update_id'];
$db = Yii::$app->db;
// Проверка идемпотентности через атомарную вставку или выборку
$exists = $db->createCommand('SELECT 1 FROM {{%telegram_updates}} WHERE update_id = :id', [
':id' => $updateId,
])->queryScalar();
if ($exists) {
return ['status' => 'ok', 'message' => 'Duplicate update'];
}
$db->createCommand()->insert('{{%telegram_updates}}', [
'update_id' => $updateId,
'created_at' => date('Y-m-d H:i:s'),
])->execute();
// Отправка в Yii Queue
Yii::$app->queue->push(new ProcessTelegramUpdateJob([
'update' => $update,
]));
return ['status' => 'ok'];
}
}
4. Фоновая обработка апдейта через Yii Queue Job
Задача очереди извлекает тело апдейта, анализирует входящий текст или команды, выполняет бизнес-логику (например, генерацию лида) и формирует ответ обратно пользователю. Ниже пример класса ProcessTelegramUpdateJob.
<?php
namespace app\jobs;
use Yii;
use yii\base\BaseObject;
use yii\queue\JobInterface;
use app\components\TelegramSender;
class ProcessTelegramUpdateJob extends BaseObject implements JobInterface
{
public array $update = [];
public function execute($queue)
{
if (!isset($this->update['message'])) {
return;
}
$message = $this->update['message'];
$chatId = $message['chat']['id'] ?? null;
$text = trim($message['text'] ?? '');
if (!$chatId) {
return;
}
if (str_starts_with($text, '/start')) {
$leadId = bin2hex(random_bytes(7));
Yii::$app->db->createCommand()->insert('{{%leads}}', [
'lead_id' => $leadId,
'chat_id' => $chatId,
'status' => 'new',
'created_at' => date('Y-m-d H:i:s'),
])->execute();
$sender = new TelegramSender();
$sender->sendMessage([
'chat_id' => $chatId,
'text' => "Заявка принята! Ваш номер лида: <strong>{$leadId}</strong>",
'parse_mode' => 'HTML',
]);
}
}
}
5. Сервис отправки сообщений через cURL в Telegram API
Для взаимодействия с API Telegram важно использовать чистое cURL-подключение с проверками HTTP-кода ответа, ошибок JSON-декодирования и структуры ok: true.
<?php
namespace app\components;
use Yii;
use RuntimeException;
class TelegramSender
{
private string $token;
public function __construct()
{
$this->token = (string)(getenv('TELEGRAM_BOT_TOKEN') ?: (Yii::$app->params['telegram_bot_token'] ?? ''));
if (empty($this->token)) {
throw new RuntimeException('Не задан TELEGRAM_BOT_TOKEN');
}
}
public function sendMessage(array $params): array
{
return $this->request('sendMessage', $params);
}
public function request(string $method, array $params = []): array
{
$url = 'https://api.telegram.org/bot' . $this->token . '/' . $method;
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($params),
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_CONNECTTIMEOUT => 5,
]);
$response = curl_exec($ch);
$curlError = curl_error($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($response === false) {
throw new RuntimeException('Ошибка cURL при запросе к Telegram: ' . $curlError);
}
$data = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new RuntimeException('Невалидный JSON ответа от Telegram API');
}
if ($httpCode !== 200 || !isset($data['ok']) || $data['ok'] !== true) {
$description = $data['description'] ?? 'Неизвестная ошибка API';
Yii::error("Telegram API Error [{$httpCode}]: {$description}", 'telegram');
throw new RuntimeException("Telegram API Error: {$description}");
}
return $data['result'];
}
}
6. Типичные ошибки при реализации Webhook в Yii2
- Забытый return true в beforeAction: Если не вернуть результат
parent::beforeAction($action), выполнение контроллера прервется и action не вызовется. - Хардкод токенов: Токен бота и секретный ключ должны передаваться через переменные окружения (
getenv()) или изолированный конфигурационный файлparams-local.php. - Синхронные внешние запросы: Обращение к сторонним CRM или AI-сервисам прямо в теле контроллера неизбежно вызывает таймауты и заставляет Telegram отправлять дубли сообщений.
Команда BotCreator разработает для вашего бизнеса отказоустойчивого Telegram-бота любой сложности с полной интеграцией во внутренние системы компании.