Переклад цього гайду зʼявиться незабаром. Поки що текст доступний російською — перемкніться на RU або читайте нижче. Відкрити російську версію

PHP і фреймворки

HTTP-клієнт на PHP

В предыдущих главах мы уже использовали функцию telegramApi() из статьи Первый запрос: getMe. Там она была минимальной — для разовых вызовов. В продакшене нужен переиспользуемый клиент: единый таймаут, обработка ошибок, логирование и ретраи. Ниже — готовый к бою класс на cURL.

Почему не file_get_contents

file_get_contents с stream_context_create работает, но скрывает HTTP-код ответа, не даёт доступа к заголовкам (важно для Retry-After при 429) и усложняет ретраи. cURL — стандарт де-факто в PHP для HTTP.

Класс TelegramClient

Положите файл в src/TelegramClient.php. Конструктор читает токен из окружения — так безопаснее и удобнее для CI/CD.

<?php

namespace App;

final class TelegramClient
{
public function __construct(
private string $token = '',
private int $timeout = 15,
private int $connectTimeout = 5,
) {
if ($this->token === '') {
$this->token = getenv('TELEGRAM_BOT_TOKEN') ?: '';
}
if ($this->token === '') {
throw new \InvalidArgumentException('Telegram bot token is empty');
}
}

/**
* Вызов любого метода Bot API.
* @return array<mixed> — поле result из ответа Telegram
*/
public function call(string $method, array $params = []): array
{
$url = 'https://api.telegram.org/bot' . $this->token . '/' . $method;

$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => $this->timeout,
CURLOPT_CONNECTTIMEOUT => $this->connectTimeout,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode($params, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
]);

$body = curl_exec($ch);
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlErrNo = curl_errno($ch);
$curlErrMsg = curl_error($ch);
curl_close($ch);

// Сетевая ошибка (DNS, timeout, TLS)
if ($curlErrNo !== 0) {
throw new \RuntimeException("cURL error {$curlErrNo}: {$curlErrMsg}");
}

// HTTP 4xx/5xx до парсинга JSON
if ($httpCode >= 400) {
throw new \RuntimeException("HTTP {$httpCode} for method {$method}");
}

$json = json_decode($body, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new \RuntimeException('Invalid JSON: ' . json_last_error_msg());
}

// Telegram вернул ok=false
if (empty($json['ok'])) {
$desc = $json['description'] ?? 'Unknown Telegram error';
$errCode = $json['error_code'] ?? $httpCode;
throw new \RuntimeException("Telegram API error {$errCode}: {$desc}");
}

return $json['result'];
}

/** Удобные сокращения */
public function getMe(): array { return $this->call('getMe'); }
public function sendMessage(int|string $chatId, string $text, array $options = []): array {
return $this->call('sendMessage', array_merge(['chat_id' => $chatId, 'text' => $text], $options));
}
}

Пример использования и как смотреть ответ

Создайте test-client.php рядом с vendor/ (или подключите автозагрузчик вашего проекта):

<?php

require_once __DIR__ . '/vendor/autoload.php';

use App\TelegramClient;

$client = new TelegramClient(); // токен возьмётся из TELEGRAM_BOT_TOKEN

try {
$me = $client->getMe();
echo "Bot info:\n";
// Красивый вывод в терминале
echo json_encode($me, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n";

// Или для отладки структуры:
// var_dump($me);
// print_r($me);
} catch (\Throwable $e) {
echo "ERROR: " . $e->getMessage() . "\n";
exit(1);
}

Что вернёт getMe — разбор JSON

Запустите php test-client.php. В консоли увидите:

{
"id": 123456789,
"is_bot": true,
"first_name": "MyBot",
"username": "mybot",
"can_join_groups": true,
"can_read_all_group_messages": false,
"supports_inline_queries": false,
"can_connect_to_business": false,
"has_main_web_app": false
}
  • id — числовой ID бота (нужен для deep-linking tg://user?id=123456789).
  • username — публичный @username, используйте его в ссылках https://t.me/mybot.
  • can_read_all_group_messages — если false, бот видит только команды и сообщения, где его упомянули. Включается в BotFather → Bot Settings → Group Privacy.
  • supports_inline_queries — включён Inline Mode (BotFather → Inline Settings).

Обработка ошибок на практике

Попробуйте намеренно сломать токен (добавьте лишний символ в .env) — увидите:

Telegram API error 401: Unauthorized

Класс бросает RuntimeException с понятным текстом. В продакшене оборачивайте вызовы в try/catch и логируйте:

<?php

try {
$client->sendMessage($chatId, 'Hello!');
} catch (\RuntimeException $e) {
// Логируем без токена
error_log(sprintf('Telegram sendMessage failed: %s', $e->getMessage()));
// Можно положить в очередь на повтор (см. главу Production)
}

Ретраи на 429 (Too Many Requests) и 5xx

Telegram требует уважать заголовок Retry-After (секунды). Добавим простой ретрай в клиент:

<?php

// Добавьте в класс TelegramClient
private int $maxRetries = 3;

public function callWithRetry(string $method, array $params = []): array
{
$attempt = 0;
while (true) {
try {
return $this->call($method, $params);
} catch (\RuntimeException $e) {
$msg = $e->getMessage();
// Извлекаем HTTP-код из сообщения "HTTP 429 for method..."
if (preg_match('/HTTP (\d{3})/', $msg, $m)) {
$code = (int)$m[1];
$retryable = ($code === 429 || $code >= 500);
if ($retryable && $attempt < $this->maxRetries) {
$attempt++;
$delay = $code === 429 ? 30 : (int)pow(2, $attempt); // экспоненциальный бэкофф
error_log("Retry {$attempt}/{$this->maxRetries} after {$delay}s: {$msg}");
sleep($delay);
continue;
}
}
throw $e;
}
}
}

Теперь вызовы через callWithRetry() сами подождут при флуд-контроле или временных проблемах Telegram.

Загрузка файлов (multipart/form-data)

Методы sendPhoto, sendDocument, sendVideo с локальным файлом требуют multipart/form-data, а не JSON. Добавьте отдельный метод:

<?php

// Внутри класса TelegramClient
public function uploadFile(string $method, string $fileField, string $filePath, array $otherFields = []): array
{
if (!is_file($filePath) || !is_readable($filePath)) {
throw new \InvalidArgumentException("File not readable: {$filePath}");
}

$cfile = new \CURLFile($filePath);
$postFields = array_merge($otherFields, [$fileField => $cfile]);

$ch = curl_init('https://api.telegram.org/bot' . $this->token . '/' . $method);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => $this->timeout,
CURLOPT_POSTFIELDS => $postFields, // cURL сам поставит multipart/form-data
]);

$body = curl_exec($ch);
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlErrNo = curl_errno($ch);
curl_close($ch);

if ($curlErrNo !== 0) {
throw new \RuntimeException("cURL error {$curlErrNo}: " . curl_error($ch));
}
if ($httpCode >= 400) {
throw new \RuntimeException("HTTP {$httpCode} for {$method}");
}
$json = json_decode($body, true);
if (empty($json['ok'])) {
throw new \RuntimeException("Telegram: " . ($json['description'] ?? 'error'));
}
return $json['result'];
}

// Использование:
// $client->uploadFile('sendPhoto', 'photo', '/path/to/image.jpg', [
// 'chat_id' => $chatId,
// 'caption' => 'Фото из файла',
// ]);

Связь с предыдущими главами

  • Функция telegramApi() из getMe — это упрощённый аналог метода call().
  • В Long polling и Webhook используйте $client->callWithRetry() для отправки ответов пользователю.
  • Для фото и документов — метод uploadFile().

Дальше: Yii2 — компонент и контроллер для вебхуков.