При високому навантаженні або нестабільній роботі зовнішніх сервісів синхронна обробка вхідних запитів від 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-бот а будь-якої складності з повною інтеграцією у внутрішні системи компанії.