PHP & frameworks
PHP HTTP client
Writing a production client on cURL: timeouts, retries for 429/5xx, file uploads via multipart, parsing JSON responses and errors.
In previous chapters we have already used function telegramApi() from article Первый запрос: getMe. There it was minimal — for one-off calls. In production you need a reusable client: unified timeout, error handling, logging and retries. Below is a battle-ready class using cURL.
Why not file_get_contents
file_get_contents with stream_context_create works, but it hides the HTTP response code, does not give access to headers (important for Retry-After when rate limited at 429) and complicates retries. cURL is the de facto standard in PHP for HTTP.
TelegramClient Class
Place the file in src/TelegramClient.php. The constructor reads the token from the environment — this is safer and more convenient for 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));
}
}
Example usage and how to view the response
Create test-client.php next to vendor/ (or include your project's autoloader):
<?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);
}
What getMe returns — JSON breakdown
Run php test-client.php. You will see in the console:
{
"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 — numeric bot ID (needed for deep-linking
tg://user?id=123456789). - username — public @username, use it in links
https://t.me/mybot. - can_read_all_group_messages — if
false, the bot sees only commands and messages where it was mentioned. Enabled in BotFather → Bot Settings → Group Privacy. - supports_inline_queries — Inline Mode is enabled (BotFather → Inline Settings).
Error handling in practice
Try intentionally breaking the token (add an extra character to .env) — you'll see:
Telegram API error 401: Unauthorized
The class throws RuntimeException with a clear message. In production wrap calls in try/catch and log:
<?php
try {
$client->sendMessage($chatId, 'Hello!');
} catch (\RuntimeException $e) {
// Логируем без токена
error_log(sprintf('Telegram sendMessage failed: %s', $e->getMessage()));
// Можно положить в очередь на повтор (см. главу Production)
}
Retries on 429 (Too Many Requests) and 5xx
Telegram requires respecting header Retry-After (seconds). Let's add a simple retry to the client:
<?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;
}
}
}
Now calls via callWithRetry() will automatically wait during flood control or temporary Telegram issues.
File uploads (multipart/form-data)
Methods sendPhoto, sendDocument, sendVideo with a local file require multipart/form-data, not JSON. Add a separate method:
<?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' => 'Фото из файла',
// ]);
Connection to previous chapters
- Function
telegramApi()from getMe is a simplified analog of methodcall(). - In Long polling and Webhook use
$client->callWithRetry()to send responses to the user. - For фото и документов — method
uploadFile().