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

Розбираємо, що автоматизувати в бізнесі та як це працює на практиці. Без спаму.