Developing a fault-tolerant Telegram-бот in PHP within the Yii2 framework requires strict adherence to two main rules of the Bot API: an immediate HTTP 200 OK response to Telegram servers and securing the endpoint against unauthorized access. Under high load or delays from third-party APIs (such as CRMs or databases), processing an incoming request directly during the HTTP session leads to timeouts (6 seconds), retries from Telegram, and bot crashes.
In this guide, we will build a high-performance webhook controller in Yii2 step by step, disable CSRF validation for the bot, authorize requests via the X-Telegram-Bot-Api-Secret-Token header, protect against duplicate updates using update_id, and offload all business logic into Yii Queue background jobs.
1. Webhook Controller Architecture and Disabling CSRF
By default, Yii2 enables Cross-Site Request Forgery (CSRF) protection. Since Telegram sends an incoming POST request with a JSON payload without the framework's CSRF token, the first step is to disable this validation for the specific controller or action. Additionally, the webhook should only accept POST requests and operate through an isolated actionWebhook.
To prevent delays, the controller should not execute complex business logic. Its job is to parse the request body, verify token authenticity, ensure the uniqueness of update_id, push a task to the queue, and return HTTP 200.
2. Secret Token Validation and Saving update_id
When setting up a webhook using the setWebhook method, the developer can pass the secret_token parameter (a string of 1 to 256 characters). Telegram will send this token in every POST request in the X-Telegram-Bot-Api-Secret-Token header. This protects your backend from request spoofing by malicious actors.
The second issue is update duplication (retries). If your server fails to respond within the allotted time or a network failure occurs, Telegram will retry the request with the same update_id. To avoid re-processing orders or double-deducting bonus points, we must log update_id in the database (or Redis) using a UNIQUE index.
<?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. Migration for the update_id Deduplication Table
For the registerUpdateId method to work correctly, you need a DBMS table structure with a primary or unique key on the update_id field. Below is a standard Yii2 migration.
<?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. Background Processing via Yii Queue and Safe Response Delivery
The received data is pushed to the Yii Queue component (e.g., based on Redis, RabbitMQ, or DB). The queue is processed by a console worker (php yii queue/listen). Inside the task, all the logic occurs: processing text commands, recording leads in a CRM, generating responses.
To interact with the Telegram API in PHP, we use plain cURL without third-party wrappers, featuring thorough checks for network errors, HTTP status codes, and ok: true response fields.
<?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. Operational and Security Recommendations
- Таймауты cURL: Always limit
CURLOPT_TIMEOUTandCURLOPT_CONNECTTIMEOUTso that the queue worker does not hang on network issues. - Secret Keys: Never store bot tokens and
secret_tokendirectly in the code. Use переменные окружения (.env) or the Yii2 configuration component (Yii::$app->params). - Cleaning up old update_ids: Set up a periodic console command (Yii2 Console Runner / Cron) to delete records from
telegram_processed_updatesolder than 7 days so the table does not grow to millions of rows. - Логирование ошибок: Configure built-in Yii2 log targets (
yii\log\FileTarget) for thetelegramcategory to promptly stay informed about errors such as 403 Forbidden or 429 Too Many Requests.
If you need professional assistance in designing bot architecture, complex CRM integrations, or ongoing service support, feel free to contact the specialists at BotCreator.