PHP & frameworks

Laravel

Native Telegram Bot API integration in Laravel: service class using the built-in HTTP client, webhook controller, Artisan command for registration, queues for sending messages.

Laravel allows you to integrate the Telegram Bot API in two ways: via the built-in HTTP client (a wrapper over Guzzle) or ready-made packages like irazasyed/telegram-bot-sdk. Below is a native option without extra dependencies, as close as possible to the approach from article HTTP-клиент на PHP. We will extract the network logic into a service class, keeping the controller only for parsing the update and calling business logic.

Service Class for Outgoing Requests

Create app/Services/TelegramBot.php. In the constructor, we read the token from the config (it is taken from getenv('TELEGRAM_BOT_TOKEN') via config/services.php). The call() method is a universal wrapper: it makes a POST request to api.telegram.org/bot<token>/<method>, checks the HTTP code, json_last_error, and the ok field in the response. On error, it throws RuntimeException with a clear message.

<?php

namespace App\Services;

use Illuminate\Support\Facades\Http;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\RequestException;

class TelegramBot
{
protected string $token;
protected string $baseUrl;

public function __construct(?string $token = null)
{
$this->token = $token ?? config('services.telegram.token');
$this->baseUrl = "https://api.telegram.org/bot{$this->token}";
}

/**
* Универсальный вызов метода Bot API.
* @throws \RuntimeException при сетевой ошибке или ok=false
*/
public function call(string $method, array $params = []): array
{
try {
$response = Http::timeout(15)
->retry(3, 100)
->post("{$this->baseUrl}/{$method}", $params)
->throw()
->json();
} catch (ConnectionException $e) {
throw new \RuntimeException("Telegram API connection failed: " . $e->getMessage());
} catch (RequestException $e) {
$body = $e->response?->json();
$desc = $body['description'] ?? $e->getMessage();
throw new \RuntimeException("Telegram API error: {$desc}");
}

if (empty($response['ok'])) {
throw new \RuntimeException("Telegram API returned ok=false: " . ($response['description'] ?? 'unknown'));
}

return $response['result'];
}

public function sendMessage(int|string $chatId, string $text, array $options = []): array
{
return $this->call('sendMessage', array_merge([
'chat_id' => $chatId,
'text' => $text,
'parse_mode' => 'HTML',
], $options));
}

public function answerCallbackQuery(string $callbackQueryId, ?string $text = null, bool $showAlert = false): array
{
return $this->call('answerCallbackQuery', [
'callback_query_id' => $callbackQueryId,
'text' => $text,
'show_alert' => $showAlert,
]);
}
}

API Response Example and How to Inspect It

The sendMessage method returns a Message object. In code, you can check the result like this:

<?php

$result = $bot->sendMessage(123456789, "Тестовое сообщение");
// Для отладки в консоли (php artisan tinker или job):
var_dump($result);
// Или красиво:
echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);

A typical JSON response (field result):

{
"message_id": 42,
"date": 1700000000,
"chat": {
"id": 123456789,
"type": "private",
"first_name": "Ivan"
},
"text": "Тестовое сообщение"
}

What to look at: message_id — needed for subsequent editing/deletion (methods editMessageText, deleteMessage); chat.id — confirms that the message was sent right where planned; date — unix timestamp of sending according to the Telegram server.

Token Configuration

Do not hardcode the token. In config/services.php, read it from the environment:

// config/services.php
return [
// ...
'telegram' => [
'token' => env('TELEGRAM_BOT_TOKEN'),
],
];
# .env
TELEGRAM_BOT_TOKEN=123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11

You can check that the token was picked up properly in Tinker:

php artisan tinker
> config('services.telegram.token')

Controller for Incoming Updates (Webhook)

Create app/Http/Controllers/TelegramWebhookController.php. Laravel automatically decodes the JSON body into an array $request->all(). In chapter Long polling, we made the processUpdate function — the logic here is the same, just inside a controller. Important: always return 200 OK with {"ok":true} within 1–2 seconds, otherwise Telegram will retry delivery.

<?php

namespace App\Http\Controllers;

use App\Services\TelegramBot;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;

class TelegramWebhookController extends Controller
{
public function __invoke(Request $request, TelegramBot $bot): JsonResponse
{
$update = $request->all();

// Защита от пустых/некорректных запросов
if (!isset($update['update_id'])) {
return response()->json(['ok' => true]);
}

// --- Сообщения ---
if (isset($update['message'])) {
$msg = $update['message'];
$chatId = $msg['chat']['id'];
$text = $msg['text'] ?? '';

// В главе "Команды /start, /help" мы делали parseCommand — используем тот же подход
if (str_starts_with($text, '/start')) {
$bot->sendMessage($chatId, "Привет! Я бот на Laravel.\nНапиши /help для списка команд.");
} elseif ($text === '/help') {
$bot->sendMessage($chatId, "Доступные команды: /start, /help");
}
}

// --- Callback queries (inline-кнопки) ---
if (isset($update['callback_query'])) {
$cq = $update['callback_query'];
// В главе "callback_query" мы разбирали answerCallbackQuery — здесь вызываем метод сервиса
$bot->answerCallbackQuery($cq['id'], "Нажата кнопка: {$cq['data']}");
}

return response()->json(['ok' => true]);
}
}

Route and Webhook Registration

In routes/web.php (or api.php), add a route. The {token} parameter in the URL provides basic protection against random requests; in the controller, it can be checked against config('services.telegram.token').

use App\Http\Controllers\TelegramWebhookController;

Route::post('/telegram/webhook/{token}', TelegramWebhookController::class)
->where('token', '.*');

Set the webhook once — via an Artisan command (convenient for deployment) or in Tinker. The command uses our service TelegramBot and the setWebhook method.

<?php

namespace App\Console\Commands;

use App\Services\TelegramBot;
use Illuminate\Console\Command;

class SetupTelegramWebhook extends Command
{
protected $signature = 'app:setup-telegram-webhook';
protected $description = 'Set Telegram webhook URL';

public function handle(TelegramBot $bot): int
{
$url = config('app.url') . '/telegram/webhook/' . config('services.telegram.token');
$result = $bot->call('setWebhook', [
'url' => $url,
'drop_pending_updates' => true, // чистим старые обновления при деплое
'allowed_updates' => ['message', 'callback_query'], // только нужные типы
]);

// Результат setWebhook — просто true при успехе
$this->info("Webhook set: " . ($result === true ? 'ok' : 'failed'));

// Проверить текущий вебхук можно так:
$info = $bot->call('getWebhookInfo');
$this->line('Current webhook: ' . json_encode($info, JSON_PRETTY_PRINT));

return self::SUCCESS;
}
}

Run: php artisan app:setup-telegram-webhook. In the output, you will see JSON from getWebhookInfo — check the fields url, pending_update_count, last_error_date/last_error_message.

Using the Service in the Application (Jobs, Listeners, Controllers)

The service is automatically resolved via the container. To send notifications from background tasks, create a Job with implements ShouldQueue — Laravel will handle retries automatically on exceptions from TelegramBot::call().

<?php

namespace App\Jobs;

use App\Services\TelegramBot;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

class NotifyUser implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

public function __construct(
protected int|string $chatId,
protected string $message
) {}

public function handle(TelegramBot $bot): void
{
$bot->sendMessage($this->chatId, $this->message);
}
}

// Диспатч из любого места:
// NotifyUser::dispatch(123456789, 'Ваш заказ #123 принят!');

Configure retries in config/queue.php (retry_after) and run the worker with the --tries=3 flag. On a network error or ok=false, the Job will fail to failed_jobs after exhausting attempts.

Debugging: How to Inspect What Telegram Sent

During development, it is convenient to log the entire update. Add to the top of the controller:

<?php

// ... внутри __invoke
\Log::channel('telegram')->info('Incoming update', [
'update_id' => $update['update_id'] ?? null,
'payload' => $update,
]);

In config/logging.php, add a channel:

// config/logging.php
'channels' => [
// ...
'telegram' => [
'driver' => 'daily',
'path' => storage_path('logs/telegram.log'),
'level' => 'info',
'days' => 14,
],
],

Now tail -f storage/logs/telegram-*.log will show the structure of each update — useful for understanding fields like message, callback_query, inline_query, etc.

Next: БД и состояния (FSM)