طَبِّقْهاتعلّم المبدأ، وطبّقه بيدكمقرر 132 · تقنيات تطوير الواجهات الخلفية
الدرس 20 · الوحدة 9: ربط الكود من جانب الخادم مع قواعد البيانات

الإضافة والتعديل والحذف بأمان

افصل الأمر عن البيانات، وتصبح قاعدة بياناتك في أمان. ⏱ نحو 40 دقيقة.

بعد هذا الدرس ستستطيع أن:
  • تشرح حقن SQL وتكتشفه في الكود.
  • تكتب استعلامات مُعدّة بأماكن مرقّمة ومسمّاة.
  • تنفّذ الإضافة والتعديل والحذف، وتستخدم lastInsertId و rowCount.
  • تجمع عدة أوامر في معاملة بـ beginTransaction و commit و rollBack.

١أخطر سطر في الواجهة الخلفية

صفحة بحث تأخذ كلمة من المستخدم وتضعها داخل الاستعلام بدمج النصوص. تبدو بريئة:

<?phprequire __DIR__ . '/db.php';$q = $_GET['q'] ?? ''; // ✗ خطر: نص المستخدم يصير جزءًا من أمر SQL$sql = "SELECT name, phone FROM customers WHERE name = '$q'";echo "<p dir='ltr'><small>$sql</small></p>"; foreach ($pdo->query($sql) as $r) {    echo "<p>{$r['name']} — {$r['phone']}</p>";}

الطلب الأول بحث عادي. والثاني كتب فيه المهاجم: x' OR '1'='1 . علامة التنصيص أغلقت النص، ثم صار الشرط «أو 1 يساوي 1» صحيحًا دائمًا، فعُرضت أرقام جوالات كل العملاء!

هذا حقن SQL SQL Injection، من أخطر الثغرات وأكثرها انتشارًا لسنوات طويلة. وبالأسلوب نفسه قد يحذف المهاجم جداول، أو يدخل بلا كلمة مرور. والسبب واحد: خلطنا الأمر بالبيانات.

٢الحل: الاستعلام المُعدّ

الاستعلام المُعدّ Prepared statement يفصل الأمر عن البيانات: نرسل الأمر أولًا وفيه أماكن فارغة ?، ثم نرسل القيم منفصلة. قاعدة البيانات تعامل القيم بيانات فقط مهما كان فيها:

<?phprequire __DIR__ . '/db.php';$q = $_GET['q'] ?? ''; // ✓ آمن: الأمر ثابت، والقيمة تُرسل منفصلة$stmt = $pdo->prepare('SELECT name, phone FROM customers WHERE name = ?');$stmt->execute([$q]);$rows = $stmt->fetchAll(); echo "<p>النتائج: " . count($rows) . "</p>";foreach ($rows as $r) {    echo "<p>{$r['name']} — {$r['phone']}</p>";}

الهجوم نفسه الآن مجرد اسم غريب لا يوجد عميل به: صفر نتائج.

الشكلمثال
أماكن مرقّمة ?$s = $pdo->prepare('SELECT * FROM products WHERE price < ? AND stock > ?');
$s->execute([50, 0]);
أماكن مسمّاة :name$s = $pdo->prepare('SELECT * FROM products WHERE category = :cat');
$s->execute(['cat' => $cat]);

٣الإضافة: INSERT

<?phprequire __DIR__ . '/db.php'; $name = trim($_POST['name'] ?? '');$price = filter_var($_POST['price'] ?? '', FILTER_VALIDATE_FLOAT);$cat = trim($_POST['category'] ?? ''); if ($name === '' || $cat === '' || $price === false || $price <= 0) {    http_response_code(422);    exit('بيانات المنتج غير صحيحة');} $stmt = $pdo->prepare('INSERT INTO products (name, category, price, stock) VALUES (?, ?, ?, 0)');$stmt->execute([$name, $cat, $price]);$id = $pdo->lastInsertId();                 // رقم الصف الجديد http_response_code(201);echo "أُضيف المنتج رقم $id: $name";

٤التعديل والحذف: UPDATE و DELETE

و rowCount() تخبرك كم صفًّا تأثّر، فتعرف إن كان العنصر موجودًا أصلًا:

<?phprequire __DIR__ . '/db.php';$id = (int) ($_POST['id'] ?? 0); if (($_POST['action'] ?? '') === 'restock') {    $stmt = $pdo->prepare('UPDATE products SET stock = stock + :qty WHERE id = :id');    $stmt->execute(['qty' => (int) ($_POST['qty'] ?? 0), 'id' => $id]);} else {    $stmt = $pdo->prepare('DELETE FROM products WHERE id = ? AND stock = 0');    $stmt->execute([$id]);} if ($stmt->rowCount() === 0) {    http_response_code(404);    exit('لم يتأثر أي منتج');}$p = $pdo->prepare('SELECT name, stock FROM products WHERE id = ?');$p->execute([$id]);$row = $p->fetch();echo $row ? "{$row['name']}: المخزون {$row['stock']}" : "حُذف المنتج $id";

الحذف مشروط بأن يكون المخزون صفرًا (قاعدة عمل داخل WHERE)، فالمنتج 1 لم يُحذف لأن مخزونه صار 10+.

٥المعاملات: كل شيء أو لا شيء

تسجيل طلب شراء يحتاج عدة أوامر: إنشاء الطلب، وإضافة عناصره، وإنقاص المخزون. لو فشل الأمر الثالث بعد نجاح الأولين، يبقى طلب بلا خصم مخزون! الحل المعاملة Transaction التي عرفتها في 121، لكن من PHP:

<?phprequire __DIR__ . '/db.php';$pid = (int) ($_POST['product'] ?? 0);$qty = (int) ($_POST['qty'] ?? 1); try {    $pdo->beginTransaction();     $s = $pdo->prepare('UPDATE products SET stock = stock - ? WHERE id = ? AND stock >= ?');    $s->execute([$qty, $pid, $qty]);    if ($s->rowCount() === 0) {        throw new RuntimeException('الكمية غير متوفرة');    }    $pdo->prepare("INSERT INTO orders (customer_id, order_date) VALUES (1, date('now'))")->execute();    $orderId = $pdo->lastInsertId();    $pdo->prepare('INSERT INTO order_items (order_id, product_id, qty, price) SELECT ?, id, ?, price FROM products WHERE id = ?')        ->execute([$orderId, $qty, $pid]);     $pdo->commit();                               // ثبّت كل شيء معًا    http_response_code(201);    echo "تم الطلب رقم $orderId";} catch (Throwable $e) {    $pdo->rollBack();                             // تراجع عن كل شيء    http_response_code(409);    echo 'لم يتم الطلب: ' . $e->getMessage();}

الشرط stock >= ? داخل UPDATE يجعل الفحص والخصم أمرًا واحدًا، فيمنع حالة السباق من الدرس 15. وإن لم يتأثر صف رمينا استثناءً فتراجعت المعاملة كلها.

توقّع وتحقّق

سبب حقن SQL:

خلط الأمر بالبيانات.

الحماية من حقن SQL:

الاستعلامات المُعدّة.

رقم الصف الذي أُضيف للتو:

بعد INSERT مباشرة.

في المعاملة، إن فشل أمر من الأوامر:

كل شيء أو لا شيء.

الخلاصة

تحدٍّ برمجي: تحديث السعر بأمان

متقدم

الملف price.php فيه ثغرة حقن SQL، ولا يتحقق من شيء. أصلحه.

المطلوب:
  • يستقبل POST فيه id و price.
  • السعر عدد موجب، وإلا 422 والنص سعر غير صحيح.
  • التحديث باستعلام مُعدّ. إن لم يتأثر صف: 404 والنص المنتج غير موجود.
  • عند النجاح: 200 والنص تم: قهوة عربية بسعر 40 (اسم المنتج من القاعدة، والسعر الجديد).
  • لا دمج لأي قيمة من الطلب داخل نص SQL.
👀 اعرض الاستجابة المطلوبة
✓ التحقق الآلي: يرسل المختبر هذه الطلبات إلى خادمك بالترتيب ثم يفحص الاستجابات
  1. POST /price.php (id=1&price=40)
  2. POST /price.php (id=1&price=-5)
  3. POST /price.php (id=999&price=10)
  4. POST /price.php (id=1 OR 1=1&price=1)
  5. POST /price.php (id=2&price=abc)
  • الكود يعمل بلا أخطاء أو تحذيرات
  • id=1 و price=40 ← تم: قهوة عربية بسعر 40
  • سعر سالب ← 422
  • منتج غير موجود ← 404
  • هجوم id=1 OR 1=1 لا يغيّر كل الأسعار
  • سعر نصي ← 422
  • تستخدم prepare و execute
  • لا متغيرات داخل نص SQL

💡 تلميح 1
$stmt = $pdo->prepare('UPDATE products SET price = ? WHERE id = ?'); $stmt->execute([$price, $id]);
💡 تلميح 2
rowCount() بعد execute تخبرك هل وُجد المنتج.

حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.

<?phprequire __DIR__ . '/db.php';$id = (int) ($_POST['id'] ?? 0);$price = filter_var($_POST['price'] ?? '', FILTER_VALIDATE_FLOAT); if ($price === false || $price <= 0) {    http_response_code(422);    exit('سعر غير صحيح');}$stmt = $pdo->prepare('UPDATE products SET price = ? WHERE id = ?');$stmt->execute([$price, $id]);if ($stmt->rowCount() === 0) {    http_response_code(404);    exit('المنتج غير موجود');}$s = $pdo->prepare('SELECT name, price FROM products WHERE id = ?');$s->execute([$id]);$p = $s->fetch();echo "تم: {$p['name']} بسعر {$p['price']}";
انتقل إلى تقييم الدرساختيار من متعدد، وصح وخطأ، وأكمل الفراغ، وتتبّع الناتج.ابدأ التقييم ←

📝 معلم؟ أرسل ملاحظة للمؤلف عن هذا الدرس