Сценарий и требования
Заказ сохранён → задание в очереди telegram → worker → BotGate → Telegram
Пользовательский HTTP-запрос отвечает за сохранение заказа и постановку задания. Отдельный процесс worker выполняет сетевую отправку. Так недоступность Telegram не удерживает запрос оформления заказа до истечения сетевого таймаута.
Пример рассчитан на Laravel 11–13. Для Laravel 11/12 нужен PHP 8.2+, для Laravel 13 — PHP 8.3+. Для работы без Laravel есть отдельное руководство по независимому PHP SDK.
Нужны API-ключ BotGate, публичный ID активного бота, ID тестового чата и настроенное соединение очереди. Пример использует database queue: таблицы заданий и неудачных заданий должны быть подготовлены обычными миграциями вашего проекта. Режим sync для этого сценария не подходит — он выполнит работу внутри текущего запроса.
1. Установите пакет и задайте конфигурацию
В каталоге клиентского Laravel-приложения:
composer require botgate/laravel php artisan vendor:publish --tag=botgate-config
Composer подберёт совместимую версию пакета и автоматически установит botgate/sdk как зависимость — отдельно добавлять SDK не нужно. Сохраните composer.lock, чтобы окружения проекта использовали одинаковые версии.
Провайдер пакета регистрируется через auto-discovery. Он связывает SDK-клиент с контейнером Laravel, поэтому его можно получить через dependency injection.
Задайте значения в окружении приложения. Ниже только placeholders:
BOTGATE_API_KEY=your_botgate_api_key BOTGATE_BOT_ID=bot_your_public_id TELEGRAM_CHAT_ID=your_test_chat_id BOTGATE_TIMEOUT=20 BOTGATE_RETRY_MAX=0 QUEUE_CONNECTION=database
BOTGATE_RETRY_MAX=0 отключает встроенные HTTP-повторы SDK. Значение BOTGATE_TIMEOUT=20 — пример для коротких текстовых запросов. Для файлов и других длительных операций время ожидания нужно подбирать отдельно.
Публичный ID бота и ID чата — настройки нашего приложения, а не готовые параметры пакета. Добавьте секцию в массив config/services.php, сохранив остальные секции:
'telegram_alerts' => [
'bot_id' => env('BOTGATE_BOT_ID'),
'chat_id' => env('TELEGRAM_CHAT_ID'),
],
В рабочем коде далее используйте config(), а не прямые вызовы env(). При публикации учитывайте кэш конфигурации и долгоживущие процессы: уже запущенный worker не перечитывает настройки на каждое задание. Обновление конфигурации и перезапуск процессов выполняются по процедуре развёртывания вашего проекта.
2. Создайте задание с ограниченными повторами
Создайте app/Jobs/SendTelegramNotification.php. PHP-файл начинается с <?php; в блоке этот тег опущен.
declare(strict_types=1);
namespace App\Jobs;
use BotGate\Client;
use BotGate\Exception\BotGateException;
use BotGate\Exception\RateLimitException;
use BotGate\Exception\TelegramException;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
use RuntimeException;
final class SendTelegramNotification implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 4;
public int $maxExceptions = 1;
public int $timeout = 40;
public bool $failOnTimeout = true;
public function __construct(
public readonly string $notificationId,
public readonly string $botId,
public readonly string $chatId,
public readonly string $text,
) {}
public function handle(Client $client): void
{
try {
$client->bot($this->botId)->call('sendMessage', [
'chat_id' => $this->chatId,
'text' => $this->text,
]);
} catch (RateLimitException $error) {
$this->releaseAfterLimit($error->retryAfter(), 'botgate');
return;
} catch (TelegramException $error) {
if ($error->telegramErrorCode() === 429) {
$this->releaseAfterLimit($error->retryAfter(), 'telegram');
} else {
Log::warning('Telegram rejected notification', [
'notification_id' => $this->notificationId,
'telegram_error_code' => $error->telegramErrorCode(),
]);
$this->fail(new RuntimeException('Telegram отклонил запрос: проверьте параметры и права бота.'));
}
return;
} catch (BotGateException $error) {
Log::warning('Telegram notification stopped', [
'notification_id' => $this->notificationId,
'http_status' => $error->httpStatusCode(),
]);
$this->fail(new RuntimeException('Отправка остановлена; проверьте результат перед повтором.'));
return;
}
Log::info('Telegram notification sent', [
'notification_id' => $this->notificationId,
]);
}
private function releaseAfterLimit(?int $wait, string $source): void
{
Log::notice('Telegram notification rate limited', [
'notification_id' => $this->notificationId,
'source' => $source,
'retry_after' => $wait,
]);
if ($wait === null || $wait < 0) {
$this->fail(new RuntimeException('Лимит без времени ожидания: требуется разбор.'));
return;
}
$this->release(max(1, $wait) + random_int(1, 3));
}
}
Задание не хранит API-ключ: клиент создаётся контейнером из конфигурации при обработке. notificationId помогает связать отправку с бизнес-событием, но сам по себе не блокирует дубликаты.
Лимит BotGate приходит как RateLimitException, а лимит Telegram — как TelegramException с telegramErrorCode() === 429. В обоих случаях retryAfter() даёт время ожидания в секундах или null. Задание освобождает worker через release(), откладывая следующую попытку; sleep() внутри процесса не используется. Добавлена небольшая случайная задержка, чтобы несколько заданий не стартовали одновременно. После обработки лимита стоит return — выполнение метода должно закончиться.
Всего разрешены четыре попытки выполнения; отложенные release тоже расходуют попытки. Отсутствующее время ожидания не заменяется бесконечным циклом. Другие SDK-ошибки переводят задание в failed. Необработанное исключение ограничено maxExceptions = 1, таймаут задания — failOnTimeout.
HTTP-статус Telegram-ошибки в BotGate остаётся 400: SDK разбирает код и параметры из JSON. Отдельный HTTP-клиент для этого не нужен. Другие ошибки Telegram, например неверный chat_id или отсутствие прав, завершают задание без повтора. Подробнее о двух источниках ограничений — в статье про 429.
3. Поставьте уведомление после сохранения заказа
В обработчике успешного создания заказа, где уже известен $orderId:
use App\Jobs\SendTelegramNotification;
$botId = config('services.telegram_alerts.bot_id');
$chatId = config('services.telegram_alerts.chat_id');
if (!is_string($botId) || $botId === '' || !is_string($chatId) || $chatId === '') {
throw new RuntimeException('Не настроен получатель Telegram-уведомлений.');
}
SendTelegramNotification::dispatch(
notificationId: 'order-created:'.$orderId,
botId: $botId,
chatId: $chatId,
text: 'Заказ #'.$orderId.' создан.',
)->onQueue('telegram')->afterCommit();
afterCommit() откладывает постановку до фиксации открытой транзакции. Это защищает от отправки сообщения о заказе, который затем откатился, и от чтения ещё не зафиксированных данных worker-ом. Поведение описано в документации очередей Laravel.
Здесь текст задания короткий и не содержит персональных данных. Для реального проекта часто удобнее передавать ID записи уведомления, а состояние, текст и результат хранить в БД. Если потеря задания между сохранением заказа и постановкой в очередь недопустима, используйте transactional outbox: запись уведомления создаётся в той же транзакции, а отдельный процесс доставляет её в очередь. Один afterCommit не делает две разные системы хранения атомарными.
4. Проверьте процесс обработки очереди
Для локальной проверки запустите worker именно для очереди telegram и того connection, куда отправляется задание:
php artisan queue:work --queue=telegram --tries=4 --timeout=40
Процесс остаётся работать в терминале. В production им должен управлять принятый в проекте менеджер процессов. Если слушается только очередь default, задания из telegram будут ждать, даже когда worker запущен.
Для database/Redis queue проверьте retry_after в config/queue.php: он должен быть больше таймаута worker/job с запасом. Например, при сетевом ожидании 20 секунд и timeout задания 40 секунд можно использовать retry_after 90 секунд. Это настройка повторной выдачи зависшего задания, а не Telegram parameters.retry_after. Для SQS соответствующий механизм называется visibility timeout.
| Симптом | Проверка |
|---|---|
| Задание не появляется | Выполнился ли участок dispatch, зафиксировалась ли транзакция, выбран ли ожидаемый connection? |
| Задание есть, попыток нет | Работает ли worker и слушает ли он очередь telegram в правильном окружении? |
| Из CLI работает, из Job — нет | Сверьте конфигурацию, пользователя процесса, сеть, доступ к секретам и время запуска worker. |
| Задание в failed | Проверьте безопасный код ошибки и журнал уведомления. Не запускайте массовый retry до выяснения результата отправки. |
Почему очередь не гарантирует отсутствие дублей
Возможна последовательность: Telegram принял сообщение → соединение оборвалось → приложение не записало успех. Повтор такого задания может отправить второе сообщение. Поэтому в примере неизвестный результат останавливает отправку, а не автоматически переключает маршрут.
Для ответственных уведомлений храните идентификатор бизнес-события и состояния «ожидает», «отправляется», «отправлено», «результат неизвестен». Защитите создание и захват записи от параллельной обработки. Но даже локальный уникальный индекс не устраняет окно между внешней отправкой и записью результата в вашу БД.
Этот учебный Job не реализует доставку «ровно один раз». После аварийного завершения процесса или повторного dispatch возможна повторная обработка. Правила восстановления нужно выбирать по смыслу уведомления; массовый queue:retry all не должен быть стандартной реакцией на любой сбой.
При массовой отправке добавьте общий контроль скорости для бота и получателей. Несколько worker-ов делят один лимит API; увеличение числа процессов может ускорить достижение 429. Принципы распределения нагрузки разобраны в статье про лимиты.
Если нужны входящие команды боту
Для исходящих уведомлений webhook не обязателен, если chat_id уже известен. Если бот должен обрабатывать команды или кнопки, пакет регистрирует POST-маршрут /botgate/webhook. Проверьте фактический путь через php artisan route:list --name=botgate.webhook и укажите его публичный HTTPS-адрес в кабинете BotGate.
Задайте BOTGATE_WEBHOOK_SECRET из карточки бота. Middleware пакета проверяет HMAC-подпись, затем публикуется событие BotGate\Laravel\Events\BotGateUpdateReceived с DTO в свойстве $event->update. Один настроенный маршрут использует один секрет; для нескольких ботов с разными секретами потребуется отдельная схема выбора и проверки.
Слушатель должен сохранять событие и ставить длительную работу в очередь. Сам пакет не хранит входящие обновления и не устраняет дубли. Не регистрируйте один слушатель одновременно вручную и через auto-discovery — иначе действие может выполниться дважды. При сбоях используйте пошаговую диагностику webhook.
Проверка перед использованием
- Один тестовый заказ создаёт одно задание и одно сообщение.
- Откат транзакции не приводит к уведомлению о несуществующем заказе.
- При остановленном worker уведомление остаётся в очереди; после запуска обрабатывается.
- Подставные ответы HTTP 429 BotGate и HTTP 400 с Telegram error_code 429 откладывают задание по указанному времени; лимит без паузы и неизвестная сетевая ошибка не вызывают слепой повтор.
- Проверены failed jobs, таймауты, состояние конфигурации и обработка повторного бизнес-события.
Проверки лимитов делайте на подставных ответах в тестах. Создавать реальные 429 нагрузкой на Telegram для этого не требуется.
Источники
Настройки пакетов описаны в README BotGate Laravel и PHP SDK. Механизм заданий и отложенных попыток — в официальной документации Laravel 13 по очередям.
Пример показывает организацию очереди и явную политику ошибок. Схему БД уведомлений, transactional outbox и менеджер процессов нужно встроить в существующую архитектуру проекта.