Разработка отказоустойчивого Telegram-бота на PHP в рамках фреймворка Yii2 требует строгого соблюдения двух главных правил Bot API: моментальный ответ HTTP 200 OK серверам Telegram и защита конечной точки (endpoint) от несанкционированного доступа. При высокой нагрузке или задержках на сторонних API (например, CRM или базы данных) обработка входящего запроса прямо во время HTTP-сессии приводит к таймаутам (6 секунд), повторным повторам со стороны Telegram и падению бота.
В этом гайде мы пошагово соберем высокопроизводительный контроллер вебхука на Yii2, отключим CSRF-валидацию для бота, авторизуем запрос через заголовок X-Telegram-Bot-Api-Secret-Token, защитимся от дублирования обновлений с помощью update_id и вынесем всю бизнес-логику в фоновые задачи Yii Queue.
1. Архитектура Webhook-контроллера и отключение CSRF
По умолчанию в Yii2 включена защита от Cross-Site Request Forgery (CSRF). Так как Telegram отправляет входящий POST-запрос с JSON-полезной нагрузкой без CSRF-токена фреймворка, первый шаг — отключить эту проверку для конкретного контроллера или действия. Кроме того, вебхук должен принимать только метод POST и работать через изолированный actionWebhook.
Для исключения задержек контроллер не должен выполнять сложную бизнес-логику. Его задача — распарсить тело запроса, проверить подлинность токена, убедиться в уникальности update_id, поставить задачу в очередь и вернуть HTTP 200.
2. Валидация Secret Token и сохранение update_id
При установке вебхука через метод setWebhook разработчик может передать параметр secret_token (строка от 1 до 256 символов). Telegram будет присылать этот токен в каждом POST-запросе в заголовке X-Telegram-Bot-Api-Secret-Token. Это защитит ваш бэкенд от подделки запросов злоумышленниками.
Вторая проблема — дублирование обновлений (retries). Если ваш сервер не ответил за отведенное время или произошел сбой сети, Telegram повторит запрос с тем же update_id. Чтобы избежать повторной обработки заказов или двойного списания бонусов, мы должны фиксировать update_id в базе данных (или в Redis) с использованием UNIQUE индекса.
<?php
namespace app\controllers;
use Yii;
use yii\web\Controller;
use yii\web\Response;
use app\jobs\ProcessTelegramUpdateJob;
class WebhookController extends Controller
{
// Отключаем CSRF-проверку для работы с внешним API Telegram
public $enableCsrfValidation = false;
public function actionIndex()
{
Yii::$app->response->format = Response::FORMAT_JSON;
$request = Yii::$app->request;
if (!$request->isPost) {
Yii::$app->response->statusCode = 405;
return ['ok' => false, 'error' => 'Method Not Allowed'];
}
// 1. Проверка X-Telegram-Bot-Api-Secret-Token
$secretToken = getenv('TELEGRAM_SECRET_TOKEN') ?: Yii::$app->params['telegramSecretToken'];
$incomingToken = $request->headers->get('X-Telegram-Bot-Api-Secret-Token');
if (empty($secretToken) || $incomingToken !== $secretToken) {
Yii::$app->response->statusCode = 403;
return ['ok' => false, 'error' => 'Access denied: invalid secret token'];
}
// 2. Чтение и валидация JSON-тела
$rawContent = $request->getRawBody();
$payload = json_decode($rawContent, true);
if (json_last_error() !== JSON_ERROR_NONE || !isset($payload['update_id'])) {
Yii::$app->response->statusCode = 400;
return ['ok' => false, 'error' => 'Invalid JSON payload'];
}
$updateId = (int)$payload['update_id'];
// 3. Защита от дублей (дедупликация по update_id)
if (!$this->registerUpdateId($updateId)) {
// Update уже получали ранее, отдаем 200 OK без повторной постановки в очередь
return ['ok' => true, 'status' => 'already_processed'];
}
// 4. Отправка апдейта на обработку в очередь Yii Queue
Yii::$app->queue->push(new ProcessTelegramUpdateJob([
'updatePayload' => $payload,
]));
// Возвращаем быстрый 200 OK
return ['ok' => true, 'status' => 'queued'];
}
/**
* Регистрирует update_id в атомарном хранилище.
* Возвращает false, если update_id уже существовал в БД.
*/
private function registerUpdateId(int $updateId): bool
{
$db = Yii::$app->db;
try {
$affected = $db->createCommand()
->insert('{{%telegram_processed_updates}}', [
'update_id' => $updateId,
'created_at' => date('Y-m-d H:i:s'),
])
->execute();
return $affected > 0;
} catch (\Exception $e) {
// В случае нарушения UNIQUE ограничения ловим исключение
return false;
}
}
}
3. Миграция для таблицы дедупликации update_id
Для корректной работы метода registerUpdateId потребуется структура таблицы в СУБД с первичным или уникальным ключом по полю update_id. Ниже представлена стандартная миграция Yii2.
<?php
use yii\db\Migration;
class m240101_000001_create_telegram_processed_updates_table extends Migration
{
public function safeUp()
{
$this->createTable('{{%telegram_processed_updates}}', [
'update_id' => $this->bigInteger()->notNull(),
'created_at' => $this->dateTime()->notNull(),
]);
$this->addPrimaryKey('pk_telegram_processed_updates', '{{%telegram_processed_updates}}', 'update_id');
}
public function safeDown()
{
$this->dropTable('{{%telegram_processed_updates}}');
}
}
4. Фоновая обработка через Yii Queue и безопасная отправка ответа
Полученные данные попадают в компонент Yii Queue (например, на базе Redis, RabbitMQ или DB). Очередь разбирается консольным воркером (php yii queue/listen). Внутри задачи происходит вся логика: обработка текстовых команд, запись лидов в CRM, генерация ответов.
Для взаимодействия с API Telegram на PHP используем чистый cURL без сторонних оберток с глубокой проверкой ошибок сети, статус-кодов HTTP и полей ответа ok: true.
<?php
namespace app\jobs;
use Yii;
use yii\base\BaseObject;
use yii\queue\JobInterface;
class ProcessTelegramUpdateJob extends BaseObject implements JobInterface
{
public array $updatePayload = [];
public function execute($queue)
{
$botToken = getenv('TELEGRAM_BOT_TOKEN') ?: Yii::$app->params['telegramBotToken'];
if (empty($botToken)) {
Yii::error('Telegram Token is missing in configuration', 'telegram');
return;
}
// Пример обработки входящего текстового сообщения
if (isset($this->updatePayload['message'])) {
$message = $this->updatePayload['message'];
$chatId = $message['chat']['id'] ?? null;
$text = trim($message['text'] ?? '');
if ($chatId && $text !== '') {
$this->handleMessage($botToken, $chatId, $text);
}
}
}
private function handleMessage(string $botToken, int $chatId, string $text): void
{
if (strpos($text, '/start') === 0) {
$replyText = "Здравствуйте! Ваш запрос передан в фоновый воркер и успешно обработан.";
} else {
$replyText = "Принято сообщение: <b>" . htmlspecialchars($text, ENT_QUOTES, 'UTF-8') . "</b>";
}
$this->sendTelegramApi($botToken, 'sendMessage', [
'chat_id' => $chatId,
'text' => $replyText,
'parse_mode' => 'HTML',
]);
}
/**
* Универсальный метод отправки запросов к Bot API через cURL
*/
private function sendTelegramApi(string $botToken, string $method, array $params): bool
{
$url = "https://api.telegram.org/bot{$botToken}/{$method}";
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($params),
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_CONNECTTIMEOUT => 5,
]);
$rawResponse = curl_exec($ch);
$curlErrno = curl_errno($ch);
$curlError = curl_error($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($curlErrno !== 0) {
Yii::error("cURL error ({$curlErrno}): {$curlError}", 'telegram');
return false;
}
if ($httpCode !== 200) {
Yii::error("Telegram API HTTP status {$httpCode}. Response: {$rawResponse}", 'telegram');
return false;
}
$response = json_decode($rawResponse, true);
if (json_last_error() !== JSON_ERROR_NONE || empty($response['ok'])) {
Yii::error("Telegram API returns error logic. Payload: {$rawResponse}", 'telegram');
return false;
}
return true;
}
}
5. Рекомендации по эксплуатации и безопасности
- Таймауты cURL: Всегда ограничивайте
CURLOPT_TIMEOUTиCURLOPT_CONNECTTIMEOUT, чтобы воркер очереди не зависал при проблемах с сетью. - Секретные ключи: Никогда не храните токены бота и
secret_tokenпрямо в коде. Используйте переменные окружения (.env) или компонент конфигурации Yii2 (Yii::$app->params). - Очистка старых update_id: Настройте периодическую консольную команду (Yii2 Console Runner / Cron) для удаления записей из
telegram_processed_updatesстарше 7 дней, чтобы таблица не разрасталась до миллионов строк. - Логирование ошибок: Настройте встроенные таргеты логов Yii2 (
yii\log\FileTarget) для категорииtelegram, чтобы оперативно узнавать об ошибках типа 403 Forbidden или 429 Too Many Requests.
Если вам требуется профессиональная помощь в создании архитектуры ботов, сложных интеграциях с CRM или сервисной поддержке, обращайтесь к специалистам компании BotCreator.