Обновления
getUpdates и update_id
Практическое руководство по getUpdates: как получать обновления, работать с update_id и offset, защищаться от дублей и парсить входящие сообщения на PHP.
Telegram отдаёт события боту объектами Update. В режиме polling вы сами запрашиваете их методом getUpdates. Это первая страница, где мы реально читаем входящие данные — до этого мы только делали getMe.
Как вызвать getUpdates
Используем функцию telegramApi(), которую мы сделали в главе Первый запрос: getMe. Там мы написали обёртку над cURL с проверкой HTTP-кода, json_last_error и поля ok — здесь просто вызываем её.
<?php
// get_updates.php
require_once __DIR__ . '/src/TelegramClient.php'; // там функция telegramApi()
$token = getenv('TELEGRAM_BOT_TOKEN');
if (!$token) {
die('TELEGRAM_BOT_TOKEN не задан в окружении\n');
}
$offset = 0; // первый запуск — берём всё
$updates = telegramApi($token, 'getUpdates', [
'offset' => $offset,
'limit' => 100, // макс. 100 за раз
'timeout' => 0, // 0 = короткий запрос (short polling)
]);
// Смотрим, что пришло — в терминале удобнее json_encode с флагами
// print_r($updates); // тоже работает, но JSON читаемее для вложенных структур
echo json_encode($updates, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
Пример ответа API
Успешный ответ всегда имеет ok: true и массив в result:
{
"ok": true,
"result": [
{
"update_id": 847392101,
"message": {
"message_id": 12,
"from": {
"id": 123456789,
"is_bot": false,
"first_name": "Ivan",
"username": "ivan_php",
"language_code": "ru"
},
"chat": {
"id": 123456789,
"first_name": "Ivan",
"username": "ivan_php",
"type": "private"
},
"date": 1724501234,
"text": "/start"
}
},
{
"update_id": 847392102,
"message": {
"message_id": 13,
"from": { ... },
"chat": { ... },
"date": 1724501235,
"text": "Привет, бוט!"
}
}
]
}
Что смотреть:
ok—trueзначит запрос прошёл,false— вdescriptionбудет ошибка (например,"Bad Request: offset must be greater than...").result— массив Update. Пустой массив[]— новых событий нет.update_id— монотонно растущий идентификатор. Каждый следующий Update имеет большийupdate_id.message— входящее сообщение. Поля:message_id,from(User),chat(Chat),date(Unix timestamp),textи др.
Параметр offset — курсор обработки
offset говорит серверу: «всё с update_id меньше этого значения уже обработано, не присылай повторно». Значение — последний обработанный update_id + 1.
<?php
// После обработки пачки обновляем offset
$nextOffset = $offset;
foreach ($updates as $update) {
// ... ваша логика: processUpdate($update);
$nextOffset = max($nextOffset, $update['update_id'] + 1);
}
// Сохраняем $nextOffset в файл/БД/Redis для следующего запуска
file_put_contents(__DIR__ . '/.offset', (string)$nextOffset);
Важно: если скрипт упал после обработки сообщения, но до сохранения offset — при следующем запуске этот Update придёт снова. Это нормально, нужно писать идемпотентную логику (см. ниже).
Идемпотентность: защита от дублей
Самый надёжный паттерн — таблица с UNIQUE на update_id. Вставляем перед бизнес-логикой; если дубликат — пропускаем.
<?php
// Пример с PDO (таблицу создайте заранее: CREATE TABLE telegram_updates (update_id BIGINT PRIMARY KEY, received_at INT NOT NULL);)
$pdo = new PDO('sqlite:' . __DIR__ . '/bot.db');
$pdo->exec("CREATE TABLE IF NOT EXISTS telegram_updates (
update_id BIGINT PRIMARY KEY,
received_at INT NOT NULL
);");
foreach ($updates as $update) {
$id = $update['update_id'];
try {
$pdo->prepare('INSERT INTO telegram_updates (update_id, received_at) VALUES (?, ?)')
->execute([$id, time()]);
} catch (PDOException $e) {
if ($e->getCode() === '23000') { // UNIQUE constraint violation
continue; // дубль — уже обрабатывали
}
throw $e;
}
// Здесь ваша бизнес-логика
processUpdate($update);
}
Что внутри Update — типы событий
У каждого Update ровно одно из полей-событий. Частые:
message— новое сообщение (текст, фото, стикер, документ…)edited_message— отредактированное сообщениеcallback_query— нажатие inline-кнопки (см. callback_query)inline_query— inline-режимmy_chat_member— бот добавлен/удалён из чатаchat_member— изменился статус участникаpre_checkout_query,shipping_query— платежи
Обрабатывайте только нужные типы, остальные игнорируйте явно:
<?php
foreach ($updates as $update) {
if (isset($update['message'])) {
handleMessage($update['message']);
} elseif (isset($update['callback_query'])) {
handleCallback($update['callback_query']);
} elseif (isset($update['my_chat_member'])) {
handleChatMember($update['my_chat_member']);
} else {
// Можно логировать неизвестные типы для отладки
error_log('Unknown update type: ' . json_encode(array_keys($update)));
}
}
Параметр allowed_updates — экономия трафика
По умолчанию Telegram присылает все типы. Если бот работает только с сообщениями и кнопками — ограничьте список:
<?php
$updates = telegramApi($token, 'getUpdates', [
'offset' => $nextOffset,
'limit' => 100,
'timeout' => 0,
'allowed_updates' => ['message', 'callback_query', 'my_chat_member'],
]);
Это уменьшает размер ответа и парсинг на вашей стороне.
Как проверить в терминале
- Напишите боту что-нибудь в Telegram.
- Запустите скрипт:
php get_updates.php. - Вы увидите массив с
update_id,message,from,chat,text. - Запустите повторно — вернётся
[](пустой массив), потому что offset не сохранялся. - Добавьте сохранение offset в файл — повторный запуск будет чистым.
Для продакшена short polling (timeout=0) не подходит — слишком много пустых запросов. В следующей главе переходим к Long polling: держим соединение открытым до 30 секунд и забираем пачки сразу по появлению.
Дальше: Long polling — держать соединение открытым и получать обновления без задержек