При разработке Telegram-ботов на фреймворке Yii2 разработчики часто сталкиваются с архитектурными проблемами. Telegram требует, чтобы сервер отвечал на webhook-запросы практически мгновенно (в течение 1-2 секунд). Если ваш код начинает выполнять тяжелые операции — отправку писем, запросы к сторонним CRM или сложные расчеты в БД — Telegram разрывает соединение по таймауту и начинает слать повторные запросы (retries). Это приводит к лавинообразному росту нагрузки и дублированию сообщений пользователям.
В этой статье мы построим профессиональный, отказоустойчивый Webhook-приемник на Yii2. Мы решим проблему валидации токенов, обойдем встроенную CSRF-защиту, реализуем строгую идемпотентность через сохранение update_id в базу данных и перенесем всю бизнес-логику в фоновую очередь с помощью расширения yii2-queue.
Шаг 1: База данных и обеспечение идемпотентности
Идемпотентность гарантирует, что один и тот же апдейт от Telegram не будет обработан дважды. Сетевые сбои — обычное дело: Telegram может отправить запрос, не дождаться ответа из-за секундного лага сети, посчитать доставку неудавшейся и прислать тот же самый update_id снова.
Для защиты от дублей создадим простую таблицу в базе данных, где первичным ключом будет выступать уникальный update_id. Используем стандартный механизм миграций Yii2.
use yii\db\Migration;
class m231024_120000_create_tg_updates_table extends Migration
{
public function safeUp()
{
$this->createTable("{{%tg_processed_update}}", [
"update_id" => $this->bigInteger()->notNull()->unsigned(),
"processed_at" => $this->timestamp()->defaultExpression("CURRENT_TIMESTAMP"),
]);
$this->addPrimaryKey("pk-tg_processed_update-update_id", "{{%tg_processed_update}}", "update_id");
}
public function safeDown()
{
$this->dropTable("{{%tg_processed_update}}");
}
}Попытка повторной вставки уже обработанного update_id вызовет ошибку нарушения уникальности ключа (Integrity Constraint Violation), которую мы легко перехватим в контроллере и вернем Telegram успешный статус 200 OK, предотвращая повторную обработку.
Шаг 2: Создание Webhook-контроллера с отключением CSRF
По умолчанию Yii2 защищает все POST-запросы с помощью CSRF-токенов. Поскольку запросы от Telegram приходят извне, Yii2 заблокирует их с ошибкой 400 Bad Request. Нам необходимо отключить валидацию CSRF именно для экшена вебхука.
Также мы добавим проверку секретного заголовка X-Telegram-Bot-Api-Secret-Token, который мы указываем при регистрации вебхука через метод setWebhook. Это гарантирует, что запросы на наш URL приходят действительно от серверов Telegram, а не от злоумышленников, узнавших адрес скрипта.
namespace app\controllers;
use Yii;
use yii\web\Controller;
use yii\web\BadRequestHttpException;
use yii\web\Response;
use app\queue\TelegramProcessorJob;
class TelegramController extends Controller
{
// Отключаем CSRF-валидацию для работы внешнего вебхука
public $enableCsrfValidation = false;
public function actionWebhook()
{
Yii::$app->response->format = Response::FORMAT_JSON;
$request = Yii::$app->request;
// Защита: проверяем <a href="/blog/telegram-webhook-pure-php-setup">секретный токен</a>, заданный при setWebhook
$expectedToken = Yii::$app->params["telegram_secret_token"] ?? null;
$receivedToken = $request->headers->get("X-Telegram-Bot-Api-Secret-Token");
if ($expectedToken && $receivedToken !== $expectedToken) {
Yii::warning("Попытка несанкционированного доступа к вебхуку", "telegram");
throw new BadRequestHttpException("Invalid secret token");
}
$rawBody = $request->getRawBody();
$update = json_decode($rawBody, true);
if (json_last_error() !== JSON_ERROR_NONE || !isset($update["update_id"])) {
return ["status" => "error", "message" => "Invalid JSON or missing update_id"];
}
$updateId = $update["update_id"];
// Проверяем идемпотентность через попытку вставки в базу данных
try {
Yii::$app->db->createCommand()
->insert("{{%tg_processed_update}}", ["update_id" => $updateId])
->execute();
} catch (\yii\db\Exception $e) {
// Если запись уже существует (код ошибки 23000 / 1062 дубликат)
if ($e->errorInfo[1] == 1062 || strpos($e->getMessage(), "23000") !== false) {
return ["status" => "ok", "message" => "Duplicate update ignored"];
}
throw $e;
}
// Отправляем задачу в очередь Yii Queue для асинхронной обработки
Yii::$app->queue->push(new TelegramProcessorJob([
"update" => $update,
]));
// Моментально отвечаем Telegram успехом
return ["status" => "ok"];
}
}Шаг 3: Асинхронная обработка событий через Yii Queue
Для фоновой обработки мы используем официальный компонент yiisoft/yii2-queue. Он позволяет складывать задачи в Redis, RabbitMQ, DB или Gearman и выполнять их в консольных воркерах демона.
Создадим класс задачи (Job), который будет отвечать за непосредственную обработку входящего сообщения и отправку ответа пользователю.
namespace app\queue;
use Yii;
use yii\base\BaseObject;
use yii\queue\JobInterface;
class TelegramProcessorJob extends BaseObject implements JobInterface
{
/** @var array Входящий массив данных от Telegram */
public $update;
public function execute($queue)
{
if (!isset($this->update["message"]["chat"]["id"])) {
return;
}
$chatId = $this->update["message"]["chat"]["id"];
$text = $this->update["message"]["text"] ?? "";
// Бизнес-логика бота
if (strpos($text, "/start") === 0) {
$this->sendMessage($chatId, "Привет! Ваша команда принята и обработана асинхронно через Yii Queue.");
}
}
private function sendMessage($chatId, $text)
{
$token = Yii::$app->params["telegram_bot_token"] ?? null;
if (!$token) {
Yii::error("Токен <a href="/blog/php-telegram-bot-getupdates-long-polling-local-dev">Telegram Bot API</a> не сконфигурирован", "telegram");
return;
}
$url = "https://api.telegram.org/bot{$token}/sendMessage";
$payload = json_encode([
"chat_id" => $chatId,
"text" => $text,
"parse_mode" => "HTML"
]);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Content-Type: application/json"]);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);
if ($curlError) {
Yii::error("Ошибка cURL при отправке в Telegram: {$curlError}", "telegram");
return;
}
if ($httpCode !== 200) {
Yii::error("Telegram API вернул код {$httpCode}. Ответ: {$response}", "telegram");
return;
}
$result = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE || !($result["ok"] ?? false)) {
Yii::error("Некорректный ответ Telegram API: " . ($result["description"] ?? "Unknown"), "telegram");
}
}
}Шаг 4: Конфигурация приложения
Для корректной работы системы добавьте параметры токенов в конфигурационный файл config/params.php:
return [
"telegram_bot_token" => getenv("TELEGRAM_BOT_TOKEN"),
"telegram_secret_token" => getenv("TELEGRAM_SECRET_TOKEN"), // Любая случайная строка
];Также убедитесь, что компонент очереди (queue) зарегистрирован в конфигурации консольного и веб-приложения (config/web.php и config/console.php):
"components" => [
"queue" => [
"class" => \yii\queue\db\Queue::class,
"db" => "db", // Компонент подключения к БД
"tableName" => "{{%queue}}", // Таблица очереди
"channel" => "telegram",
"mutex" => \yii\mutex\MysqlMutex::class,
],
],Типичные ошибки при интеграции вебхуков в Yii2
- Использование file_get_contents("php://input"): В Yii2 для получения сырого тела запроса всегда следует использовать метод
Yii::$app->request->getRawBody(). Он кэширует результат внутри фреймворка, что исключает проблемы с повторным чтением потока ввода. - Отсутствие таймаутов на cURL: Всегда явно задавайте
CURLOPT_TIMEOUTиCURLOPT_CONNECTTIMEOUT. Без них зависший запрос к API Telegram заблокирует воркер очереди, снижая общую пропускную способность системы. - Отправка ответа внутри HTTP-сессии контроллера: Никогда не выполняйте долгую логику внутри
actionWebhook. Telegram разорвет соединение, а пользователь получит дублированное сообщение, так как сервер вернет ошибку таймаута, и Telegram повторит попытку доставки.
Реализованная архитектура легко масштабируется: при росте нагрузки достаточно запустить несколько воркеров очереди с помощью супервизора (Supervisor) командой php yii queue/listen.
Если вам нужна профессиональная разработка сложных интеграций и ботов под ключ, обратитесь к специалистам BotCreator.