/ دورة PHP الشاملة
0/17 مكتملة
درس 14 من 17

قاعدة البيانات — CRUD

Create · Read · Update · Delete — تطبيق شامل مع PDO وتحقق كامل

🕐 65 دقيقة 💾 CRUD كامل 📝 6 أسئلة

ما هو CRUD؟

CRUD هو اختصار للعمليات الأربع الأساسية على قاعدة البيانات:

C — Create إضافة بيانات جديدة (INSERT)
R — Read قراءة وعرض البيانات (SELECT)
U — Update تعديل بيانات موجودة (UPDATE)
D — Delete حذف بيانات (DELETE)

كل تطبيق ويب — مهما كان — يُنجز هذه العمليات الأربع بأشكال مختلفة.

C Create — إضافة بيانات

سنبني نظام إضافة منتج كامل مع Validation:
PHP — add-product.php
<?php
require_once 'db.php';
session_start();

$errors  = [];
$success = '';

// ─── معالجة الـ form عند الإرسال ─────────────────────────
if ($_SERVER['REQUEST_METHOD'] === 'POST') {

    // 1. استقبال وتنظيف المدخلات
    $name        = trim($_POST['name']        ?? '');
    $description = trim($_POST['description'] ?? '');
    $price       = $_POST['price']            ?? '';
    $stock       = $_POST['stock']            ?? '';
    $category    = trim($_POST['category']    ?? '');

    // 2. التحقق (Validation)
    if (empty($name))             $errors[] = 'اسم المنتج مطلوب';
    if (strlen($name) > 200)      $errors[] = 'اسم المنتج طويل جداً (200 حرف)';
    if (!is_numeric($price) || $price < 0) $errors[] = 'السعر يجب أن يكون رقماً موجباً';
    if (!is_numeric($stock) || $stock < 0) $errors[] = 'الكمية يجب أن تكون رقماً موجباً';
    if (empty($category))         $errors[] = 'الفئة مطلوبة';

    // 3. التحقق من عدم تكرار الاسم
    if (empty($errors)) {
        $check = $pdo->prepare("SELECT id FROM products WHERE name = ? LIMIT 1");
        $check->execute([$name]);
        if ($check->fetch()) {
            $errors[] = 'يوجد منتج بهذا الاسم بالفعل';
        }
    }

    // 4. الإضافة إلى قاعدة البيانات
    if (empty($errors)) {
        try {
            $stmt = $pdo->prepare("
                INSERT INTO products (name, description, price, stock, category)
                VALUES (:name, :description, :price, :stock, :category)
            ");
            $stmt->execute([
                ':name'        => $name,
                ':description' => $description ?: null,
                ':price'       => (float) $price,
                ':stock'       => (int) $stock,
                ':category'    => $category,
            ]);

            $new_id  = $pdo->lastInsertId();
            $success = "تم إضافة المنتج بنجاح! (المعرف: $new_id)";

            // تفريغ المدخلات بعد النجاح
            $name = $description = $price = $stock = $category = '';

        } catch (PDOException $e) {
            $errors[] = 'خطأ في قاعدة البيانات: ' . $e->getMessage();
        }
    }
}
?>
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<body>
    <h2>إضافة منتج جديد</h2>

    <?php foreach ($errors as $err): ?>
        <div style="color:red">❌ <?= htmlspecialchars($err) ?></div>
    <?php endforeach; ?>

    <?php if ($success): ?>
        <div style="color:green">✅ <?= htmlspecialchars($success) ?></div>
    <?php endif; ?>

    <form method="POST">
        <input type="text"   name="name"        placeholder="اسم المنتج" required
               value="<?= htmlspecialchars($name ?? '') ?>">
        <textarea name="description" placeholder="الوصف"><?= htmlspecialchars($description ?? '') ?></textarea>
        <input type="number" name="price"       placeholder="السعر"  step="0.01" min="0" required>
        <input type="number" name="stock"       placeholder="الكمية" min="0"     required>
        <select name="category" required>
            <option value="">اختر الفئة</option>
            <option value="كتب">كتب</option>
            <option value="كورسات">كورسات</option>
            <option value="ملابس">ملابس</option>
        </select>
        <button type="submit">إضافة المنتج</button>
    </form>
</body></html>

R Read — قراءة وعرض البيانات مع Pagination

PHP — products-list.php (مع ترقيم الصفحات)
<?php
require_once 'db.php';

// ─── Pagination ───────────────────────────────────────────
$per_page    = 10;
$page        = max(1, (int)($_GET['page'] ?? 1));
$offset      = ($page - 1) * $per_page;

// ─── بحث وفلترة ──────────────────────────────────────────
$search   = trim($_GET['search']   ?? '');
$category = trim($_GET['category'] ?? '');
$sort     = $_GET['sort'] ?? 'created_at';
$order    = $_GET['order'] ?? 'DESC';

// تحقق من sort المسموح بها (لمنع SQL Injection)
$allowed_sorts  = ['name', 'price', 'stock', 'created_at'];
$allowed_orders = ['ASC', 'DESC'];
if (!in_array($sort, $allowed_sorts))   $sort  = 'created_at';
if (!in_array($order, $allowed_orders)) $order = 'DESC';

// ─── بناء الاستعلام ────────────────────────────────────────
$where_parts = [];
$params      = [];

if ($search) {
    $where_parts[] = "(name LIKE :search OR description LIKE :search2)";
    $params[':search']  = "%$search%";
    $params[':search2'] = "%$search%";
}
if ($category) {
    $where_parts[] = "category = :category";
    $params[':category'] = $category;
}

$where_sql = $where_parts ? "WHERE " . implode(" AND ", $where_parts) : "";

// ─── عد الكل لـ Pagination ────────────────────────────────
$count_sql  = "SELECT COUNT(*) FROM products $where_sql";
$count_stmt = $pdo->prepare($count_sql);
$count_stmt->execute($params);
$total_rows = $count_stmt->fetchColumn();
$total_pages = ceil($total_rows / $per_page);

// ─── الاستعلام الرئيسي ────────────────────────────────────
$sql = "SELECT * FROM products $where_sql
        ORDER BY $sort $order
        LIMIT :limit OFFSET :offset";

$stmt = $pdo->prepare($sql);
foreach ($params as $key => $val) {
    $stmt->bindValue($key, $val);
}
$stmt->bindValue(':limit',  $per_page, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset,   PDO::PARAM_INT);
$stmt->execute();
$products = $stmt->fetchAll();

// ─── عرض النتائج ──────────────────────────────────────────
echo "<p>إجمالي: $total_rows منتج | الصفحة $page من $total_pages</p>";

echo "<table border='1'>";
echo "<tr><th>المعرف</th><th>الاسم</th><th>السعر</th><th>المخزون</th><th>الفئة</th><th>إجراءات</th></tr>";

foreach ($products as $p) {
    $name = htmlspecialchars($p['name']);
    echo "<tr>";
    echo "<td>" . $p['id'] . "</td>";
    echo "<td>$name</td>";
    echo "<td>" . number_format($p['price'], 2) . " ر.س</td>";
    echo "<td>" . $p['stock'] . "</td>";
    echo "<td>" . htmlspecialchars($p['category']) . "</td>";
    echo "<td>
        <a href='edit-product.php?id=" . $p['id'] . "'>تعديل</a> |
        <a href='delete-product.php?id=" . $p['id'] . "' onclick='return confirm(\"حذف؟\")'>حذف</a>
    </td>";
    echo "</tr>";
}
echo "</table>";

// ─── روابط التنقل بين الصفحات ─────────────────────────────
for ($i = 1; $i <= $total_pages; $i++) {
    $active = ($i === $page) ? "font-weight:bold" : "";
    echo "<a href='?page=$i&search=" . urlencode($search) . "' style='$active'>$i</a> ";
}
?>
الناتج
إجمالي: 3 منتج | الصفحة 1 من 1 | المعرف | الاسم | السعر | المخزون | الفئة | |--------|-------------------|-----------|---------|---------| | 1 | كتاب PHP المتقدم | 89.99 ر.س | 50 | كتب | | 2 | كورس تصميم الويب | 299.00 ر.س| 999 | كورسات | | 3 | قميص المبرمجين | 149.50 ر.س| 30 | ملابس |

U Update — تعديل البيانات

PHP — edit-product.php
<?php
require_once 'db.php';

// ─── التحقق من الـ ID ─────────────────────────────────────
$id = (int)($_GET['id'] ?? 0);
if ($id <= 0) {
    die("معرف غير صحيح");
}

// ─── جلب بيانات المنتج الحالية ────────────────────────────
$stmt = $pdo->prepare("SELECT * FROM products WHERE id = ? LIMIT 1");
$stmt->execute([$id]);
$product = $stmt->fetch();

if (!$product) {
    die("المنتج غير موجود");
}

$errors  = [];
$success = '';

// ─── معالجة الـ form ──────────────────────────────────────
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $name        = trim($_POST['name']        ?? '');
    $description = trim($_POST['description'] ?? '');
    $price       = $_POST['price']            ?? 0;
    $stock       = $_POST['stock']            ?? 0;
    $category    = trim($_POST['category']    ?? '');

    // Validation
    if (empty($name))        $errors[] = 'الاسم مطلوب';
    if ($price < 0)          $errors[] = 'السعر لا يمكن أن يكون سالباً';
    if ($stock < 0)          $errors[] = 'المخزون لا يمكن أن يكون سالباً';

    // تحقق من عدم تكرار الاسم (مع استثناء المنتج الحالي)
    if (empty($errors)) {
        $check = $pdo->prepare("SELECT id FROM products WHERE name = ? AND id != ? LIMIT 1");
        $check->execute([$name, $id]);
        if ($check->fetch()) {
            $errors[] = 'يوجد منتج آخر بهذا الاسم';
        }
    }

    if (empty($errors)) {
        $stmt = $pdo->prepare("
            UPDATE products
            SET name = :name,
                description = :description,
                price = :price,
                stock = :stock,
                category = :category
            WHERE id = :id
        ");
        $stmt->execute([
            ':name'        => $name,
            ':description' => $description ?: null,
            ':price'       => (float) $price,
            ':stock'       => (int) $stock,
            ':category'    => $category,
            ':id'          => $id,
        ]);

        // تحقق هل تم التعديل فعلاً
        if ($stmt->rowCount() > 0) {
            $success = 'تم تحديث المنتج بنجاح!';
        } else {
            $success = 'لم يتغير شيء (البيانات مطابقة للموجود)';
        }

        // إعادة جلب البيانات المحدّثة
        $stmt2 = $pdo->prepare("SELECT * FROM products WHERE id = ?");
        $stmt2->execute([$id]);
        $product = $stmt2->fetch();
    }
}
?>
<!-- عرض الـ form مع القيم الحالية -->
<h2>تعديل: <?= htmlspecialchars($product['name']) ?></h2>

<form method="POST">
    <input type="text" name="name"
           value="<?= htmlspecialchars($product['name']) ?>" required>
    <input type="number" name="price" step="0.01"
           value="<?= $product['price'] ?>" required>
    <input type="number" name="stock"
           value="<?= $product['stock'] ?>" required>
    <textarea name="description"><?= htmlspecialchars($product['description'] ?? '') ?></textarea>
    <button type="submit">حفظ التعديلات</button>
</form>

D Delete — حذف البيانات بأمان

PHP — delete-product.php (مع CSRF Protection)
<?php
require_once 'db.php';
session_start();

// ─── يجب أن يكون POST وليس GET للحذف ──────────────────────
// (GET يمكن تزويره بسهولة - رابط في صورة في بريد إلكتروني مثلاً)
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    http_response_code(405);
    die("Method Not Allowed — استخدم POST للحذف");
}

// ─── CSRF Token Protection ─────────────────────────────────
$token = $_POST['csrf_token'] ?? '';
if (!hash_equals($_SESSION['csrf_token'] ?? '', $token)) {
    http_response_code(403);
    die("CSRF Token غير صحيح");
}

$id = (int)($_POST['id'] ?? 0);
if ($id <= 0) die("معرف غير صحيح");

// ─── تحقق من وجود المنتج أولاً ────────────────────────────
$check = $pdo->prepare("SELECT id, name FROM products WHERE id = ?");
$check->execute([$id]);
$product = $check->fetch();

if (!$product) die("المنتج غير موجود");

// ─── الحذف الفعلي ─────────────────────────────────────────
try {
    $pdo->beginTransaction();

    // حذف الصور المرتبطة إن وجدت
    if (!empty($product['image']) && file_exists("uploads/" . $product['image'])) {
        unlink("uploads/" . $product['image']);
    }

    // حذف من DB
    $stmt = $pdo->prepare("DELETE FROM products WHERE id = ?");
    $stmt->execute([$id]);

    $pdo->commit();

    $_SESSION['flash'] = "تم حذف المنتج «" . $product['name'] . "» بنجاح";
    header("Location: products-list.php");
    exit;

} catch (PDOException $e) {
    $pdo->rollBack();
    die("خطأ في الحذف: " . $e->getMessage());
}
?>

<?php
// ─── في صفحة القائمة — إنشاء CSRF Token ──────────────────
session_start();
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
?>
<!-- زر الحذف الصحيح -->
<form method="POST" action="delete-product.php"
      onsubmit="return confirm('هل أنت متأكد من الحذف؟')">
    <input type="hidden" name="id"         value="<?= $product['id'] ?>">
    <input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
    <button type="submit">🗑️ حذف</button>
</form>
⚠️ لماذا POST وليس GET للحذف؟ رابط GET يمكن تضمينه في صورة في بريد إلكتروني أو صفحة خارجية. لو فتح المستخدم البريد وجُمِّل المتصفح الرابط تلقائياً، ستُحذف البيانات! POST يتطلب إرسال form متعمّد.

نظام CRUD كامل — Class Database Helper

لتجنب تكرار الكود، نبني Class مساعدة:
PHP — Database Helper Class
<?php
class DB {
    private static ?PDO $instance = null;

    // Singleton — اتصال واحد فقط
    public static function connect(): PDO {
        if (self::$instance === null) {
            self::$instance = new PDO(
                "mysql:host=localhost;dbname=php_course_db;charset=utf8mb4",
                'root', '',
                [
                    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
                    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
                    PDO::ATTR_EMULATE_PREPARES   => false,
                ]
            );
        }
        return self::$instance;
    }

    // SELECT — قراءة صفوف متعددة
    public static function select(string $sql, array $params = []): array {
        $stmt = self::connect()->prepare($sql);
        $stmt->execute($params);
        return $stmt->fetchAll();
    }

    // SELECT — صف واحد
    public static function find(string $sql, array $params = []): ?array {
        $stmt = self::connect()->prepare($sql);
        $stmt->execute($params);
        return $stmt->fetch() ?: null;
    }

    // SELECT بـ ID مباشرة
    public static function findById(string $table, int $id): ?array {
        return self::find("SELECT * FROM $table WHERE id = ?", [$id]);
    }

    // INSERT
    public static function insert(string $table, array $data): int {
        $cols    = implode(', ', array_keys($data));
        $holders = implode(', ', array_fill(0, count($data), '?'));
        $sql     = "INSERT INTO $table ($cols) VALUES ($holders)";
        self::connect()->prepare($sql)->execute(array_values($data));
        return (int) self::connect()->lastInsertId();
    }

    // UPDATE
    public static function update(string $table, array $data, string $where, array $where_params = []): int {
        $sets = implode(', ', array_map(fn($col) => "$col = ?", array_keys($data)));
        $sql  = "UPDATE $table SET $sets WHERE $where";
        $stmt = self::connect()->prepare($sql);
        $stmt->execute([...array_values($data), ...$where_params]);
        return $stmt->rowCount();
    }

    // DELETE
    public static function delete(string $table, string $where, array $params = []): int {
        $stmt = self::connect()->prepare("DELETE FROM $table WHERE $where");
        $stmt->execute($params);
        return $stmt->rowCount();
    }

    // COUNT
    public static function count(string $table, string $where = '1', array $params = []): int {
        return (int) self::select("SELECT COUNT(*) as cnt FROM $table WHERE $where", $params)[0]['cnt'] ?? 0;
    }
}

// ─── استخدام الـ Class ────────────────────────────────────
// إضافة منتج
$new_id = DB::insert('products', [
    'name'     => 'منتج جديد',
    'price'    => 99.99,
    'stock'    => 10,
    'category' => 'كتب',
]);
echo "أُضيف بمعرف: $new_id";

// قراءة منتج بـ ID
$product = DB::findById('products', 1);
echo $product['name'];

// قراءة كل منتجات فئة
$books = DB::select("SELECT * FROM products WHERE category = ?", ['كتب']);

// تعديل سعر
$affected = DB::update('products', ['price' => 79.99], 'id = ?', [1]);
echo "تم تعديل $affected سجل";

// حذف
$deleted = DB::delete('products', 'id = ?', [5]);
echo "تم حذف $deleted سجل";

// عد
$total = DB::count('products', 'category = ?', ['كتب']);
echo "عدد الكتب: $total";
?>
الناتج
أُضيف بمعرف: 4 كتاب PHP المتقدم تم تعديل 1 سجل تم حذف 1 سجل عدد الكتب: 1

Transactions — ضمان اتساق البيانات

الـ Transaction يضمن أن مجموعة عمليات تتم كلها أو لا شيء. مثلاً: عند شراء منتج، يجب خصم من المخزون وإضافة للطلبات معاً.
PHP — Transactions
<?php
require_once 'db.php';

function purchaseProduct(PDO $pdo, int $user_id, int $product_id, int $quantity): array {
    try {
        $pdo->beginTransaction(); // بدء الـ Transaction

        // 1. جلب المنتج مع LOCK (يمنع التعديل المتزامن)
        $product = $pdo->prepare("SELECT * FROM products WHERE id = ? FOR UPDATE");
        $product->execute([$product_id]);
        $prod = $product->fetch();

        if (!$prod) throw new Exception("المنتج غير موجود");

        // 2. التحقق من توافر المخزون
        if ($prod['stock'] < $quantity) {
            throw new Exception("المخزون غير كافٍ. المتاح: " . $prod['stock']);
        }

        // 3. خصم من المخزون
        $update = $pdo->prepare("UPDATE products SET stock = stock - ? WHERE id = ?");
        $update->execute([$quantity, $product_id]);

        // 4. إنشاء الطلب
        $total    = $prod['price'] * $quantity;
        $order    = $pdo->prepare("INSERT INTO orders (user_id, total, status) VALUES (?, ?, 'pending')");
        $order->execute([$user_id, $total]);
        $order_id = $pdo->lastInsertId();

        $pdo->commit(); // نجح كل شيء — احفظ!

        return [
            'success'  => true,
            'order_id' => $order_id,
            'total'    => $total,
            'message'  => "تم الشراء بنجاح! رقم الطلب: $order_id",
        ];

    } catch (Exception $e) {
        $pdo->rollBack(); // فشل شيء — تراجع عن كل شيء!
        return [
            'success' => false,
            'message' => $e->getMessage(),
        ];
    }
}

// استخدام
$result = purchaseProduct($pdo, user_id: 1, product_id: 2, quantity: 3);

if ($result['success']) {
    echo "✅ " . $result['message'];
    echo " | الإجمالي: " . number_format($result['total'], 2) . " ر.س";
} else {
    echo "❌ " . $result['message'];
}
?>
الناتج (نجاح)
✅ تم الشراء بنجاح! رقم الطلب: 1 | الإجمالي: 897.00 ر.س

🧪 اختبر فهمك — CRUD

6 أسئلة
سؤال 1
ما الاختصار الصحيح لـ CRUD؟
سؤال 2
ما الدالة التي تُعيد عدد الصفوف المتأثرة بعد UPDATE أو DELETE؟
سؤال 3
لماذا يجب استخدام POST وليس GET لحذف البيانات؟
سؤال 4
ما فائدة beginTransaction() و commit() و rollBack() في PDO؟
سؤال 5
ما الكود الصحيح لحساب عدد الصفحات في Pagination؟
سؤال 6
ما هو CSRF وكيف نحميه؟