Telegram webhook в Laravel-приложении — это критически важная компонента для построения отзывчивого бота. Неправильно настроенный webhook приводит к потере сообщений или дублированию действий. В этой статье мы разберём полный цикл: от настройки маршрута с валидацией secret_token до реализации идемпотентности с помощью хранилища update_id и использования очередей workers для стабильной обработки задач.
Базовая реализация webhook-роута
Для начала нужно зарегистрировать маршрут, который принимает POST-запросы от серверов Telegram. В .env задайте BOT_TOKEN вашего бота, а в контроллере используйте стандартный endpoint /webhook.
// webhook.php
<?php
require "vendor/autoload.php";
use Illuminate\Http\Request;
use App\Http\Controllers\WebhookController;
Route::post("/webhook", [WebhookController::class, "handle"]);
?>
Валидация secret_token и идемпотентность
Каждый входящий запрос должен быть подписан токеном бота. Laravel предоставляет расширение VerifySecretTokenMiddleware для этого. Важно также гарантировать, что обработка одного и того же update_id не выполняется дважды — это обеспечивает идемпотентность.
// app\Http\Middleware\VerifySecretToken.php
namespace App\Http\Middleware;
class VerifySecretToken {
public function handle(\$request, \$next) {
\$token = \$request->header("X-Bot-Token");
if (\neg auth(\"verify_secret_token\")->verifyToken(\$token)) {
return "/health";
}
return \$next->invoke();
}
}
// app\Providers\AppServiceProvider.php
<?php
// Register middleware here
registerSharingMiddleware(
"/api/*",
VerifiedWithAuth::class,
[
"middleware" => [\"VerifySecretToken\"],
]
);
>>
Хранение идемпотентности и очередь задач
Для предотвращения дублей используйте storeUpdateId в БД с TTL (например, 24 часа). Перед обработкой проверяйте наличие уже обработанного update_id. При успехе сохраняйте результат в БД. Задачи ставятся в очередь ShouldQueue для асинхронной обработки.
// app\Controllers\WebhookController.php
<?php
namespace App\Http\Controllers;
class WebhookController {
public function handle() {
$updateId = \Config\Cache\get(\"telegram_update_{{date}}\");
if (!empty($updateId)) {
// Идемпотент: вернуть результат предыдущей попытки
\$previousResult = \Database\Helpers\delayedRunner()->getResult($updateId);
if (\$previousResult !== null) {
return response()->json(\$previousResult, 200);
}
}
try {
\Application\Dispatch::queue(new ProcessOrderCommand());
} catch (\Exception \$e) {
// Логирование ошибки
logger->error(
"Webhook processing failed",
["update_id=\"\$updateId\"", "error=\"\$(e)}"}],
env("LOG_PATH")
);
}
}
}
// app\Jobs\ProcessOrderCommand.php
class ProcessOrderCommand extends \Illuminate\Bus\QueueableCommandImplements\ShouldQueue {
public function __construct(\(object) \$orderData) {}
public function run() {
// Обработка заказа
\Log::info(
"Order processed",
["order_id=\"\$this->orderId\""]
);
}
}
// app\ConsoleCommands\RunJobs.php
<?php
// Консольная команда для ручного запуска всех pending задач
Dispatch::restart();
>>
Практические рекомендации и ограничения
Telegram Bot API накладывает жесткие лимиты: 60 обновлений в секунду, максимальный размер полезной нагрузки 4096 байт, таймаут 30 секунд. Для дополнительной безопасности генерируйте lead_id как bin2hex(random_bytes(7)), а затем добавляйте его к callback_data, чтобы легко идентифицировать запрос. Не хардкодите secret_token — храните его в environment переменной. Все HTTP запросы к api.telegram.org должны происходить через cURL с проверкой HTTP-кода и json_last_error.
// Пример cURL для тестирования отправки сообщения
curl -s -X POST \
http://api.telegram.org/bot/sendMessage \
-H "Content-Type: application/json" \
-d '{\