CRUD loyiha — vazifalar ro'yxati (todo)

~40 daq · PHP · Ma'lumotlar bazasi

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, match dagi bizning uchta variantimizdan biri. Shuning uchun query() 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

  1. Muddat qo'shing: muddat DATE NULL ustuni, formada <input type="date">, muddati o'tganlarni qizil rangda ko'rsating (26-dars).
  2. Qidiruv: ro'yxat tepasida qidiruv maydoni, LIKE bilan (36-dars).
  3. Sahifalash: 10 tadan ko'p bo'lsa — LIMIT va OFFSET bilan sahifalarga bo'ling.
  4. Takrorlanishni kamaytiring: index.php va tahrirlash.php dagi validatsiya bir xil. Uni vazifaniTekshir(array $data): array funksiyasiga chiqaring.
  5. OOP ga o'tkazing: VazifaRepository classi — 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