InlineKeyboardMarkup на PHP: обработка callback_query, лимит 64 байта и уплотнение payload

Интерактивные инлайн-клавиатуры (InlineKeyboardMarkup) — основной инструмент взаимодействия пользователя с ботом без отправки текстовых команд. Однако при их реализации разработчики часто сталкиваются со скрытыми подводными камнями Telegram Bot API: жестким ограничением размера callback_data в 64 байта, зависшими индикаторами загрузки на кнопках и гонками запросов при повторных вебхуках.

В этой статье мы подробно разберем, как грамотно проектировать короткий payload, реализовывать клиенты для выполнения вызовов API через cURL, обрабатывать события callback_query и бесшовно обновлять интерфейс с помощью editMessageText.

Ограничение 64 байта и проектирование короткого payload

Тег callback_data в объекте InlineKeyboardButton имеет фундаментальное ограничение: его длина не может превышать 64 байта в кодировке UTF-8. Если переписать туда слишком большой JSON или длинный строковый идентификатор, Telegram Bot API вернет ошибку HTTP 400 со статусом Bad Request: BUTTON_DATA_INVALID.

Попытка хранить состояние заказа, имена сущностей или длительные фильтры прямо в кнопке — главная ошибка начинающих разработчиков. Данные callback_data должны быть максимально компактными и содержать только короткий префикс действия (action) и уникальный идентификатор записи.

Стратегии сжатия данных:

  • Короткие префиксы команд: Вместо action=confirm_order используйте cnf или act:c.
  • Сгенерированные хеши или бинарные ключи: Для временных состояний используйте короткие ключи в базе данных (например, bin2hex(random_bytes(7))) или хранилище Redis.
  • Разделители минимальной длины: Используйте символы : или | для разделения аргументов вместо сложных форматов.

Ниже приведен класс для кодирования и декодирования передаваемых в кнопки команд с проверкой жесткого лимита байтов:

<?php

declare(strict_types=1);

class CallbackPayload
{
private const MAX_BYTES = 64;
private const DELIMITER = ':';

public static function pack(string $action, string|int $id): string
{
$payload = $action . self::DELIMITER . $id;

if (strlen($payload) > self::MAX_BYTES) {
throw new InvalidArgumentException(
sprintf('Payload exceeds %d bytes limit: "%s"', self::MAX_BYTES, $payload)
);
}

return $payload;
}

public static function unpack(string $payload): array
{
$parts = explode(self::DELIMITER, $payload, 2);

return [
'action' => $parts[0] ?? '',
'id' => $parts[1] ?? '',
];
}
}

Надежный HTTP-клиент cURL для вызовов Telegram Bot API

Для выполнения запросов к Telegram API нельзя использовать функцию file_get_contents, так как она не позволяет гибко управлять таймаутами, заголовками и обработкой ошибок сетевого уровня. Использование библиотеки cURL гарантирует контроль над выполнением запроса.

При отправке команд answerCallbackQuery и editMessageText важно проверять три уровня ошибок: статус выполнения самого cURL, валидность полученного JSON и статус ok: true во внутреннем ответе Telegram.

<?php

declare(strict_types=1);

function callTelegramApi(string $method, array $params = []): array
{
$token = getenv('TELEGRAM_BOT_TOKEN');
if (!$token) {
throw new RuntimeException('Environment variable TELEGRAM_BOT_TOKEN is not set.');
}

$url = sprintf('https://api.telegram.org/bot%s/%s', $token, $method);

$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode($params, JSON_UNESCAPED_UNICODE),
CURLOPT_TIMEOUT => 5,
CURLOPT_CONNECTTIMEOUT => 3,
]);

$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);

if ($response === false) {
throw new RuntimeException('cURL error: ' . $error);
}

$decoded = json_decode((string)$response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new RuntimeException('JSON parse error: ' . json_last_error_msg());
}

if ($httpCode !== 200 || empty($decoded['ok'])) {
$description = $decoded['description'] ?? 'Unknown API Error';
throw new RuntimeException(sprintf('Telegram API Error [%d]: %s', $httpCode, $description));
}

return $decoded['result'];
}

Зачем обязателен answerCallbackQuery

Когда пользователь нажимает inline-кнопку, клиент Telegram переходит в состояние ожидания и отображает вращающийся индикатор (loader) на самой кнопке. Если сервер бота не ответит вызовом метода answerCallbackQuery, анимация будет продолжаться около 60 секунд, вызывая ощущение зависшего бота.

Вызывать answerCallbackQuery нужно максимально быстро, передав callback_query_id. Метод принимает опциональные параметры:

  • text: текст всплывающего уведомления (до 200 символов).
  • show_alert: если false (по умолчанию), покажется короткая плашка вверху экрана. Если true, откроется модальное окно с кнопкой «ОК».

Редактирование сообщений через editMessageText

После получения нажатия кнопки часто требуется изменить исходное сообщение: обновить статус заказа, поменять текст или полностью убрать/заменить клавиатуру. Для этого применяется метод editMessageText.

Для идентификации обновляемого сообщения в метод передаются параметры chat_id и message_id, взятые из объекта callback_query['message'].

Важно: Если отправить в editMessageText абсолютно идентичный текст и структуру reply_markup, Telegram вернет ошибку 400 Bad Request: message is not modified. Эту ошибку следует перехватывать и игнорировать.

Полная реализация Webhook-обработчика

Объединим все этапы в готовом скрипте обработчика вебхука. В нем реализована проверка секрета X-Telegram-Bot-Api-Secret-Token, дедупликация входящих событий по update_id (для предотвращения повторной обработки при сбоях сети), вызов answerCallbackQuery и последующая замена текста через editMessageText.

<?php

declare(strict_types=1);

header('Content-Type: application/json');

// 1. Проверка Secret Token от Telegram
$secretToken = getenv('TELEGRAM_SECRET_TOKEN') ?: 'my-super-secret-token';
$incomingSecret = $_SERVER['HTTP_X_TELEGRAM_BOT_API_SECRET_TOKEN'] ?? '';

if (!hash_equals($secretToken, $incomingSecret)) {
http_response_code(403);
echo json_encode(['ok' => false, 'error' => 'Forbidden']);
exit;
}

// 2. Чтение входящего Update
$rawContent = file_get_contents('php://input');
$update = json_decode((string)$rawContent, true);

if (json_last_error() !== JSON_ERROR_NONE || !isset($update['update_id'])) {
http_response_code(400);
echo json_encode(['ok' => false, 'error' => 'Invalid JSON']);
exit;
}

// 3. Проверка идемпотентности по update_id (SQLite для примера)
$pdo = new PDO('sqlite:' . __DIR__ . '/updates.sqlite');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->exec('CREATE TABLE IF NOT EXISTS processed_updates (update_id INTEGER PRIMARY KEY)');

$stmt = $pdo->prepare('INSERT OR IGNORE INTO processed_updates (update_id) VALUES (?)');
$stmt->execute([$update['update_id']]);

if ($stmt->rowCount() === 0) {
// Данный update_id уже был успешно обработан ранее
http_response_code(200);
echo json_encode(['ok' => true, 'status' => 'duplicate_skipped']);
exit;
}

// 4. Обработка callback_query
if (isset($update['callback_query'])) {
$cq = $update['callback_query'];
$cqId = $cq['id'];
$data = $cq['data'] ?? '';
$chatId = $cq['message']['chat']['id'] ?? null;
$messageId = $cq['message']['message_id'] ?? null;

$parsed = CallbackPayload::unpack($data);
$action = $parsed['action'];
$leadId = $parsed['id'];

try {
// Обязательно гасим лоадер на кнопке
callTelegramApi('answerCallbackQuery', [
'callback_query_id' => $cqId,
'text' => 'Выбор принят',
'show_alert' => false,
]);

if ($action === 'cnf' && $chatId && $messageId) {
// Менявм текст сообщения и удаляем клавиатуру
callTelegramApi('editMessageText', [
'chat_id' => $chatId,
'message_id' => $messageId,
'text' => sprintf('Заявка <b>#%s</b> успешно подтверждена!', htmlspecialchars($leadId)),
'parse_mode' => 'HTML',
'reply_markup' => [
'inline_keyboard' => []
],
]);
}
} catch (Throwable $e) {
error_log('Callback processing error: ' . $e->getMessage());
}
}

http_response_code(200);
echo json_encode(['ok' => true]);

Правильное проектирование payload и соблюдение стандарта вызовов Bot API позволяет создавать быстрый и устойчивый к сбоям интерфейс инлайн-клавиатур на чисто PHP без задержек и лишних ошибочных состояний.

Если вам требуется разработка надежных Telegram-ботов и веб-приложений с интеграцией в бизнес-процессы, обратитесь к специалистам BotCreator.

Новые статьи — в Telegram

Разбираем, что автоматизировать в бизнесе и как это работает на практике. Без спама.