16-bo‘lim

PDO va ma'lumotlar bazasi

PDO bilan ulanish, tayyorlangan so'rovlar, natijani o'qish, tranzaksiyalar va SQL in'eksiyadan himoya.

🕑 11 daqiqa o‘qish 📄 640 so‘z 👁 1 marta ko‘rilgan
Ushbu bo‘lim mundarijasi
  1. Ulanish
  2. Tayyorlangan so'rovlar
  3. SQL in'eksiyani amalda ko'rish
  4. Yozish va tranzaksiyalar
  5. Natijani obyektga aylantirish
  6. Xulosa

Ma'lumotlar bazasi - veb ilovaning yuragi. PHP unga PDO (PHP Data Objects) orqali murojaat qiladi.

Bu bo'limdagi barcha misollar haqiqiy SQLite bazasida ishga tushirilgan - shuning uchun ularni o'zingiz ham darhol takrorlashingiz mumkin.

Ulanish #

PHP
// Har bir misol qat'iy turlar rejimida ishlaydi.
// Misollar uchun xotiradagi SQLite bazasidan foydalanamiz.
function bazaYarat(): PDO
{
    $pdo = new PDO("sqlite::memory:", null, null, [
        PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        PDO::ATTR_EMULATE_PREPARES   => false,
    ]);

    $pdo->exec("
        CREATE TABLE mahsulotlar (
            id     INTEGER PRIMARY KEY AUTOINCREMENT,
            nom    TEXT    NOT NULL,
            narx   INTEGER NOT NULL,
            soni   INTEGER NOT NULL DEFAULT 0,
            turkum TEXT    NOT NULL
        )
    ");

    $qatorlar = [
        ["Klaviatura",  35000000, 12, "aksessuar"],
        ["Sichqoncha",  12000000, 40, "aksessuar"],
        ["Monitor 24",  240000000, 5, "monitor"],
        ["Monitor 27",  390000000, 2, "monitor"],
        ["Quloqchin",    8900000,  0, "aksessuar"],
    ];

    $stmt = $pdo->prepare(
        "INSERT INTO mahsulotlar (nom, narx, soni, turkum) VALUES (?, ?, ?, ?)"
    );
    foreach ($qatorlar as $q) {
        $stmt->execute($q);
    }

    return $pdo;
}
PHP
// Haqiqiy loyihada ulanish shunday bo'ladi:
//
// $dsn = "mysql:host=localhost;dbname=dokon;charset=utf8mb4";
// $pdo = new PDO($dsn, $foydalanuvchi, $parol, [
//     PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
//     PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
//     PDO::ATTR_EMULATE_PREPARES   => false,
// ]);

$pdo = bazaYarat();

echo "Ulandi. Drayver: " . $pdo->getAttribute(PDO::ATTR_DRIVER_NAME) . "\n";

echo "\nMuhim uchta sozlama:\n";
$sozlamalar = [
    ["ERRMODE_EXCEPTION", "Xato bo'lsa istisno tashlaydi", "Aks holda JIMGINA false qaytaradi"],
    ["FETCH_ASSOC", "Natija assotsiativ massiv", "Standart holatda har qator IKKI marta keladi"],
    ["EMULATE_PREPARES=false", "Haqiqiy tayyorlangan so'rov", "Taqlid qilingani zaifroq"],
];

foreach ($sozlamalar as [$nom, $nima, $nimaga]) {
    printf("  %-24s %s\n", $nom, $nima);
    printf("  %-24s (%s)\n", "", $nimaga);
}

$soni = $pdo->query("SELECT COUNT(*) FROM mahsulotlar")->fetchColumn();
echo "\nJadvalda {$soni} ta yozuv bor.\n";
Natija
Ulandi. Drayver: sqlite

Muhim uchta sozlama:
  ERRMODE_EXCEPTION        Xato bo'lsa istisno tashlaydi
                           (Aks holda JIMGINA false qaytaradi)
  FETCH_ASSOC              Natija assotsiativ massiv
                           (Standart holatda har qator IKKI marta keladi)
  EMULATE_PREPARES=false   Haqiqiy tayyorlangan so'rov
                           (Taqlid qilingani zaifroq)

Jadvalda 5 ta yozuv bor.
ERRMODE_EXCEPTION ni har doim yoqing

Standart holatda PDO xatoni jimgina o'tkazib yuboradi va false qaytaradi:

PHP
$stmt = $pdo->prepare("SELECT * FROM yoq_jadval");
$stmt->execute();              // false - lekin hech narsa aytmaydi
$qatorlar = $stmt->fetchAll(); // Fatal error: null da fetchAll

Xato haqiqiy sababdan uzoqda ko'rinadi va uni topish soatlab vaqt oladi.

ERRMODE_EXCEPTION bilan esa darhol aniq xabar keladi: SQLSTATE[HY000]: General error: 1 no such table: yoq_jadval.

EMULATE_PREPARES => false ham muhim: taqlid rejimida PDO parametrlarni o'zi so'rovga qo'shadi, haqiqiy rejimda esa ularni bazaga alohida yuboradi.

Ikkinchisi xavfsizroq va turlarni ham to'g'ri saqlaydi: taqlid rejimida LIMIT ? ga son bergan bo'lsangiz ham u qo'shtirnoqda ketadi va MySQL xato beradi.

Va charset=utf8mb4 ni DSN da albatta ko'rsating - aks holda emoji va ba'zi belgilar buziladi.

Tayyorlangan so'rovlar #

PHP
$pdo = bazaYarat();

// 1. Pozitsion parametrlar - ? belgisi
$stmt = $pdo->prepare("SELECT nom, narx FROM mahsulotlar WHERE turkum = ? AND soni > ?");
$stmt->execute(["aksessuar", 0]);

echo "Pozitsion parametrlar bilan:\n";
foreach ($stmt as $q) {
    printf("  %-14s %12s\n", $q["nom"], number_format($q["narx"] / 100, 0, ",", " "));
}

// 2. Nomli parametrlar - o'qishga qulayroq
echo "\nNomli parametrlar bilan:\n";
$stmt = $pdo->prepare("
    SELECT nom, narx, soni
    FROM mahsulotlar
    WHERE narx BETWEEN :eng_kam AND :eng_kop
    ORDER BY narx DESC
");
$stmt->execute(["eng_kam" => 10000000, "eng_kop" => 300000000]);

foreach ($stmt->fetchAll() as $q) {
    printf("  %-14s %12s  %2d dona\n", $q["nom"],
           number_format($q["narx"] / 100, 0, ",", " "), $q["soni"]);
}

// 3. Bitta qiymat olish
echo "\n";
$stmt = $pdo->prepare("SELECT COUNT(*) FROM mahsulotlar WHERE turkum = ?");
$stmt->execute(["monitor"]);
echo "Monitorlar soni: " . $stmt->fetchColumn() . "\n";

// 4. Bitta qator olish
$stmt = $pdo->prepare("SELECT * FROM mahsulotlar WHERE id = ?");
$stmt->execute([1]);
$bitta = $stmt->fetch();
echo "id=1: " . ($bitta === false ? "topilmadi" : $bitta["nom"]) . "\n";

$stmt->execute([999]);
$yoq = $stmt->fetch();
echo "id=999: " . ($yoq === false ? "topilmadi" : $yoq["nom"]) . "\n";
Natija
Pozitsion parametrlar bilan:
  Klaviatura          350 000
  Sichqoncha          120 000

Nomli parametrlar bilan:
  Monitor 24        2 400 000   5 dona
  Klaviatura          350 000  12 dona
  Sichqoncha          120 000  40 dona

Monitorlar soni: 2
id=1: Klaviatura
id=999: topilmadi
Nima uchun tayyorlangan so'rov xavfsiz XAVFLI: satrni birlashtirish $sql = "SELECT * FROM users WHERE ism = '" . $_GET["ism"] . "'"; Foydalanuvchi yozadi: ' OR '1'='1 Bazaga boradi: ... WHERE ism = '' OR '1'='1' Natija: BARCHA foydalanuvchilar qaytadi. Parol tekshiruvi chetlab o'tiladi. XAVFSIZ: tayyorlangan so'rov $stmt = $pdo->prepare("SELECT * FROM users WHERE ism = ?"); $stmt->execute([$_GET["ism"]]); 1. SO'ROV bazaga yuboriladi (tuzilma) 2. QIYMAT alohida yuboriladi (ma'lumot) Baza so'rov tuzilmasini ALLAQACHON tahlil qilgan. Keyin kelgan qiymat hech qachon SQL buyrug'iga aylana olmaydi. Diqqat: jadval va ustun nomlarini parametr qilib bo'lmaydi - ular uchun OQ RO'YXAT kerak.
Tuzilma va ma'lumot alohida yuboriladi - in'eksiya shuning uchun ishlamaydi

SQL in'eksiyani amalda ko'rish #

PHP
$pdo = bazaYarat();

$hujum = "' OR '1'='1";

// 1. XAVFLI usul - satrni birlashtirish
$xavfliSql = "SELECT nom FROM mahsulotlar WHERE nom = '" . $hujum . "'";
echo "Xavfli so'rov:\n  {$xavfliSql}\n";

$natija = $pdo->query($xavfliSql)->fetchAll();
echo "  Qaytgan qatorlar: " . count($natija) . " ta  <- HAMMASI!\n";

// 2. XAVFSIZ usul - tayyorlangan so'rov
echo "\nXavfsiz so'rov:\n  SELECT nom FROM mahsulotlar WHERE nom = ?\n";
$stmt = $pdo->prepare("SELECT nom FROM mahsulotlar WHERE nom = ?");
$stmt->execute([$hujum]);
$natija = $stmt->fetchAll();
echo "  Qaytgan qatorlar: " . count($natija) . " ta  <- to'g'ri\n";
echo "  (chunki bunday NOMLI mahsulot yo'q)\n";

echo "\n";
echo "Farq shundaki, ikkinchi holatda \"' OR '1'='1\" - bu\n";
echo "SQL buyrug'i emas, shunchaki QIDIRILAYOTGAN MATN.\n";
Natija
Xavfli so'rov:
  SELECT nom FROM mahsulotlar WHERE nom = '' OR '1'='1'
  Qaytgan qatorlar: 5 ta  <- HAMMASI!

Xavfsiz so'rov:
  SELECT nom FROM mahsulotlar WHERE nom = ?
  Qaytgan qatorlar: 0 ta  <- to'g'ri
  (chunki bunday NOMLI mahsulot yo'q)

Farq shundaki, ikkinchi holatda "' OR '1'='1" - bu
SQL buyrug'i emas, shunchaki QIDIRILAYOTGAN MATN.
Jadval va ustun nomlarini parametrlashtirib bo'lmaydi

Tayyorlangan so'rov faqat qiymatlarni himoya qiladi. Bu ishlamaydi:

PHP
$stmt = $pdo->prepare("SELECT * FROM mahsulotlar ORDER BY ? ?");
$stmt->execute([$ustun, $yonalish]);      // XATO

Sabab: baza so'rovni oldindan tahlil qiladi va o'shanda jadval bilan ustun nomlari ma'lum bo'lishi kerak.

Shuning uchun ular uchun oq ro'yxat (8-bo'lim) ishlatiladi:

PHP
$ruxsat = ["nom", "narx", "soni"];
$ustun = in_array($_GET["tartib"] ?? "", $ruxsat, true)
    ? $_GET["tartib"]
    : "nom";

$yonalish = ($_GET["yonalish"] ?? "") === "desc" ? "DESC" : "ASC";

$sql = "SELECT * FROM mahsulotlar ORDER BY {$ustun} {$yonalish}";

Bu yerda {$ustun} to'g'ridan-to'g'ri qo'yilyapti - va bu xavfsiz, chunki uning qiymati faqat uch variantdan biri bo'lishi mumkin.

Xuddi shu holat LIMIT uchun ham amal qiladi ba'zi drayverlarda: sonni (int) ga aylantirib qo'ying.

Yozish va tranzaksiyalar #

PHP
$pdo = bazaYarat();

// INSERT va oxirgi id
$stmt = $pdo->prepare(
    "INSERT INTO mahsulotlar (nom, narx, soni, turkum) VALUES (:nom, :narx, :soni, :turkum)"
);
$stmt->execute([
    "nom" => "Veb-kamera", "narx" => 45000000, "soni" => 8, "turkum" => "aksessuar",
]);
$yangiId = (int) $pdo->lastInsertId();
echo "Qo'shildi, id = {$yangiId}\n";

// UPDATE va ta'sirlangan qatorlar
$stmt = $pdo->prepare("UPDATE mahsulotlar SET narx = narx * 9 / 10 WHERE turkum = ?");
$stmt->execute(["monitor"]);
echo "Chegirma qo'llandi: " . $stmt->rowCount() . " qatorga\n";

// DELETE
$stmt = $pdo->prepare("DELETE FROM mahsulotlar WHERE soni = 0");
$stmt->execute();
echo "O'chirildi: " . $stmt->rowCount() . " qator\n";

echo "\n";
// TRANZAKSIYA - yo hammasi, yo hech nima
echo "Tranzaksiya sinovi (ombordan ikkita mahsulot ko'chirish):\n";

try {
    $pdo->beginTransaction();

    $kamaytir = $pdo->prepare("UPDATE mahsulotlar SET soni = soni - ? WHERE id = ?");
    $kamaytir->execute([5, 1]);
    echo "  1-qadam: 5 dona ayirildi\n";

    // Ikkinchi qadamda ataylab xato keltiramiz
    $qoldiq = $pdo->query("SELECT soni FROM mahsulotlar WHERE id = 1")->fetchColumn();
    if ($qoldiq < 10) {
        throw new RuntimeException("Omborda yetarli emas (qoldi: {$qoldiq})");
    }

    $pdo->commit();
    echo "  Tasdiqlandi\n";
} catch (Throwable $x) {
    $pdo->rollBack();
    echo "  XATO: " . $x->getMessage() . "\n";
    echo "  Orqaga qaytarildi\n";
}

$qoldiq = $pdo->query("SELECT soni FROM mahsulotlar WHERE id = 1")->fetchColumn();
echo "\nYakuniy qoldiq: {$qoldiq} (o'zgarmadi - tranzaksiya bekor qilindi)\n";
Natija
Qo'shildi, id = 6
Chegirma qo'llandi: 2 qatorga
O'chirildi: 1 qator

Tranzaksiya sinovi (ombordan ikkita mahsulot ko'chirish):
  1-qadam: 5 dona ayirildi
  XATO: Omborda yetarli emas (qoldi: 7)
  Orqaga qaytarildi

Yakuniy qoldiq: 12 (o'zgarmadi - tranzaksiya bekor qilindi)
Tranzaksiyani try/catch/finally bilan o'rang

Tranzaksiya atomarlikni kafolatlaydi: bir necha amal birga bajariladi yoki birortasi ham bajarilmaydi.

Klassik misol - pul o'tkazish:

PHP
$pdo->beginTransaction();
try {
    $ayir->execute([$summa, $kimdan]);
    $qosh->execute([$summa, $kimga]);
    $pdo->commit();
} catch (Throwable $x) {
    $pdo->rollBack();
    throw $x;                    // yuqoriga uzatamiz
}

Birinchi UPDATE bajarilib, ikkinchisi xato bersa - pul yo'q bo'lib ketardi. rollBack() buni oldini oladi.

Uch muhim eslatma:

EslatmaSabab
rollBack() dan keyin istisnoni qayta tashlangAks holda xato yashiriladi
DDL (CREATE, ALTER) tranzaksiyani avtomatik tasdiqlaydiMySQL da
Tranzaksiyani qisqa tutingUzun tranzaksiya qatorlarni qulflaydi

MyISAM jadval turi tranzaksiyani umuman qo'llab-quvvatlamaydi - MySQL da InnoDB ishlating.

Natijani obyektga aylantirish #

PHP
$pdo = bazaYarat();

final class Mahsulot
{
    public function __construct(
        public readonly int $id,
        public readonly string $nom,
        public readonly int $narx,
        public readonly int $soni,
        public readonly string $turkum,
    ) {}

    public function narxMatni(): string
    {
        return number_format($this->narx / 100, 0, ",", " ") . " so'm";
    }

    public function omborda(): bool
    {
        return $this->soni > 0;
    }
}

$stmt = $pdo->query("SELECT id, nom, narx, soni, turkum FROM mahsulotlar ORDER BY narx");

$mahsulotlar = [];
foreach ($stmt as $q) {
    $mahsulotlar[] = new Mahsulot(
        (int) $q["id"],
        $q["nom"],
        (int) $q["narx"],
        (int) $q["soni"],
        $q["turkum"],
    );
}

printf("%-14s %14s %8s %s\n", "NOM", "NARX", "SONI", "HOLAT");
echo str_repeat("-", 50) . "\n";
foreach ($mahsulotlar as $m) {
    printf("%-14s %14s %8d %s\n", $m->nom, $m->narxMatni(), $m->soni,
           $m->omborda() ? "mavjud" : "tugagan");
}

echo "\n";
$jami = array_sum(array_map(
    static fn(Mahsulot $m): int => $m->narx * $m->soni,
    $mahsulotlar
));
echo "Ombor qiymati: " . number_format($jami / 100, 0, ",", " ") . " so'm\n";
echo "Obyektlar soni: " . count($mahsulotlar) . "\n";
Natija
NOM                      NARX     SONI HOLAT
--------------------------------------------------
Quloqchin         89 000 so'm        0 tugagan
Sichqoncha       120 000 so'm       40 mavjud
Klaviatura       350 000 so'm       12 mavjud
Monitor 24     2 400 000 so'm        5 mavjud
Monitor 27     3 900 000 so'm        2 mavjud

Ombor qiymati: 28 800 000 so'm
Obyektlar soni: 5
Nima uchun massiv emas, obyekt

Bazadan kelgan qiymatlar hammasi satr bo'lishi mumkin (drayverga bog'liq). Yuqoridagi kodda (int) $q["narx"] aylantirish aynan shuning uchun.

Obyektga aylantirish uch foyda beradi:

FoydaIzoh
Turlar to'g'riint - int, string - string
Xatti-harakat bir joydanarxMatni() takrorlanmaydi
Muharrir yordam beradi$m->nmo xato darhol ko'rinadi

PDO ning FETCH_CLASS rejimi ham bor:

PHP
$stmt->fetchAll(PDO::FETCH_CLASS, Mahsulot::class);

Lekin u konstruktorni chetlab o'tib xossalarga to'g'ridan-to'g'ri yozadi - ya'ni readonly va tekshiruvlar ishlamaydi. Shuning uchun yuqoridagi oshkora usul afzalroq.

Katta loyihalarda bu ish ORM (Doctrine, Eloquent) tomonidan bajariladi.

Amaliy topshiriq
  1. SQLite bazasiga ulaning va jadval yarating.
  2. ERRMODE_EXCEPTION siz mavjud bo'lmagan jadvalga murojaat qiling.
  3. Uni yoqing va farqni ko'ring.
  4. Pozitsion va nomli parametrlar bilan ikkita so'rov yozing.
  5. ' OR '1'='1 ni satr birlashtirish bilan sinang.
  6. Xuddi shuni tayyorlangan so'rov bilan takrorlang.
  7. ORDER BY ? yozib ko'ring - ishladimi?
  8. Saralash ustuni uchun oq ro'yxat qo'llang.
  9. Tranzaksiya ichida xato keltirib, rollBack() ni sinang.
  10. Natija qatorlarini obyektlarga aylantiring.

Xulosa #

  • PDO ni uchta sozlama bilan yarating: ERRMODE_EXCEPTION, FETCH_ASSOC, EMULATE_PREPARES=false.
  • Standart holatda PDO xatoni jimgina o'tkazadi - xato haqiqiy sababdan uzoqda ko'rinadi.
  • DSN da charset=utf8mb4 ni ko'rsating.
  • Tayyorlangan so'rov tuzilma va ma'lumotni alohida yuboradi - in'eksiya shuning uchun ishlamaydi.
  • Foydalanuvchi qiymatini SQL ga hech qachon birlashtirmang.
  • Jadval va ustun nomlarini parametrlashtirib bo'lmaydi - ular uchun oq ro'yxat kerak.
  • fetch() topilmasa false qaytaradi - === false bilan tekshiring.
  • lastInsertId() yangi yozuvning id sini, rowCount() ta'sirlangan qatorlar sonini beradi.
  • Tranzaksiya atomarlikni kafolatlaydi: yo hammasi, yo hech nima.
  • rollBack() dan keyin istisnoni qayta tashlang; tranzaksiyani qisqa tuting.
  • MySQL da InnoDB ishlating - MyISAM tranzaksiyani qo'llab-quvvatlamaydi.
  • Natijani obyektga aylantiring: turlar to'g'ri bo'ladi va xatti-harakat bir joyda turadi.

Keyingi bo'limda xavfsizlikni to'liq ko'rib chiqamiz - XSS, CSRF va parol xeshlash.

Xatolik topdingizmi?

Imlo xatosi, ishlamaydigan kod yoki noto‘g‘ri ma‘lumotni ko‘rsangiz - bizga xabar bering. Har bir xabar administrator tomonidan ko‘rib chiqiladi.