CRUD loyiha — vazifalar ro'yxati (todo)
Maqsad
Shu paytgacha o'rganganlarni bitta ishlaydigan ilovaga birlashtiramiz. Vazifalar ro'yxati (todo) — oddiy, lekin unda haqiqiy ilovaning barcha qismlari bor:
- Bazaga ulanish (PDO)
- Create — vazifa qo'shish (forma, validatsiya)
- Read — ro'yxatni ko'rsatish, filtrlash
- Update — bajarildi deb belgilash, tahrirlash
- Delete — o'chirish
- XSS dan himoya, SQL injection dan himoya, PRG usuli
Har bir qadamni o'zingiz yozib chiqing — ko'chirib olmang.
1-qadam: Baza
CREATE DATABASE todo CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE todo;
CREATE TABLE vazifalar (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
matn VARCHAR(255) NOT NULL,
bajarildi TINYINT(1) NOT NULL DEFAULT 0,
muhimlik ENUM('past', 'orta', 'yuqori') NOT NULL DEFAULT 'orta',
yaratilgan DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
2-qadam: Papka tuzilmasi
todo/
├── config.php
├── src/
│ ├── db.php
│ └── yordamchi.php
└── public/
├── index.php ← ro'yxat + qo'shish formasi
├── tahrirlash.php
└── amal.php ← belgilash va o'chirish (POST)
Ishga tushirish: todo/ papkasida php -S localhost:8000 -t public.
3-qadam: Ulanish va yordamchilar
config.php:
<?php
return [
'dsn' => 'mysql:host=localhost;dbname=todo;charset=utf8mb4',
'user' => 'root',
'parol' => '',
];
src/db.php:
<?php
declare(strict_types=1);
function db(): PDO
{
static $pdo = null;
if ($pdo === null) {
$c = require __DIR__ . '/../config.php';
$pdo = new PDO($c['dsn'], $c['user'], $c['parol'], [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]);
}
return $pdo;
}
src/yordamchi.php:
<?php
declare(strict_types=1);
function e(?string $q): string
{
return htmlspecialchars($q ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
function yonaltir(string $manzil): never
{
header("Location: $manzil", true, 303);
exit;
}
function flash(?string $xabar = null): ?string
{
if ($xabar !== null) {
$_SESSION['flash'] = $xabar;
return null;
}
$x = $_SESSION['flash'] ?? null;
unset($_SESSION['flash']);
return $x;
}
const MUHIMLIK = ['past' => 'Past', 'orta' => "O'rta", 'yuqori' => 'Yuqori'];
4-qadam: Ro'yxat va qo'shish — public/index.php
<?php
declare(strict_types=1);
session_start();
require __DIR__ . '/../src/db.php';
require __DIR__ . '/../src/yordamchi.php';
// ===== CREATE =====
$xatolar = [];
$matn = '';
$muhimlik = 'orta';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$matn = trim($_POST['matn'] ?? '');
$muhimlik = $_POST['muhimlik'] ?? 'orta';
if ($matn === '') {
$xatolar[] = "Vazifa matnini kiriting";
} elseif (mb_strlen($matn) > 255) {
$xatolar[] = "Matn 255 belgidan oshmasin";
}
if (!array_key_exists($muhimlik, MUHIMLIK)) {
$xatolar[] = "Muhimlik darajasi noto'g'ri";
}
if (!$xatolar) {
$stmt = db()->prepare('INSERT INTO vazifalar (matn, muhimlik) VALUES (?, ?)');
$stmt->execute([$matn, $muhimlik]);
flash("Vazifa qo'shildi");
yonaltir('/');
}
}
// ===== READ =====
$filtr = $_GET['filtr'] ?? 'hammasi';
$shart = match ($filtr) {
'faol' => 'WHERE bajarildi = 0',
'bajarilgan' => 'WHERE bajarildi = 1',
default => '',
};
$vazifalar = db()->query("
SELECT * FROM vazifalar
$shart
ORDER BY bajarildi ASC, FIELD(muhimlik, 'yuqori', 'orta', 'past'), yaratilgan DESC
")->fetchAll();
$statistika = db()->query('
SELECT COUNT(*) AS jami, COALESCE(SUM(bajarildi), 0) AS bajarilgan FROM vazifalar
')->fetch();
$xabar = flash();
?>
<!DOCTYPE html>
<html lang="uz">
<head>
<meta charset="UTF-8">
<title>Vazifalar</title>
<style>
body { font-family: system-ui; max-width: 640px; margin: 40px auto; padding: 0 16px; }
.bajarildi span { text-decoration: line-through; color: #999; }
.yuqori { border-left: 4px solid #e11d48; padding-left: 8px; }
li { display: flex; gap: 8px; align-items: center; margin: 6px 0; }
li span { flex: 1; }
form.inline { display: inline; }
</style>
</head>
<body>
<h1>Vazifalar</h1>
<p><?= (int) $statistika['bajarilgan'] ?> / <?= (int) $statistika['jami'] ?> bajarildi</p>
<?php if ($xabar): ?><p style="color:green"><?= e($xabar) ?></p><?php endif; ?>
<?php foreach ($xatolar as $x): ?><p style="color:red"><?= e($x) ?></p><?php endforeach; ?>
<form method="post">
<input name="matn" value="<?= e($matn) ?>" placeholder="Yangi vazifa..." maxlength="255" autofocus>
<select name="muhimlik">
<?php foreach (MUHIMLIK as $kalit => $nomi): ?>
<option value="<?= e($kalit) ?>" <?= $kalit === $muhimlik ? 'selected' : '' ?>><?= e($nomi) ?></option>
<?php endforeach; ?>
</select>
<button>Qo'shish</button>
</form>
<p>
<a href="?filtr=hammasi">Hammasi</a> ·
<a href="?filtr=faol">Faol</a> ·
<a href="?filtr=bajarilgan">Bajarilgan</a>
</p>
<?php if (!$vazifalar): ?>
<p>Vazifalar yo'q. Dam oling! 🎉</p>
<?php else: ?>
<ul style="list-style:none;padding:0">
<?php foreach ($vazifalar as $v): ?>
<li class="<?= $v['bajarildi'] ? 'bajarildi' : '' ?> <?= e($v['muhimlik']) ?>">
<form method="post" action="/amal.php" class="inline">
<input type="hidden" name="amal" value="belgilash">
<input type="hidden" name="id" value="<?= (int) $v['id'] ?>">
<button title="Belgilash"><?= $v['bajarildi'] ? '↩' : '✓' ?></button>
</form>
<span><?= e($v['matn']) ?></span>
<a href="/tahrirlash.php?id=<?= (int) $v['id'] ?>">✎</a>
<form method="post" action="/amal.php" class="inline"
onsubmit="return confirm('O\'chirilsinmi?')">
<input type="hidden" name="amal" value="ochirish">
<input type="hidden" name="id" value="<?= (int) $v['id'] ?>">
<button title="O'chirish">✕</button>
</form>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</body>
</html>
E'tibor bering:
$shart— foydalanuvchi qiymati emas,matchdagi bizning uchta variantimizdan biri. Shuning uchunquery()ga qo'shish xavfsiz.FIELD(muhimlik, 'yuqori', 'orta', 'past')— MySQL da o'z tartibimiz bilan saralash.- Har bir chiqish
e()orqali.
5-qadam: Belgilash va o'chirish — public/amal.php
Nima uchun bular havola (<a href="amal.php?ochirish=5">) emas, POST forma? Chunki GET so'rov ma'lumotni o'zgartirmasligi kerak. Brauzerlar, qidiruv botlari va havolani oldindan yuklovchi kengaytmalar GET havolalarni o'zi ochib ko'radi — va vazifalaringizni o'chirib yuboradi.
<?php
declare(strict_types=1);
session_start();
require __DIR__ . '/../src/db.php';
require __DIR__ . '/../src/yordamchi.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
exit('Faqat POST');
}
$id = filter_input(INPUT_POST, 'id', FILTER_VALIDATE_INT);
if (!$id) {
http_response_code(400);
exit("Noto'g'ri ID");
}
match ($_POST['amal'] ?? '') {
'belgilash' => db()->prepare('UPDATE vazifalar SET bajarildi = NOT bajarildi WHERE id = ?')->execute([$id]),
'ochirish' => db()->prepare('DELETE FROM vazifalar WHERE id = ?')->execute([$id]),
default => exit("Noma'lum amal"),
};
flash(($_POST['amal'] === 'ochirish') ? "Vazifa o'chirildi" : "Holat o'zgardi");
yonaltir($_SERVER['HTTP_REFERER'] ?? '/');
SET bajarildi = NOT bajarildi — bitta so'rovda 0 → 1, 1 → 0.
6-qadam: Tahrirlash — public/tahrirlash.php
<?php
declare(strict_types=1);
session_start();
require __DIR__ . '/../src/db.php';
require __DIR__ . '/../src/yordamchi.php';
$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
$stmt = db()->prepare('SELECT * FROM vazifalar WHERE id = ?');
$stmt->execute([$id]);
$vazifa = $stmt->fetch();
if (!$vazifa) {
http_response_code(404);
exit('Vazifa topilmadi');
}
$xatolar = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$vazifa['matn'] = trim($_POST['matn'] ?? '');
$vazifa['muhimlik'] = $_POST['muhimlik'] ?? 'orta';
if ($vazifa['matn'] === '' || mb_strlen($vazifa['matn']) > 255) {
$xatolar[] = "Matn 1-255 belgi bo'lsin";
}
if (!array_key_exists($vazifa['muhimlik'], MUHIMLIK)) {
$xatolar[] = "Muhimlik noto'g'ri";
}
if (!$xatolar) {
db()->prepare('UPDATE vazifalar SET matn = :matn, muhimlik = :m WHERE id = :id')
->execute(['matn' => $vazifa['matn'], 'm' => $vazifa['muhimlik'], 'id' => $id]);
flash('Saqlandi');
yonaltir('/');
}
}
?>
<!DOCTYPE html>
<html lang="uz">
<head><meta charset="UTF-8"><title>Tahrirlash</title></head>
<body style="font-family:system-ui;max-width:640px;margin:40px auto;padding:0 16px">
<h1>Tahrirlash</h1>
<?php foreach ($xatolar as $x): ?><p style="color:red"><?= e($x) ?></p><?php endforeach; ?>
<form method="post">
<input name="matn" value="<?= e($vazifa['matn']) ?>" maxlength="255" style="width:100%">
<select name="muhimlik">
<?php foreach (MUHIMLIK as $k => $n): ?>
<option value="<?= e($k) ?>" <?= $k === $vazifa['muhimlik'] ? 'selected' : '' ?>><?= e($n) ?></option>
<?php endforeach; ?>
</select>
<button>Saqlash</button>
<a href="/">Bekor qilish</a>
</form>
</body>
</html>
Tekshiruv ro'yxati
Ilovani tugatgach, o'zingiz sinab ko'ring:
- Vazifa matniga
<script>alert(1)</script>yozing — matn sifatida ko'rinishi kerak, oyna chiqmasligi kerak - Bo'sh vazifa qo'shishga urinib ko'ring — xato chiqishi kerak
- Qo'shgandan keyin F5 bosing — vazifa ikki marta qo'shilmasligi kerak
-
tahrirlash.php?id=99999— 404 chiqishi kerak -
tahrirlash.php?id=abc— 404 chiqishi kerak - DevTools da
<select>ga<option value="xakerlik">qo'shib yuboring — xato chiqishi kerak
Mustaqil vazifalar
- Muddat qo'shing:
muddat DATE NULLustuni, formada<input type="date">, muddati o'tganlarni qizil rangda ko'rsating (26-dars). - Qidiruv: ro'yxat tepasida qidiruv maydoni,
LIKEbilan (36-dars). - Sahifalash: 10 tadan ko'p bo'lsa —
LIMITvaOFFSETbilan sahifalarga bo'ling. - Takrorlanishni kamaytiring:
index.phpvatahrirlash.phpdagi validatsiya bir xil. UnivazifaniTekshir(array $data): arrayfunksiyasiga chiqaring. - OOP ga o'tkazing:
VazifaRepositoryclassi —hammasi(),top(int $id),qosh(),yangila(),ochir()metodlari bilan. Barcha SQL faqat shu classda bo'lsin.
Keyingi dars
38-dars: Parollar — password_hash, password_verify, ro'yxatdan o'tish