PHP и фреймворки
БД и состояния (FSM)
Как хранить данные бота в БД и реализовывать многошаговые сценарии через FSM. Примеры схемы таблиц, PHP/PDO и Laravel подходы, обработка конкурентности и TTL состояний.
Когда бот выходит за рамки простых команд «запрос — ответ», появляется необходимость хранить данные: настройки пользователей, корзины, прогресс многошаговых сценариев. Для этого нужны БД и конечный автомат (FSM).
Зачем БД в боте
Telegram не хранит контекст диалогов за вас. chat_id и user_id — единственные стабильные идентификаторы. Всё остальное — ваша ответственность:
- Профили пользователей: язык, таймзона, согласия на рассылки.
- Состояние FSM: на каком шаге сценария находится пользователь.
- Бизнес-данные: корзина, черновики постов, выбранные фильтры.
- Логи и аналитика:
update_id, входящиеcallback_data, ошибки.
Для небольших проектов достаточно SQLite, для продакшена — PostgreSQL или MySQL. В Laravel используйте Eloquent, в чистом PHP — PDO с подготовленными выражениями.
FSM: паттерн для многошаговых сценариев
FSM (Finite State Machine) моделирует диалог как набор состояний и переходов. Пример: оформление заказа — idle → awaiting_address → awaiting_phone → confirming → completed.
Храните текущее состояние в БД, привязанное к user_id (или chat_id для групп). При каждом update читайте состояние, решаете, какой хендлер вызвать, пишете новое состояние.
Схема таблицы состояний
CREATE TABLE user_states (
user_id BIGINT PRIMARY KEY,
state VARCHAR(64) NOT NULL DEFAULT 'idle',
payload JSON DEFAULT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Поле payload (JSON) хранит промежуточные данные шага: выбранный товар, введённый адрес, ID сообщения для редактирования.
Минимальный FSM-хендлер на PHP (PDO)
<?php
require 'vendor/autoload.php';
$pdo = new PDO('sqlite:bot.db');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
function getState(PDO $pdo, int $userId): array {
$stmt = $pdo->prepare('SELECT state, payload FROM user_states WHERE user_id = ?');
$stmt->execute([$userId]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
return $row ?: ['state' => 'idle', 'payload' => null];
}
function setState(PDO $pdo, int $userId, string $state, ?array $payload = null): void {
$json = $payload ? json_encode($payload) : null;
$pdo->prepare(
'INSERT INTO user_states (user_id, state, payload) VALUES (?, ?, ?)'
. ' ON CONFLICT(user_id) DO UPDATE SET state = excluded.state, payload = excluded.payload, updated_at = CURRENT_TIMESTAMP'
)->execute([$userId, $state, $json]);
}
// Пример обработки
$update = json_decode(file_get_contents('php://input'), true);
$message = $update['message'] ?? $update['callback_query']['message'] ?? null;
if (!$message) exit;
$userId = $message['from']['id'];
$text = $message['text'] ?? '';
$current = getState($pdo, $userId);
switch ($current['state']) {
case 'idle':
if ($text === '/order') {
sendMessage($userId, 'Введите адрес доставки:');
setState($pdo, $userId, 'awaiting_address');
}
break;
case 'awaiting_address':
$payload = ['address' => $text];
sendMessage($userId, 'Адрес принят. Введите телефон:');
setState($pdo, $userId, 'awaiting_phone', $payload);
break;
case 'awaiting_phone':
$payload = array_merge($current['payload'] ?? [], ['phone' => $text]);
sendMessage($userId, "Проверьте данные: {$payload['address']}, {$payload['phone']}. Подтвердить? /yes /no");
setState($pdo, $userId, 'confirming', $payload);
break;
case 'confirming':
if ($text === '/yes') {
// сохраняем заказ
setState($pdo, $userId, 'idle');
sendMessage($userId, 'Заказ оформлен!');
} elseif ($text === '/no') {
setState($pdo, $userId, 'idle');
sendMessage($userId, 'Отменено.');
}
break;
}
function sendMessage(int $chatId, string $text): void {
$token = getenv('BOT_TOKEN');
$ch = curl_init("https://api.telegram.org/bot{$token}/sendMessage");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(['chat_id' => $chatId, 'text' => $text]),
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
]);
$res = curl_exec($ch);
if ($res === false) {
error_log('cURL error: ' . curl_error($ch));
}
curl_close($ch);
}
FSM в Laravel
В Laravel удобно оформить FSM как отдельный сервис с Eloquent-моделью UserState. Используйте updateOrCreate для атомарного сохранения. Хендлеры выносите в отдельные классы (Action-паттерн), диспетчерите по полю state. Middleware может автоматически подгружать состояние в $request->userState.
Важные нюансы
- Конкурентность: пользователь может прислать два сообщения почти одновременно. Используйте транзакции БД или
SELECT ... FOR UPDATE, чтобы не потерять переход состояния. - TTL состояний: добавляйте cron-задачу, сбрасывающую зависшие состояния (например, старше 24 часов) в
idle. - Callback-кнопки: в
callback_dataкладите только идентификатор действия (order_confirm), а не весь контекст — контекст уже в БД. - Группы: в групповом чате состояние лучше привязывать к паре
chat_id + user_idили кmessage_thread_idдля тем.
Готовая реализация FSM есть в пакете botman/botman (драйвер botman/driver-telegram) и в irazasyed/telegram-bot-sdk — они абстрагируют хранение состояний за интерфейсом Conversation.