Переклад цього гайду зʼявиться незабаром. Поки що текст доступний російською — перемкніться на RU або читайте нижче. Відкрити російську версію

Проєкти

Заявка з сайту → Telegram

Одна из самых частых практических задач для PHP-разработчика — отправка заявок (лидов) с сайта напрямую в Telegram-чат компании или менеджеру. Это решает проблему «потерянных писем» в спаме и позволяет моментально реагировать на новые заказы.

В этой главе мы построим интерактивную систему: заявка с сайта сохраняется в базу данных, отправляется в чат менеджеров с кнопкой «Взять в работу», а при клике на кнопку статус заявки обновляется в БД, и сообщение в Telegram меняет свой вид, показывая, кто именно забрал лид.

Форма на сайте
↓
Обработчик PHP (submit.php) → Запись в БД (leads)
↓
Отправка в Telegram (sendMessage с Inline-кнопкой)
↓
Клик менеджера «Взять в работу»
↓
Webhook (webhook.php) → Обновление БД → Изменение сообщения (editMessageText)

Шаг 1. База данных и сохранение лида

Для работы нам понадобится таблица в базе данных. Мы будем использовать уникальный строковый идентификатор лида вместо обычного автоинкремента (ID 1, 2, 3). Это защитит данные от перебора конкурентами и сэкономит место в лимите callback_data (который в Telegram ограничен всего 64 байтами, туда не поместятся длинные тексты, поэтому лаконичный ID — идеальное решение).

Создадим простую таблицу leads:

CREATE TABLE leads (
id VARCHAR(14) PRIMARY KEY,
name VARCHAR(255) NOT NULL,
phone VARCHAR(50) NOT NULL,
status VARCHAR(50) DEFAULT 'new',
manager_username VARCHAR(255) DEFAULT NULL,
created_at INT NOT NULL
);

Теперь напишем скрипт submit.php, который принимает POST-запрос от формы на сайте, сохраняет лид в базу и отправляет уведомление в Telegram.

Используем функцию telegramApi(), которую мы сделали в главе Первый запрос: getMe. Здесь мы подключаем файл с ней (предположим, она вынесена в bot_lib.php) и вызываем её — полную реализацию функции cURL здесь не дублируем.

Для отправки сообщения нам также понадобится ID чата менеджеров TELEGRAM_MANAGER_CHAT. Как получить этот ID (группы или личного чата), подробно описано в главе chat_id и user_id.

<?php
// submit.php

require_once 'bot_lib.php'; // Здесь находится функция telegramApi()

// Настройки подключения к БД
$dsn = 'mysql:host=localhost;dbname=my_shop;charset=utf8mb4';
$dbUser = 'db_user';
$dbPass = 'db_password';

try {
$pdo = new PDO($dsn, $dbUser, $dbPass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
]);
} catch (PDOException $e) {
die('Ошибка подключения к БД: ' . $e->getMessage());
}

// Получаем данные из формы
$name = trim($_POST['name'] ?? '');
$phone = trim($_POST['phone'] ?? '');

if (empty($name) || empty($phone)) {
die('Заполните все поля!');
}

// Генерируем уникальный ID лида (14 символов)
$leadId = bin2hex(random_bytes(7));

// Сохраняем в БД
$stmt = $pdo->prepare('INSERT INTO leads (id, name, phone, created_at) VALUES (?, ?, ?, ?)');
$stmt->execute([$leadId, $name, $phone, time()]);

// Читаем ID чата менеджеров из окружения
$managerChatId = getenv('TELEGRAM_MANAGER_CHAT');
if (!$managerChatId) {
die('Ошибка: Не задан TELEGRAM_MANAGER_CHAT в окружении');
}

// Формируем текст сообщения с HTML-разметкой
$text = "<b>🚨 Новая заявка!</b>\n\n";
$text .= "<b>Имя:</b> " . htmlspecialchars($name, ENT_QUOTES, 'UTF-8') . "\n";
$text .= "<b>Телефон:</b> " . htmlspecialchars($phone, ENT_QUOTES, 'UTF-8') . "\n";
$text .= "<b>ID заявки:</b> <code>{$leadId}</code>";

// Подключаем Inline-клавиатуру (подробнее о структуре в главе "Inline Keyboard")
// Ссылка на главу: <a href="/telegram-bot-api/inline-keyboards">Inline Keyboard</a>
$keyboard = [
'inline_keyboard' => [[
['text' => '📥 Взять в работу', 'callback_data' => 'take:' . $leadId]
]]
];

// Отправляем запрос через наш хелпер
$response = telegramApi('sendMessage', [
'chat_id' => $managerChatId,
'text' => $text,
'parse_mode' => 'HTML',
'reply_markup' => $keyboard
]);

// Логируем результат для отладки
file_put_contents('debug.log', json_encode($response, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n", FILE_APPEND);

if (isset($response['ok']) && $response['ok'] === true) {
echo "Заявка успешно отправлена!";
} else {
echo "Ошибка при отправке в Telegram. Подробности в debug.log";
}

Шаг 2. Анализ ответа Telegram API

После вызова sendMessage Telegram возвращает JSON-ответ. Нам важно убедиться, что поле ok равно true. Чтобы посмотреть, что именно пришло от API, мы записали ответ в файл debug.log с помощью json_encode. Посмотреть лог в реальном времени в терминале можно командой:

tail -f debug.log

Вот пример успешного ответа, который запишется в лог:

{
"ok": true,
"result": {
"message_id": 1425,
"from": {
"id": 123456789,
"is_bot": true,
"first_name": "My Business Bot",
"username": "my_biz_bot"
},
"chat": {
"id": -100123456789,
"title": "Отдел продаж",
"type": "supergroup"
},
"date": 1698754321,
"text": "🚨 Новая заявка!\n\nИмя: Иван\nТелефон: +79991112233\nID заявки: a1b2c3d4e5f6g7",
"entities": [
{ "offset": 0, "length": 16, "type": "bold" },
{ "offset": 23, "length": 4, "type": "bold" }
]
}
}

Если запрос завершился ошибкой (например, неверный chat_id или бот не добавлен в группу), поле ok будет false, а в поле description вернется причина ошибки:

{
"ok": false,
"error_code": 400,
"description": "Bad Request: chat not found"
}

Шаг 3. Обработка клика «Взять в работу» на Webhook

Когда менеджер нажимает кнопку «Взять в работу», Telegram отправляет CallbackQuery на ваш Webhook. Нам нужно принять этот запрос, обновить статус лида в базе данных, изменить текст сообщения (убрать кнопку и написать, кто забрал лид) и обязательно вызвать answerCallbackQuery, чтобы у пользователя пропала анимация загрузки на кнопке.

Подробнее о настройке вебхуков читайте в главе Webhook, а о работе с кнопками — в callback_query.

Используем функцию telegramApi(), которую мы сделали в главе Первый запрос: getMe. Здесь мы подключаем её из файла bot_lib.php.

<?php
// webhook.php

require_once 'bot_lib.php'; // Здесь находится функция telegramApi()

$input = file_get_contents('php://input');
$update = json_decode($input, true);

if (!$update || !isset($update['callback_query'])) {
exit; // Игнорируем всё, кроме кликов по кнопкам
}

$callbackQuery = $update['callback_query'];
$callbackData = $callbackQuery['data'];
$chatId = $callbackQuery['message']['chat']['id'];
$messageId = $callbackQuery['message']['message_id'];
$callbackQueryId = $callbackQuery['id'];
$manager = $callbackQuery['from'];

// Проверяем, наш ли это callback
if (strpos($callbackData, 'take:') !== 0) {
exit;
}

$leadId = str_replace('take:', '', $callbackData);

// Подключение к БД
$dsn = 'mysql:host=localhost;dbname=my_shop;charset=utf8mb4';
$dbUser = 'db_user';
$dbPass = 'db_password';

try {
$pdo = new PDO($dsn, $dbUser, $dbPass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
]);
} catch (PDOException $e) {
exit; // В вебхуках ошибки БД лучше гасить молча для Telegram, но логировать у себя
}

// Проверяем статус лида в БД
$stmt = $pdo->prepare('SELECT * FROM leads WHERE id = ?');
$stmt->execute([$leadId]);
$lead = $stmt->fetch();

if (!$lead) {
// Отправляем всплывающее уведомление в Telegram, если лид не найден
telegramApi('answerCallbackQuery', [
'callback_query_id' => $callbackQueryId,
'text' => 'Ошибка: Заявка не найдена в базе.',
'show_alert' => true
]);
exit;
}

if ($lead['status'] !== 'new') {
// Если лид уже обработан другим менеджером
$takenBy = $lead['manager_username'] ? '@' . $lead['manager_username'] : 'другим менеджером';
telegramApi('answerCallbackQuery', [
'callback_query_id' => $callbackQueryId,
'text' => "Эта заявка уже обрабатывается {$takenBy}!",
'show_alert' => true
]);
exit;
}

// Обновляем статус в БД
$managerUsername = $manager['username'] ?? $manager['first_name'];
$updateStmt = $pdo->prepare("UPDATE leads SET status = 'in_progress', manager_username = ? WHERE id = ?");
$updateStmt->execute([$managerUsername, $leadId]);

// Формируем новый текст сообщения без кнопки
$newText = "<b>✅ Заявка взята в работу!</b>\n\n";
$newText .= "<b>Имя:</b> " . htmlspecialchars($lead['name'], ENT_QUOTES, 'UTF-8') . "\n";
$newText .= "<b>Телефон:</b> " . htmlspecialchars($lead['phone'], ENT_QUOTES, 'UTF-8') . "\n";
$newText .= "<b>Менеджер:</b> @" . htmlspecialchars($managerUsername, ENT_QUOTES, 'UTF-8');

// Обновляем сообщение в Telegram (удаляем кнопку и меняем текст)
// Используем метод editMessageText (подробнее в главе "Редактирование и удаление")
// Ссылка на главу: <a href="/telegram-bot-api/edit-delete">Редактирование и удаление</a>
telegramApi('editMessageText', [
'chat_id' => $chatId,
'message_id' => $messageId,
'text' => $newText,
'parse_mode' => 'HTML'
]);

// Подтверждаем обработку клика, чтобы убрать бесконечную анимацию загрузки на кнопке
telegramApi('answerCallbackQuery', [
'callback_query_id' => $callbackQueryId,
'text' => 'Вы успешно взяли заявку!'
]);

Важные нюансы безопасности и архитектуры

  • Не используйте сессии PHP: Скрипт submit.php вызывается пользователем из браузера, а webhook.php вызывается серверами Telegram. У них абсолютно разные сессии PHP. Всегда передавайте состояние через базу данных и уникальный ID лида в callback_data.
  • Защита формы: Бот отправляет всё, что придет в POST-запросе. Чтобы спамеры не завалили ваш чат мусором, обязательно защищайте форму на сайте с помощью CSRF-токенов и капчи (например, Cloudflare Turnstile или hCaptcha).
  • Лимиты API: Telegram разрешает отправлять не более 30 сообщений в секунду. Если у вас на сайте идет мощный поток заявок, отправка напрямую в обработчике формы затормозит загрузку страницы для пользователя. В таких случаях отправку нужно выносить в фоновую очередь (например, через Redis или таблицу очередей в MySQL). Подробнее об этом читайте в главе Production: очередь и retries.

Дальше: Магазин в боте