10 β Ma'lumotlar bazasi bilan ishlash¶
β¬ οΈ Oldingi: 09 β Middleware Β· π README Β· Keyingi: 11 β Loyiha tuzilishi va konfiguratsiya β‘οΈ
Bu bobda: Botingiz endi xabar yuboradi, klaviatura ko'rsatadi, suhbat oladi β lekin u hech narsani eslab qolmaydi. Bot qayta ishga tushsa, hamma narsa yo'qoladi. Bu bobda botga xotira beramiz: ma'lumotni ma'lumotlar bazasiga saqlaymiz. O'rganamiz: PHP'ning PDO kengaytmasi orqali bazaga ulanish (default SQLite β bitta fayl, o'rnatish shart emas; va MySQL β bir qator o'zgartirish bilan); oddiy migratsiya (jadval yaratish); repository naqsh (SQL'ni handlerdan ajratish); foydalanuvchilarni middleware orqali ro'yxatga olish (
INSERT ... ON CONFLICTβ upsert, dublikatsiz); DB ulanishini handlerga Nutgram DI resolver/konteyner orqali (yoki global) berish; va to'liq CRUD (Create/Read/Update/Delete) bot buyruqlari sifatida. SQL'ni chuqurroq o'rganish uchun ../sql/README.md, PDO va PHP asoslari uchun ../php/README.md.Halol eslatma: bu bobdagi BARCHA DB mantig'i β migratsiya, repository metodlari, upsert (dublikat yaratmaslik), middleware orqali ro'yxatga olish, resolver orqali repository in'ektsiyasi va CRUD oqimi β
PDO sqlite::memory:+Nutgram::fake()(FakeNutgram) bilan offline, tarmoqsiz va tokensiz HAQIQATAN ishga tushirilib tekshirilgan (14 ta tekshiruv, hammasi o'tdi β natijalar quyida). Jonli Telegram'ga xabar yuborish va production'da fayl/MySQL serverga real ulanish β illustrativ qism, u jonli bot ishga tushganda ko'rinadi.
Nega botga ma'lumotlar bazasi kerak?¶
Oldingi boblardagi botlar ma'lumotni xotirada (setUserData, oddiy massiv) saqladi. Bu vaqtinchalik: PHP jarayoni to'xtasa yoki bot qayta ishga tushsa β ma'lumot yo'qoladi. Haqiqiy bot esa eslab qolishi kerak:
- kim botdan foydalangan (foydalanuvchilar ro'yxati, broadcast uchun, statistika uchun);
- foydalanuvchining sozlamalari (til, bildirishnoma yoqilganmi);
- foydalanuvchi yaratgan ma'lumotlar (eslatmalar, buyurtmalar, ballar β clicker o'yinidagi tangalar kabi).
Bularning hammasi doimiy xotira β ma'lumotlar bazasini talab qiladi. Biz PHP'ning standart PDO (PHP Data Objects) kengaytmasidan foydalanamiz: u bitta umumiy API orqali turli bazalar (SQLite, MySQL, PostgreSQL, ...) bilan ishlashga imkon beradi. SQL tilini bu yerda noldan o'rgatmaymiz β agar INSERT, SELECT, WHERE tanish bo'lmasa, avval ../sql/README.md ni ko'ring. PDO'ning o'zi haqida ../php/README.md da batafsil.
Yuqoridagi diagramma butun bobning xaritasi: handler Telegram mantig'ini biladi, lekin SQL yozmaydi; u repository'ni chaqiradi; repository PDO orqali bazaga boradi; PDO esa SQLite yoki MySQL bilan ishlaydi. DB ulanishi handlerga DI resolver orqali yetadi. Endi har bir qatlamni quramiz.
PDO bilan ulanish: SQLite (default)¶
SQLite β bot loyihalari uchun eng qulay boshlang'ich tanlov: u server talab qilmaydi, ma'lumot bitta .sqlite faylda yashaydi, va PHP'da pdo_sqlite odatda yoqilgan bo'ladi. Kichik-o'rta bot uchun bu yetarli.
Ulanish β bitta PDO obyekti yaratish:
<?php
function makeDb(string $path = __DIR__ . '/storage/bot.sqlite'): PDO
{
$pdo = new PDO('sqlite:' . $path);
// Xato bo'lsa istisno (exception) tashlasin β eng muhim sozlama
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// SELECT natijasi assotsiativ massiv bo'lsin: ['name' => '...'] ko'rinishida
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
return $pdo;
}
DSN ('sqlite:' . $path) β bu baza manzili. Maxsus holat: sqlite::memory: β baza faqat xotirada yashaydi va jarayon tugashi bilan yo'qoladi. Bu test uchun ideal: har test toza, bo'sh bazadan boshlanadi. Aynan shuni bu bobni tekshirishda ishlatdik.
Ikki sozlama nega muhim?
ERRMODE_EXCEPTION'sizPDO xatolarni "jim" yutadi β noto'g'ri SQL hech narsa demay ishlamay qoladi, debug qiyinlashadi.FETCH_ASSOC'sizesa har qator ham raqamli, ham nomli kalitlar bilan keladi (ikki barobar katta massiv). Bu ikkovini doim o'rnating.
MySQL'ga o'tish: faqat DSN o'zgaradi¶
PDO'ning kuchi shunda: kod deyarli o'zgarmaydi, faqat DSN va login boshqacha bo'ladi. SQLite'dan MySQL'ga o'tish uchun ulanish funksiyasini almashtirsangiz kifoya β repository va handlerlar teginmaydi:
<?php
function makeDbMysql(): PDO
{
$host = $_ENV['DB_HOST'] ?? '127.0.0.1';
$name = $_ENV['DB_NAME'] ?? 'mybot';
$user = $_ENV['DB_USER'] ?? 'root';
$pass = $_ENV['DB_PASS'] ?? '';
$dsn = "mysql:host={$host};dbname={$name};charset=utf8mb4";
$pdo = new PDO($dsn, $user, $pass);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
return $pdo;
}
Farqlar:
- DSN endi
mysql:host=...;dbname=...;charset=utf8mb4βutf8mb4o'zbekcha matn va emoji uchun shart; - login va parol kerak β ular
.envdan o'qiladi, kodga yozilmaydi (xuddi bot tokeni kabi β 11-bobga qarang); - MySQL serveri ishlab turishi va baza oldindan yaratilgan bo'lishi kerak.
Diqqat β kichik dialekt farqlari bor. SQLite va MySQL SQL'i 95% bir xil, lekin ba'zi joylar farq qiladi: avtomatik ID (
INTEGER PRIMARY KEY AUTOINCREMENTvsINT AUTO_INCREMENT PRIMARY KEY), vaqt (CURRENT_TIMESTAMPikkovida ham bor) va upsert sintaksisi. Bu bobda SQLite sintaksisini ishlatamiz (chunki offline tekshirdik); MySQL ekvivalentlarini mos joyda eslatamiz. Dialekt tafsilotlari β ../sql/README.md.
Migratsiya: jadvallarni yaratish¶
Baza bo'sh β unda jadvallar kerak. Jadvallarni yaratuvchi SQL'ni bir joyda saqlash va botni birinchi marta ishga tushganda yurgizish β bu sodda migratsiya. (Yirik loyihalarda alohida migratsiya kutubxonasi bo'ladi; biz bu yerda eng oddiy "kerak bo'lsa yarat" usulini ko'ramiz.)
Ikki jadval quramiz: foydalanuvchilar (users) va eslatmalar (notes):
<?php
function migrate(PDO $pdo): void
{
$pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
telegram_id INTEGER NOT NULL UNIQUE,
first_name TEXT,
username TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
last_seen TEXT
)
SQL);
$pdo->exec(<<<SQL
CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
body TEXT NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(telegram_id)
)
SQL);
}
Muhim nuqtalar:
CREATE TABLE IF NOT EXISTSβ jadval allaqachon bor bo'lsa, qayta yaratmaydi (xato bermaydi). Shuning uchun har ishga tushganda chaqirsa bo'ladi.telegram_id ... UNIQUEβ bu Telegram foydalanuvchining ID'si.UNIQUEbo'lgani uchun bitta odam ikki marta yozilmaydi β bu upsertning asosi (pastda).exec()natija qaytarmaydigan SQL (CREATE, ba'zan DELETE) uchun ishlatiladi. Ma'lumot qaytaruvchi (SELECT) uchun esaquery()yokiprepare()ishlatamiz.
MySQL farqi: u yerda
id INTEGER PRIMARY KEYo'rnigaid INT AUTO_INCREMENT PRIMARY KEY,TEXT DEFAULT CURRENT_TIMESTAMPo'rnigaTIMESTAMP DEFAULT CURRENT_TIMESTAMPyoziladi. Tuzilma g'oyasi bir xil.
Repository naqsh: SQL'ni handlerdan ajrating¶
SQL'ni to'g'ridan-to'g'ri handler ichiga yozish mumkin, lekin tez orada bu chalkashlikka aylanadi: bir xil so'rov bir necha joyda takrorlanadi, o'zgartirish qiyin, test qilib bo'lmaydi. Repository naqsh β yechim: bitta jadval bilan ishlashning barcha SQL'ini bitta sinfga jamlaymiz. Handler faqat metod chaqiradi ($users->find(123)), SQL'ni ko'rmaydi.
<?php
final class UserRepository
{
public function __construct(private PDO $pdo) {}
public function upsert(int $tgId, ?string $firstName, ?string $username): void
{
$st = $this->pdo->prepare(<<<SQL
INSERT INTO users (telegram_id, first_name, username, last_seen)
VALUES (:tid, :fn, :un, CURRENT_TIMESTAMP)
ON CONFLICT(telegram_id) DO UPDATE SET
first_name = excluded.first_name,
username = excluded.username,
last_seen = CURRENT_TIMESTAMP
SQL);
$st->execute(['tid' => $tgId, 'fn' => $firstName, 'un' => $username]);
}
public function find(int $tgId): ?array
{
$st = $this->pdo->prepare('SELECT * FROM users WHERE telegram_id = :tid');
$st->execute(['tid' => $tgId]);
return $st->fetch() ?: null; // qator yo'q bo'lsa null
}
public function count(): int
{
return (int) $this->pdo->query('SELECT COUNT(*) FROM users')->fetchColumn();
}
}
Diqqat qiling β har doim prepare() + execute([':param' => $qiymat]), hech qachon qiymatni SQL satriga yopishtirmaymiz. Bu SQL in'ektsiyadan himoya: agar foydalanuvchi nomi '; DROP TABLE users; -- bo'lsa ham, u oddiy matn sifatida saqlanadi, kod sifatida ishlamaydi. Bu bot xavfsizligining eng muhim qoidalaridan biri.
upsert() ichidagi INSERT ... ON CONFLICT(telegram_id) DO UPDATE β eng muhim usul. Uni alohida ko'ramiz.
MySQL upsert farqi: SQLite'da
ON CONFLICT(telegram_id) DO UPDATE SET col = excluded.col. MySQL'da esaINSERT ... ON DUPLICATE KEY UPDATE col = VALUES(col). Mantiq bir xil: "bor bo'lsa yangila, yo'q bo'lsa qo'sh".
Foydalanuvchilarni middleware orqali ro'yxatga olish (upsert)¶
Endi eng amaliy qism. Biz har bir updateda (xabar, callback, hamma narsa) foydalanuvchini avtomatik ro'yxatga olishni xohlaymiz β bu 9-bobdagi middleware uchun mukammal vazifa: bitta joyda yozamiz, hamma handler buni "tekin" oladi.
Muammo: agar har update'da oddiy INSERT qilsak, ikkinchi xabarda dublikat yoki UNIQUE xato chiqadi. Yechim β upsert: "agar yo'q bo'lsa qo'sh, agar bor bo'lsa yangila". Aynan shu uchun telegram_id ustuniga UNIQUE qo'ydik va ON CONFLICT(telegram_id) DO UPDATE yozdik.
<?php
use SergiX44\Nutgram\Nutgram;
$users = new UserRepository($pdo); // $pdo yuqorida yaratilgan
$bot->middleware(function (Nutgram $bot, $next) use ($users) {
if ($bot->userId() !== null) { // kanal posti kabi userId yo'q holatlar bor
$u = $bot->user();
$users->upsert($bot->userId(), $u?->first_name, $u?->username);
}
$next($bot); // <-- davom: handler ishlaydi
});
Bu middleware: birinchi /startda foydalanuvchini qo'shadi, keyingi har bir update'da esa uning first_name/username va last_seen ni yangilaydi β yangi qator yaratmaydi. Buni FakeNutgram bilan tasdiqladik (quyida).
Nega
$bot->userId() !== nulltekshiruvi? Kanal posti, ba'zi service-update'larda foydalanuvchi bo'lmasligi mumkin.nullID'ni bazaga yozish mantiqsiz β shuning uchun avval tekshiramiz. Bu 9-bobdagi "before" mantig'i:$nextdan oldin ishlaydi.
DB ulanishini handlerga berish: Nutgram resolver / konteyner¶
Repository tayyor, lekin handler ichida unga qanday yetib boramiz? Uch yo'l bor. Eng tozasi β Nutgram'ning DI konteyneri (resolver): obyektni bir marta konteynerga set qilamiz, so'ng handler parametr orqali (type-hint bo'yicha) avtomatik oladi.
<?php
use SergiX44\Nutgram\Nutgram;
// Boshda bir marta: repository'larni konteynerga ro'yxatdan o'tkazamiz
$bot->getContainer()->set(PDO::class, $pdo);
$bot->getContainer()->set(UserRepository::class, new UserRepository($pdo));
$bot->getContainer()->set(NoteRepository::class, new NoteRepository($pdo));
// Handler parametr orqali oladi β Nutgram type-hint'ni ko'rib o'zi in'ektsiya qiladi
$bot->onCommand('stats', function (Nutgram $bot, UserRepository $users) {
$bot->sendMessage('Jami foydalanuvchilar: ' . $users->count());
});
Bu yerda UserRepository $users parametri β Nutgram konteynerdan o'sha obyektni topib, handlerga uzatadi. Handler new UserRepository(...) yozishi shart emas, global o'zgaruvchiga ham tayanmaydi β bu toza va test qilish oson (testda boshqa repository "ulashingiz" mumkin).
Muqobil yo'llar:
- Global /
use: kichik, bir faylli botda repository'ni shunchaki o'zgaruvchida ushlab, handlergause ($users)orqali bering (yuqoridagi middleware kabi). Sodda, lekin yiriklashganda chalkash. - Konteyner orqali qo'lda olish:
$repo = $bot->getContainer()->get(UserRepository::class);β parametr in'ektsiyasi noqulay bo'lgan joyda.
Tavsiya: o'rta-yirik botda resolver (parametr in'ektsiyasi) ni ishlating β handlerlar tashqi holatga bog'lanmaydi. Loyiha tuzilishi va konteynerni to'liqroq sozlash 11-bobda. Konteyner 9-bobdagi "ma'lumot ulashish" bilan bir mexanizm.
To'liq CRUD: eslatma boti¶
Endi hamma narsani birlashtirib, kichik eslatma botini quramiz β bu klassik CRUD (Create, Read, Update, Delete) namunasi. Har foydalanuvchi o'z eslatmalarini qo'shadi, ko'radi va o'chiradi.
Avval notes repository'si:
<?php
final class NoteRepository
{
public function __construct(private PDO $pdo) {}
// CREATE
public function add(int $userId, string $body): int
{
$st = $this->pdo->prepare('INSERT INTO notes (user_id, body) VALUES (:u, :b)');
$st->execute(['u' => $userId, 'b' => $body]);
return (int) $this->pdo->lastInsertId(); // yangi yozuv ID'si
}
// READ β faqat shu foydalanuvchining eslatmalari
/** @return array<int,array> */
public function allFor(int $userId): array
{
$st = $this->pdo->prepare('SELECT id, body FROM notes WHERE user_id = :u ORDER BY id');
$st->execute(['u' => $userId]);
return $st->fetchAll();
}
// DELETE β faqat o'z eslatmasini (user_id sharti muhim!)
public function delete(int $userId, int $id): bool
{
$st = $this->pdo->prepare('DELETE FROM notes WHERE id = :id AND user_id = :u');
$st->execute(['id' => $id, 'u' => $userId]);
return $st->rowCount() > 0; // rost = haqiqatan o'chirildi
}
}
Xavfsizlik nozikligi:
delete()vaallFor()da har doimWHERE user_id = :ubor. Busiz foydalanuvchi/del 5deb boshqa odamning eslatmasini o'chira olardi. ID'ni doim joriy foydalanuvchiga bog'lang β bu "ruxsatni baza darajasida tekshirish".
Endi handlerlar (resolver orqali NoteRepository keladi). E'tibor bering: onCommand('add {body}', ...) da {body} β buyruqdan keyingi matnni ushlab, handlerga string $body parametri sifatida beradi (04-bobdagi buyruq parametrlari):
<?php
use SergiX44\Nutgram\Nutgram;
// CREATE: /add Sut olish
$bot->onCommand('add {body}', function (Nutgram $bot, NoteRepository $notes, string $body) {
$id = $notes->add($bot->userId(), $body);
$bot->sendMessage("Eslatma #{$id} saqlandi.");
});
// READ: /list
$bot->onCommand('list', function (Nutgram $bot, NoteRepository $notes) {
$rows = $notes->allFor($bot->userId());
if (!$rows) {
$bot->sendMessage('Eslatma yoq.');
return;
}
$text = "Eslatmalar:\n";
foreach ($rows as $r) {
$text .= "#{$r['id']} {$r['body']}\n";
}
$bot->sendMessage(trim($text));
});
// DELETE: /del 3
$bot->onCommand('del {id}', function (Nutgram $bot, NoteRepository $notes, int $id) {
$bot->sendMessage(
$notes->delete($bot->userId(), $id)
? "Eslatma #{$id} o'chirildi."
: "Bunday eslatma topilmadi."
);
});
Update (CRUD'ning "U"si) ham shu naqshda: onCommand('edit {id} {body}', ...) -> UPDATE notes SET body = :b WHERE id = :id AND user_id = :u. Uni o'zingiz qo'shishni mashqlarda topshiramiz.
Foydalanuvchi avval
usersda bormi?notes.user_idusers.telegram_idga ishora qiladi. Shuning uchun ro'yxatga oluvchi upsert-middleware global bo'lishi kerak β u har handlerdan oldin ishlaydi va foydalanuvchi bazada borligini kafolatlaydi.
Bobni qanday tekshirdik (halol eslatma)¶
Yuqoridagi BARCHA DB mantig'i PDO sqlite::memory: (har test toza, bo'sh baza) + Nutgram::fake() (FakeNutgram) bilan offline, tarmoqsiz va tokensiz ishga tushirilib tasdiqlandi β 14 ta tekshiruv, hammasi o'tdi:
OK migrate: users jadvali 6 ustun
OK upsert: yangi user qoshildi
OK upsert: takror INSERT emas UPDATE (count=1)
OK upsert: first_name yangilandi
OK notes: 2 ta qoshildi
OK notes: ochirish ishladi
OK notes: ochirgandan keyin 1 ta
OK notes: yoq id ochirish false
OK middleware upsert: /start da user DB ga yozildi
OK middleware upsert: first_name DB da
OK middleware upsert: takror start dublikat yaratmadi
OK CRUD: /add eslatma yozdi (resolver bilan NoteRepository keldi)
OK CRUD: /list eslatmalarni DB dan oqib chiqardi
OK CRUD: bosh royxat 'Eslatma yoq.'
Tekshirilgan jihatlar: migratsiya (jadval va ustunlar), upsert (yangi qo'shish + takror UPDATE, dublikat yaratmaslik), repository CRUD metodlari (add/allFor/delete, jumladan "o'z ma'lumoti" sharti), middleware orqali avtomatik ro'yxatga olish, resolver orqali NoteRepository ni handler parametriga in'ektsiya qilish, va to'liq /add -> /list oqimi (jumladan bo'sh ro'yxat tarmog'i). Quyida shu test mantig'ining qisqartirilgan ko'rinishi:
<?php
use SergiX44\Nutgram\Nutgram;
$pdo = new PDO('sqlite::memory:');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
migrate($pdo);
$users = new UserRepository($pdo);
$notes = new NoteRepository($pdo);
$bot = Nutgram::fake();
$bot->getContainer()->set(NoteRepository::class, $notes);
$bot->middleware(function (Nutgram $bot, $next) use ($users) {
if ($bot->userId() !== null) {
$users->upsert($bot->userId(), $bot->user()?->first_name, $bot->user()?->username);
}
$next($bot);
});
$bot->onCommand('add {body}', function (Nutgram $bot, NoteRepository $notes, string $body) {
$id = $notes->add($bot->userId(), $body);
$bot->sendMessage("Eslatma #{$id} saqlandi.");
});
$from = ['id' => 777, 'is_bot' => false, 'first_name' => 'Vali'];
$bot->hearMessage(['text' => '/add Sut olish', 'from' => $from])->reply();
$bot->assertReplyText('Eslatma #1 saqlandi.'); // β
// upsert: foydalanuvchi DB da bor, takror /add dublikat user yaratmaydi β
Jonli Telegram'ga real sendMessage, production'da fayl-SQLite yoki MySQL serverga ulanish, ko'p jarayonli yuk ostidagi xatti-harakat (qulflash, tranzaksiya konkurensiyasi) esa jonli muhitni talab qiladi β bu qismlar illustrativ. Kod va mantiq to'g'ri; faqat real ulanish jonli muhitda ko'rinadi.
Mashqlar¶
Oson¶
makeDb()funksiyasini yozing:sqlite::memory:ga ulanib,ERRMODE_EXCEPTIONvaFETCH_ASSOCni o'rnatsin,PDOqaytarsin. BittaCREATE TABLEbilan sinab ko'ring.migrate()ga uchinchi ustun qo'shing:usersjadvaligalang TEXT DEFAULT 'uz'.PRAGMA table_info(users)orqali ustun qo'shilganini tasdiqlang.UserRepository::count()metodini yozing (SELECT COUNT(*)+fetchColumn()). Bo'sh bazada0, ikki upsert'dan keyin2qaytishini tekshiring.- Nega SQL'da qiymatlarni
prepare()/execute()orqali beramiz, satrga yopishtirmaymiz? Bir jumlada SQL in'ektsiya xavfini tushuntiring. find()metodi mavjud bo'lmagan ID uchunnullqaytarishini bittaassertbilan tekshiring.- SQLite va MySQL uchun DSN satrlarini yonma-yon yozing. Qaysi qismlar (host, parol, charset) faqat MySQL'da kerakligini belgilang.
O'rta¶
NoteRepositorygaupdate(int $userId, int $id, string $body): boolmetodini qo'shing (UPDATE ... WHERE id = :id AND user_id = :u).rowCount() > 0qaytarsin. Bittasini yangilab, qiymat o'zgarganini tekshiring./edit {id} {body}handlerini yozing va uniNoteRepository::updatega ulang. FakeNutgram bilan: avval/add, so'ng/edit 1 Yangi matn, so'ng/listda yangi matn chiqishiniassertReplyTextbilan tasdiqlang.- Upsert-middleware'ni yozing va FakeNutgram bilan tekshiring: bir foydalanuvchi
/startni ikki marta yuborsa hamusersjadvalida bitta qator bo'lishini (count() === 1) tasdiqlang. delete()dauser_idshartini olib tashlang, so'ng tiklang. Nega bu shart xavfsizlik uchun zarurligini (boshqa odamning eslatmasi) izohlang.usersjadvaligais_blocked INTEGER DEFAULT 0ustun qo'shing vaUserRepository::block(int $tgId)metodini yozing. Bloklangan foydalanuvchini middleware'da short-circuit qilib, "Siz bloklangansiz" yuboring (9-bob gate'i).NoteRepository::allForni FakeNutgram'da ikki xil foydalanuvchi bilan sinang: A foydalanuvchi qo'shgan eslatma B foydalanuvchining/listida ko'rinmasligini tasdiqlang (ma'lumot ajratilishi).
Qiyin¶
- Tranzaksiya:
NoteRepositorygaaddMany(int $userId, array $bodies): voidmetodini yozing β barcha eslatmalarni bittabeginTransaction()/commit()ichida qo'shsin, xato bo'lsarollBack(). Qisman yozilmasligini (atomik) sodda test bilan ko'rsating (masalan, ataylab bo'shbodyda istisno tashlab, hech narsa yozilmaganini tekshiring). - Statistika handleri:
/statsni yozing β resolver orqaliUserRepositoryni olib, jami foydalanuvchilar sonini va (qo'shimcha jadval bilan) bugun faol bo'lganlar sonini chiqarsin. FakeNutgram'da bir necha foydalanuvchini "ko'rsatib", sonniassertReplyTextbilan tekshiring. - Repository'ni interfeys orqali ulang:
NoteRepositoryInterfaceyarating,PdoNoteRepositoryuni amalga oshirsin. Testda esa xotiradagi soxtaInMemoryNoteRepositoryni konteynergasetqiling. Handler o'zgarmasdan ikkala implementatsiya bilan ishlashini ko'rsating (haqiqiy DI kuchi). - Sahifalash (pagination) + DB:
/listni har sahifada 5 ta eslatma ko'rsatadigan qilib,LIMIT 5 OFFSET :offbilan yozing va inline tugmalar (βοΈ / βΆοΈ) orqali sahifalarni almashtiring (07-bob callback). FakeNutgram'da 12 ta eslatma qo'shib, ikkinchi sahifada 6β10 chiqishini tekshiring.
Yechimlar
Oson 1.
<?php
function makeDb(): PDO
{
$pdo = new PDO('sqlite::memory:');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
return $pdo;
}
$pdo = makeDb();
$pdo->exec('CREATE TABLE t (id INTEGER PRIMARY KEY, x TEXT)');
$pdo->exec("INSERT INTO t (x) VALUES ('salom')");
assert($pdo->query('SELECT COUNT(*) FROM t')->fetchColumn() === 1);
Oson 2.
<?php
$pdo->exec("ALTER TABLE users ADD COLUMN lang TEXT DEFAULT 'uz'");
// yoki migrate() ichidagi CREATE ga: lang TEXT DEFAULT 'uz' qatorini qo'shing
$cols = $pdo->query('PRAGMA table_info(users)')->fetchAll();
$names = array_column($cols, 'name');
assert(in_array('lang', $names, true));
Oson 3.
<?php
public function count(): int
{
return (int) $this->pdo->query('SELECT COUNT(*) FROM users')->fetchColumn();
}
// Test:
$users = new UserRepository($pdo);
assert($users->count() === 0);
$users->upsert(1, 'A', null);
$users->upsert(2, 'B', null);
assert($users->count() === 2);
Oson 4. Qiymatni SQL satriga yopishtirsangiz, foydalanuvchi kiritgan matn SQL kodi sifatida bajarilishi mumkin β masalan '; DROP TABLE users; -- butun jadvalni o'chiradi (SQL in'ektsiya). prepare()/execute() da qiymat doim ma'lumot sifatida ketadi, hech qachon kod sifatida emas.
Oson 5.
Oson 6.
<?php
$sqlite = 'sqlite:' . __DIR__ . '/bot.sqlite'; // fayl yo'li yetarli
$mysql = 'mysql:host=127.0.0.1;dbname=mybot;charset=utf8mb4'; // host + dbname + charset
// MySQL: + foydalanuvchi va parol (new PDO($mysql, $user, $pass)); SQLite'da login kerak emas.
O'rta 1.
<?php
public function update(int $userId, int $id, string $body): bool
{
$st = $this->pdo->prepare('UPDATE notes SET body = :b WHERE id = :id AND user_id = :u');
$st->execute(['b' => $body, 'id' => $id, 'u' => $userId]);
return $st->rowCount() > 0;
}
// Test:
$notes = new NoteRepository($pdo);
$id = $notes->add(5, 'eski');
assert($notes->update(5, $id, 'yangi') === true);
assert($notes->allFor(5)[0]['body'] === 'yangi');
O'rta 2.
<?php
use SergiX44\Nutgram\Nutgram;
$bot->onCommand('edit {id} {body}', function (Nutgram $bot, NoteRepository $notes, int $id, string $body) {
$bot->sendMessage($notes->update($bot->userId(), $id, $body)
? "#{$id} yangilandi." : "Topilmadi.");
});
// Test:
$from = ['id' => 3, 'is_bot' => false, 'first_name' => 'X'];
$bot->hearMessage(['text' => '/add eski', 'from' => $from])->reply();
$bot->hearMessage(['text' => '/edit 1 yangi matn', 'from' => $from])->reply();
$bot->assertReplyText('#1 yangilandi.');
$bot->hearMessage(['text' => '/list', 'from' => $from])->reply();
$bot->assertReplyText("Eslatmalar:\n#1 yangi matn");
O'rta 3.
<?php
use SergiX44\Nutgram\Nutgram;
$users = new UserRepository($pdo);
$bot = Nutgram::fake();
$bot->middleware(function (Nutgram $bot, $next) use ($users) {
if ($bot->userId() !== null) {
$users->upsert($bot->userId(), $bot->user()?->first_name, $bot->user()?->username);
}
$next($bot);
});
$bot->onCommand('start', fn (Nutgram $bot) => $bot->sendMessage('ok'));
$msg = ['text' => '/start', 'from' => ['id' => 42, 'is_bot' => false, 'first_name' => 'Z']];
$bot->hearMessage($msg)->reply();
$bot->hearMessage($msg)->reply(); // ikkinchi marta
assert($users->count() === 1); // dublikat YO'Q
O'rta 4. user_id shartisiz DELETE FROM notes WHERE id = :id har qanday eslatmani β jumladan boshqa foydalanuvchining eslatmasini β ID bo'yicha o'chiradi. ID'lar ketma-ket (1, 2, 3...) bo'lgani uchun kimdir /del 1 deb birovning eslatmasini o'chirib yuborardi. AND user_id = :u "faqat o'zingnikini" kafolatlaydi.
O'rta 5.
<?php
$pdo->exec('ALTER TABLE users ADD COLUMN is_blocked INTEGER DEFAULT 0');
// repository:
public function block(int $tgId): void
{
$st = $this->pdo->prepare('UPDATE users SET is_blocked = 1 WHERE telegram_id = :t');
$st->execute(['t' => $tgId]);
}
public function isBlocked(int $tgId): bool
{
$st = $this->pdo->prepare('SELECT is_blocked FROM users WHERE telegram_id = :t');
$st->execute(['t' => $tgId]);
return (bool) $st->fetchColumn();
}
// middleware gate:
$bot->middleware(function (\SergiX44\Nutgram\Nutgram $bot, $next) use ($users) {
if ($bot->userId() && $users->isBlocked($bot->userId())) {
$bot->sendMessage('Siz bloklangansiz.');
return; // short-circuit
}
$next($bot);
});
O'rta 6.
<?php
$notes = new NoteRepository($pdo);
$notes->add(100, 'A ning eslatmasi');
assert(count($notes->allFor(100)) === 1);
assert(count($notes->allFor(200)) === 0); // B ning ro'yxati bo'sh β ajratilgan
Qiyin 1.
<?php
public function addMany(int $userId, array $bodies): void
{
$this->pdo->beginTransaction();
try {
$st = $this->pdo->prepare('INSERT INTO notes (user_id, body) VALUES (:u, :b)');
foreach ($bodies as $b) {
if ($b === '') {
throw new \InvalidArgumentException('bo\'sh body');
}
$st->execute(['u' => $userId, 'b' => $b]);
}
$this->pdo->commit();
} catch (\Throwable $e) {
$this->pdo->rollBack();
throw $e;
}
}
// Test (atomik): xato bo'lsa HECH NARSA yozilmasligi kerak
$notes = new NoteRepository($pdo);
try { $notes->addMany(1, ['a', '', 'c']); } catch (\Throwable) {}
assert(count($notes->allFor(1)) === 0); // rollback => 'a' ham yo'q
Qiyin 2.
<?php
use SergiX44\Nutgram\Nutgram;
$bot->getContainer()->set(UserRepository::class, $users);
$bot->onCommand('stats', function (Nutgram $bot, UserRepository $users) {
$bot->sendMessage('Jami: ' . $users->count());
});
// Test: 3 foydalanuvchini upsert qilamiz (middleware orqali yoki to'g'ridan)
foreach ([1, 2, 3] as $id) {
$bot->hearMessage(['text' => '/start', 'from' => ['id' => $id, 'is_bot' => false, 'first_name' => 'U']])->reply();
}
$bot->hearMessage(['text' => '/stats', 'from' => ['id' => 1, 'is_bot' => false, 'first_name' => 'U']])->reply();
$bot->assertReplyText('Jami: 3');
WHERE date(last_seen) = date('now') shartli COUNT qo'shing.)
Qiyin 3.
<?php
interface NoteRepositoryInterface
{
public function add(int $userId, string $body): int;
public function allFor(int $userId): array;
}
final class PdoNoteRepository implements NoteRepositoryInterface
{
public function __construct(private PDO $pdo) {}
public function add(int $userId, string $body): int { /* INSERT ... */ return 1; }
public function allFor(int $userId): array { /* SELECT ... */ return []; }
}
final class InMemoryNoteRepository implements NoteRepositoryInterface
{
private array $rows = [];
private int $seq = 0;
public function add(int $userId, string $body): int
{
$id = ++$this->seq;
$this->rows[] = ['id' => $id, 'user_id' => $userId, 'body' => $body];
return $id;
}
public function allFor(int $userId): array
{
return array_values(array_filter($this->rows, fn ($r) => $r['user_id'] === $userId));
}
}
// Handler interfeysga bog'lanadi:
$bot->onCommand('add {body}', function (\SergiX44\Nutgram\Nutgram $bot, NoteRepositoryInterface $notes, string $body) {
$bot->sendMessage('#' . $notes->add($bot->userId(), $body) . ' saqlandi.');
});
// Testda soxta implementatsiyani ulaymiz:
$bot->getContainer()->set(NoteRepositoryInterface::class, new InMemoryNoteRepository());
// Production'da: ->set(NoteRepositoryInterface::class, new PdoNoteRepository($pdo));
Qiyin 4.
<?php
// Repository:
public function page(int $userId, int $limit, int $offset): array
{
$st = $this->pdo->prepare('SELECT id, body FROM notes WHERE user_id = :u ORDER BY id LIMIT :lim OFFSET :off');
$st->bindValue(':u', $userId, PDO::PARAM_INT);
$st->bindValue(':lim', $limit, PDO::PARAM_INT);
$st->bindValue(':off', $offset, PDO::PARAM_INT);
$st->execute();
return $st->fetchAll();
}
// Handler (07-bob inline tugmalari bilan):
use SergiX44\Nutgram\Telegram\Types\Keyboard\InlineKeyboardMarkup;
use SergiX44\Nutgram\Telegram\Types\Keyboard\InlineKeyboardButton;
$perPage = 5;
$bot->onCommand('list', function (Nutgram $bot, NoteRepository $notes) use ($perPage) {
$rows = $notes->page($bot->userId(), $perPage, 0);
$kb = InlineKeyboardMarkup::make()->addRow(
InlineKeyboardButton::make('βΆοΈ', callback_data: 'page:1')
);
$bot->sendMessage($this->render($rows), reply_markup: $kb);
});
// callback: 'page:N' -> offset = N * $perPage, edit qilib yangi sahifa ko'rsatish.
// Test: 12 eslatma qo'shib, page(uid, 5, 5) 6β10-yozuvlarni qaytarishini tekshiring.
LIMIT/OFFSETuchunbindValue(..., PDO::PARAM_INT)ishlating β aks holda SQLite ularni satr deb qabul qilib xato berishi mumkin.
β¬ οΈ Oldingi: 09 β Middleware Β· π README Β· Keyingi: 11 β Loyiha tuzilishi va konfiguratsiya β‘οΈ