Telegram-бот на чистом PHP можно собрать за час: получить токен у @BotFather, поднять HTTPS-эндпоинт и научить его понимать команды и нажатия inline-кнопок. В этом руководстве — рабочий каркас, который вы поставите на свой сервер и расширите под любую задачу: уведомления, опросы, FAQ, заявки.
Получение токена и регистрация бота
Откройте в Telegram чат с @BotFather, отправьте /newbot, придумайте имя и username (заканчивается на bot). В ответ придёт строка вида 123456789:AAH...XYZ — это HTTP API token, через который бот ходит к серверам Telegram. Храните его как пароль: вынесите в .env или константу вне репозитория.
<?php
// config.php — НЕ коммитить в git
define('BOT_TOKEN', '123456789:AAH...XYZ');
define('BOT_API', 'https://api.telegram.org/bot' . BOT_TOKEN . '/');
// Секрет для проверки, что webhook действительно от Telegram
define('WEBHOOK_SECRET', 'k2Lp9xQz7vR4nM8wJ3sT');
Все запросы к Bot API — это обычные HTTP-вызовы вида https://api.telegram.org/bot<TOKEN>/methodName. Параметры передаются GET, POST или JSON-телом. Ответ всегда JSON с полем ok.

Webhook против long polling
У Bot API два способа получать обновления:
- long polling — ваш скрипт сам дёргает
getUpdatesв цикле. Подходит для локальной разработки и серверов без публичного HTTPS. - webhook — Telegram POSTит JSON на ваш URL при каждом событии. Это рабочий вариант для продакшена: меньше задержка, не нужен фоновой процесс, скрипт стартует только когда есть что обработать.
Webhook требует HTTPS с валидным сертификатом (Let’s Encrypt подходит) и публичный URL. Зарегистрировать webhook — один разовый вызов:
<?php
// set_webhook.php — запустить один раз вручную
require __DIR__ . '/config.php';
$params = [
'url' => 'https://example.com/bot.php',
'secret_token' => WEBHOOK_SECRET, // Telegram пришлёт его в заголовке
'allowed_updates' => json_encode(['message', 'callback_query']),
'drop_pending_updates' => true, // сбросить накопившиеся апдейты
];
$response = file_get_contents(BOT_API . 'setWebhook?' . http_build_query($params));
echo $response;
// Ожидаем: {"ok":true,"result":true,"description":"Webhook was set"}
Проверить, что webhook повешен корректно, можно через getWebhookInfo — ответ покажет URL, дату последней ошибки и количество ожидающих апдейтов.
Скелет webhook-обработчика
Файл bot.php — единая точка входа. Telegram POSTит сюда JSON, мы его парсим и решаем, что делать. Главное правило — отвечать как можно быстрее (под 60 секунд, а лучше под 5), иначе Telegram повторит доставку и пользователь получит дубль.
<?php
require __DIR__ . '/config.php';
// 1. Проверяем секрет — Telegram кладёт его в заголовок
$header = $_SERVER['HTTP_X_TELEGRAM_BOT_API_SECRET_TOKEN'] ?? '';
if (!hash_equals(WEBHOOK_SECRET, $header)) {
http_response_code(403);
exit;
}
// 2. Читаем тело запроса
$input = file_get_contents('php://input');
$update = json_decode($input, true);
if (!$update) {
http_response_code(400);
exit;
}
// 3. Логируем для отладки (на проде уровень DEBUG отключайте)
file_put_contents(__DIR__ . '/bot.log', date('c') . ' ' . $input . PHP_EOL, FILE_APPEND);
// 4. Маршрутизация
if (isset($update['message'])) {
handleMessage($update['message']);
} elseif (isset($update['callback_query'])) {
handleCallback($update['callback_query']);
}
// Telegram достаточно HTTP 200
http_response_code(200);
Главный приём для скорости — закрывать соединение перед тяжёлой работой через fastcgi_finish_request(). Telegram получает 200 и считает доставку успешной, а ваш скрипт продолжает выполняться:
<?php
http_response_code(200);
header('Content-Length: 0');
if (function_exists('fastcgi_finish_request')) {
fastcgi_finish_request(); // ответ ушёл, скрипт работает дальше
}
// Тут уже можно обращаться к медленному API, рассылать и т.д.
processHeavyTask($update);
Команды и текстовые сообщения
Сообщение приходит в поле message.text. Команды (/start, /help) начинаются со слэша. Удобно вытащить команду регуляркой и хранить обработчики в массиве:
<?php
function handleMessage(array $msg): void {
$chatId = $msg['chat']['id'];
$text = trim($msg['text'] ?? '');
// Команда вида /cmd@MyBot или /cmd arg
if (preg_match('~^/(\w+)(?:@\S+)?(?:\s+(.+))?$~u', $text, $m)) {
$cmd = strtolower($m[1]);
$args = $m[2] ?? '';
runCommand($cmd, $args, $chatId);
return;
}
// Не команда — отправим эхо или прогоним через свой парсер
sendMessage($chatId, 'Не понял. Наберите /help');
}
function runCommand(string $cmd, string $args, int $chatId): void {
$handlers = [
'start' => fn() => sendMessage($chatId, 'Привет! Я бот karoche.ru. Жми /menu'),
'help' => fn() => sendMessage($chatId, "Доступно:\n/menu\n/about"),
'menu' => fn() => sendMenu($chatId),
'about' => fn() => sendMessage($chatId, 'PHP-бот, открытый код'),
];
if (isset($handlers[$cmd])) {
$handlers[$cmd]();
} else {
sendMessage($chatId, 'Неизвестная команда: /' . $cmd);
}
}
Для отправки текста — метод sendMessage. Поддерживается простое HTML-форматирование: <b>, <i>, <code>, <a>. Не забывайте экранировать пользовательский ввод функцией htmlspecialchars() — иначе бот словит ошибку парсинга на сообщении со знаком <.
<?php
function apiCall(string $method, array $params = []): array {
$ch = curl_init(BOT_API . $method);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($params),
CURLOPT_TIMEOUT => 10,
]);
$body = curl_exec($ch);
curl_close($ch);
return json_decode($body, true) ?? ['ok' => false];
}
function sendMessage(int $chatId, string $text, array $extra = []): array {
return apiCall('sendMessage', array_merge([
'chat_id' => $chatId,
'text' => $text,
'parse_mode' => 'HTML',
'disable_web_page_preview' => true,
], $extra));
}
Inline-кнопки и callback_query
Inline-клавиатура — это кнопки, прикреплённые к конкретному сообщению. У каждой кнопки есть callback_data (до 64 байт) — короткий маркер, который придёт обратно при нажатии в виде callback_query.
<?php
function sendMenu(int $chatId): void {
$keyboard = [
'inline_keyboard' => [
// Первый ряд
[
['text' => 'Статьи', 'callback_data' => 'cat:articles'],
['text' => 'Поиск', 'callback_data' => 'cat:search'],
],
// Второй ряд
[
['text' => 'Контакты', 'callback_data' => 'cat:contacts'],
],
// Третий — внешняя ссылка
[
['text' => 'Открыть karoche.ru', 'url' => 'https://karoche.ru'],
],
],
];
sendMessage($chatId, '<b>Главное меню</b>', [
'reply_markup' => json_encode($keyboard),
]);
}
Когда пользователь нажмёт кнопку, прилетит callback_query. Его нужно обработать и обязательно ответить через answerCallbackQuery — иначе кнопка будет крутить «часики» 30 секунд.
<?php
function handleCallback(array $cb): void {
$chatId = $cb['message']['chat']['id'];
$messageId = $cb['message']['message_id'];
$data = $cb['data'];
$callbackId = $cb['id'];
// Сразу гасим "часики" на кнопке
apiCall('answerCallbackQuery', [
'callback_query_id' => $callbackId,
'text' => 'Загружаю...', // короткий тост, опционально
]);
// Парсим callback_data — формат "action:param"
[$action, $param] = explode(':', $data, 2) + [null, null];
if ($action === 'cat') {
$titles = [
'articles' => 'Свежие статьи: PHP, JS, Bitrix',
'search' => 'Введите запрос текстом',
'contacts' => 'serzhmir79@gmail.com',
];
$text = $titles[$param] ?? 'Раздел не найден';
// Редактируем то же сообщение, не плодим новые
apiCall('editMessageText', [
'chat_id' => $chatId,
'message_id' => $messageId,
'text' => $text,
'parse_mode' => 'HTML',
'reply_markup' => json_encode([
'inline_keyboard' => [[
['text' => '« Назад', 'callback_data' => 'menu:main'],
]],
]),
]);
}
if ($action === 'menu' && $param === 'main') {
// Возврат к главному меню — редактируем текущее сообщение
apiCall('editMessageText', [
'chat_id' => $chatId,
'message_id' => $messageId,
'text' => '<b>Главное меню</b>',
'parse_mode' => 'HTML',
'reply_markup' => json_encode(buildMainMenu()),
]);
}
}
Принципы работы с callback_data:
- Лимит 64 байта — длинные параметры не лезут. Храните состояние в БД и передавайте только короткий ID.
- Делайте префикс действия (
cat:,order:,page:) — это упрощает маршрутизацию. - Не доверяйте данным: пользователь может прислать что угодно через свой клиент. Валидируйте.
- Для пагинации формат
list:page:5работает без БД — текущий номер хранится в самой кнопке.
Файлы, фото и клавиатура внизу экрана
Кроме inline-кнопок есть reply-клавиатура — заменяет системную и появляется внизу экрана. Удобна для постоянного меню, голосования по числам, выбора шага в форме:
<?php
$replyKb = [
'keyboard' => [
[['text' => '/menu'], ['text' => '/help']],
[['text' => 'Поделиться номером', 'request_contact' => true]],
[['text' => 'Прислать локацию', 'request_location' => true]],
],
'resize_keyboard' => true, // подгонка по высоте
'one_time_keyboard' => false, // не прятать после нажатия
];
sendMessage($chatId, 'Выберите действие:', [
'reply_markup' => json_encode($replyKb),
]);
// Чтобы убрать клавиатуру:
// 'reply_markup' => json_encode(['remove_keyboard' => true])
Отправка фото — отдельный метод sendPhoto. Можно слать URL картинки, file_id (если уже грузили в Telegram) или multipart-файл с диска через CURLFile:
<?php
function sendPhoto(int $chatId, string $path, string $caption = ''): array {
$ch = curl_init(BOT_API . 'sendPhoto');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => [
'chat_id' => $chatId,
'photo' => new CURLFile($path),
'caption' => $caption,
'parse_mode' => 'HTML',
],
]);
$body = curl_exec($ch);
curl_close($ch);
return json_decode($body, true);
}
Безопасность и эксплуатация
Чек-лист продакшена:
- HTTPS обязателен. Self-signed сертификаты Telegram больше не принимает — берите Let’s Encrypt.
- Проверяйте
secret_tokenчерезhash_equals, чтобы исключить таймтак-атаки. Без него на ваш URL может прийти мусор от ботов. - Не логируйте токен и тело сообщений с приватными данными. Логи имеют свойство утекать.
- Ограничивайте rate limit на стороне бота: Telegram держит лимит ~30 сообщений в секунду на бота и 1 в секунду в один чат. Используйте очередь (Redis, Beanstalkd).
- Сохраняйте
update_idи игнорируйте дубли — Telegram повторяет доставку, если не получил 200. - Owner-команды вроде
/broadcastзакрывайте проверкойchat.idпо списку администраторов. - Хостинг — подойдёт любой PHP-FPM с HTTPS. На shared-хостинге может быть ограничен
fastcgi_finish_request, проверяйте.
Локальная разработка без белого IP решается через ngrok или cloudflared tunnel: получаете публичный HTTPS-URL, вешаете на него webhook, отлаживаете в IDE.
Итог: чеклист рабочего бота
- Получили токен у
@BotFatherи положили вconfig.phpвне корня сайта. - Настроили HTTPS-домен и поставили webhook через
setWebhookсsecret_token. - Парсим
messageиcallback_query, отдаём 200 быстро. - Команды роутятся через массив обработчиков, ответы экранированы для HTML
parse_mode. - Inline-кнопки используют короткий
callback_dataс префиксом действия. - Тяжёлую работу делаем после
fastcgi_finish_requestили в очереди. - Логи пишем, дубли по
update_idотбрасываем, rate limit учитываем.
Этот каркас — основа, на которой собираются и FAQ-ответчики, и боты заявок с интеграцией в CRM, и рассыльщики уведомлений из 1С-Битрикс. Дальше остаётся подключить БД (MySQL через PDO либо Redis для состояний диалога), добавить FSM для пошаговых форм и логи в Sentry — и у вас полноценный продакшен-бот на голом PHP, без фреймворков и зависимостей.
