أساسيات PHP

بناء تطبيق CRUD

13 دقيقة الدرس 39 من 45

ما هو CRUD؟

CRUD يرمز إلى الإنشاء والقراءة والتحديث والحذف - العمليات الأساسية الأربع للتخزين الدائم. في هذا الدرس، سنبني تطبيق إدارة مهام كامل يوضح جميع عمليات CRUD مع الأمان المناسب.

بنية المشروع

task-manager/
├── config/
│   └── database.php      # تكوين قاعدة البيانات
├── includes/
│   ├── header.php        # رأس الصفحة المشترك
│   └── footer.php        # تذييل الصفحة المشترك
├── css/
│   └── style.css         # الأنماط
├── index.php             # قائمة المهام (القراءة)
├── create.php            # إضافة مهمة جديدة (الإنشاء)
├── edit.php              # تعديل المهمة (التحديث)
├── delete.php            # حذف المهمة (الحذف)
└── view.php              # عرض مهمة واحدة (القراءة)

الخطوة 1: إعداد قاعدة البيانات

أولاً، أنشئ قاعدة البيانات وجدول المهام:

CREATE DATABASE task_manager;
USE task_manager;

CREATE TABLE tasks (
    id INT AUTO_INCREMENT PRIMARY KEY,
    title VARCHAR(200) NOT NULL,
    description TEXT,
    status ENUM('pending', 'in_progress', 'completed') DEFAULT 'pending',
    priority ENUM('low', 'medium', 'high') DEFAULT 'medium',
    due_date DATE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

الخطوة 2: تكوين قاعدة البيانات (config/database.php)

<?php
// تكوين قاعدة البيانات
define('DB_HOST', 'localhost');
define('DB_USER', 'root');
define('DB_PASS', '');
define('DB_NAME', 'task_manager');

// تفعيل الإبلاغ عن الأخطاء (عطّل في الإنتاج)
ini_set('display_errors', 1);
error_reporting(E_ALL);

// تفعيل وضع استثناء MySQLi
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);

// إنشاء الاتصال
try {
    $conn = new mysqli(DB_HOST, DB_USER, DB_PASS, DB_NAME);
    $conn->set_charset("utf8mb4");
} catch (mysqli_sql_exception $e) {
    die("فشل الاتصال بقاعدة البيانات: " . $e->getMessage());
}
?>

الخطوة 3: قالب الرأس (includes/header.php)

<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title><?php echo $page_title ?? 'مدير المهام'; ?></title>
    <link rel="stylesheet" href="css/style.css">
</head>
<body>
    <nav>
        <div class="container">
            <h1>مدير المهام</h1>
            <ul>
                <li><a href="index.php">جميع المهام</a></li>
                <li><a href="create.php">إضافة مهمة جديدة</a></li>
            </ul>
        </div>
    </nav>
    <main class="container">

الخطوة 4: قالب التذييل (includes/footer.php)

    </main>
    <footer>
        <div class="container">
            <p>&copy; <?php echo date('Y'); ?> مدير المهام</p>
        </div>
    </footer>
</body>
</html>

الخطوة 5: القراءة - قائمة جميع المهام (index.php)

<?php
require_once 'config/database.php';
$page_title = 'جميع المهام';
include 'includes/header.php';

// الحصول على الفلتر من URL
$filter = $_GET['status'] ?? 'all';

// بناء الاستعلام بناءً على الفلتر
if ($filter === 'all') {
    $sql = "SELECT * FROM tasks ORDER BY created_at DESC";
    $stmt = $conn->prepare($sql);
} else {
    $sql = "SELECT * FROM tasks WHERE status = ? ORDER BY created_at DESC";
    $stmt = $conn->prepare($sql);
    $stmt->bind_param("s", $filter);
}

$stmt->execute();
$result = $stmt->get_result();
?>

<div class="page-header">
    <h2>جميع المهام</h2>
    <a href="create.php" class="btn btn-primary">إضافة مهمة جديدة</a>
</div>

<!-- أزرار التصفية -->
<div class="filters">
    <a href="?status=all" class="btn <?php echo $filter === 'all' ? 'active' : ''; ?>">الكل</a>
    <a href="?status=pending" class="btn <?php echo $filter === 'pending' ? 'active' : ''; ?>">معلقة</a>
    <a href="?status=in_progress" class="btn <?php echo $filter === 'in_progress' ? 'active' : ''; ?>">قيد التنفيذ</a>
    <a href="?status=completed" class="btn <?php echo $filter === 'completed' ? 'active' : ''; ?>">مكتملة</a>
</div>

<?php if ($result->num_rows > 0): ?>
    <table class="task-table">
        <thead>
            <tr>
                <th>العنوان</th>
                <th>الحالة</th>
                <th>الأولوية</th>
                <th>تاريخ الاستحقاق</th>
                <th>الإجراءات</th>
            </tr>
        </thead>
        <tbody>
            <?php while ($task = $result->fetch_assoc()): ?>
                <tr>
                    <td>
                        <a href="view.php?id=<?php echo $task['id']; ?>">
                            <?php echo htmlspecialchars($task['title']); ?>
                        </a>
                    </td>
                    <td>
                        <span class="badge badge-<?php echo $task['status']; ?>">
                            <?php echo ucfirst(str_replace('_', ' ', $task['status'])); ?>
                        </span>
                    </td>
                    <td>
                        <span class="badge badge-<?php echo $task['priority']; ?>">
                            <?php echo ucfirst($task['priority']); ?>
                        </span>
                    </td>
                    <td><?php echo $task['due_date'] ? date('M d, Y', strtotime($task['due_date'])) : 'غير محدد'; ?></td>
                    <td>
                        <a href="edit.php?id=<?php echo $task['id']; ?>" class="btn btn-sm">تعديل</a>
                        <a href="delete.php?id=<?php echo $task['id']; ?>" class="btn btn-sm btn-danger" onclick="return confirm('حذف هذه المهمة؟')">حذف</a>
                    </td>
                </tr>
            <?php endwhile; ?>
        </tbody>
    </table>
<?php else: ?>
    <p class="no-tasks">لم يتم العثور على مهام. <a href="create.php">أنشئ واحدة الآن</a></p>
<?php endif; ?>

<?php
$stmt->close();
include 'includes/footer.php';
?>

الخطوة 6: الإنشاء - إضافة مهمة جديدة (create.php)

<?php
require_once 'config/database.php';
$page_title = 'إضافة مهمة جديدة';

$errors = [];
$success = false;

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    // التحقق من صحة المدخلات
    $title = trim($_POST['title'] ?? '');
    $description = trim($_POST['description'] ?? '');
    $status = $_POST['status'] ?? 'pending';
    $priority = $_POST['priority'] ?? 'medium';
    $due_date = $_POST['due_date'] ?? null;

    // التحقق
    if (empty($title)) {
        $errors[] = "العنوان مطلوب";
    } elseif (strlen($title) > 200) {
        $errors[] = "يجب ألا يتجاوز العنوان 200 حرف";
    }

    if (!in_array($status, ['pending', 'in_progress', 'completed'])) {
        $errors[] = "حالة غير صالحة";
    }

    if (!in_array($priority, ['low', 'medium', 'high'])) {
        $errors[] = "أولوية غير صالحة";
    }

    // إذا لم تكن هناك أخطاء، أدرج المهمة
    if (empty($errors)) {
        $sql = "INSERT INTO tasks (title, description, status, priority, due_date)
                VALUES (?, ?, ?, ?, ?)";

        $stmt = $conn->prepare($sql);
        $stmt->bind_param("sssss", $title, $description, $status, $priority, $due_date);

        if ($stmt->execute()) {
            $success = true;
            $task_id = $stmt->insert_id;
            header("Location: view.php?id=$task_id&created=1");
            exit;
        } else {
            $errors[] = "فشل في إنشاء المهمة";
        }

        $stmt->close();
    }
}

include 'includes/header.php';
?>

<h2>إضافة مهمة جديدة</h2>

<?php if (!empty($errors)): ?>
    <div class="alert alert-danger">
        <ul>
            <?php foreach ($errors as $error): ?>
                <li><?php echo htmlspecialchars($error); ?></li>
            <?php endforeach; ?>
        </ul>
    </div>
<?php endif; ?>

<form method="POST" action="" class="task-form">
    <div class="form-group">
        <label for="title">العنوان *</label>
        <input type="text" id="title" name="title" required
               value="<?php echo htmlspecialchars($_POST['title'] ?? ''); ?>">
    </div>

    <div class="form-group">
        <label for="description">الوصف</label>
        <textarea id="description" name="description" rows="5"><?php echo htmlspecialchars($_POST['description'] ?? ''); ?></textarea>
    </div>

    <div class="form-row">
        <div class="form-group">
            <label for="status">الحالة</label>
            <select id="status" name="status">
                <option value="pending">معلقة</option>
                <option value="in_progress">قيد التنفيذ</option>
                <option value="completed">مكتملة</option>
            </select>
        </div>

        <div class="form-group">
            <label for="priority">الأولوية</label>
            <select id="priority" name="priority">
                <option value="low">منخفضة</option>
                <option value="medium" selected>متوسطة</option>
                <option value="high">عالية</option>
            </select>
        </div>

        <div class="form-group">
            <label for="due_date">تاريخ الاستحقاق</label>
            <input type="date" id="due_date" name="due_date">
        </div>
    </div>

    <div class="form-actions">
        <button type="submit" class="btn btn-primary">إنشاء المهمة</button>
        <a href="index.php" class="btn">إلغاء</a>
    </div>
</form>

<?php include 'includes/footer.php'; ?>

الخطوة 7: القراءة - عرض مهمة واحدة (view.php)

تعرض صفحة view.php تفاصيل المهمة الكاملة مع خيارات التعديل والحذف.

الخطوة 8: التحديث - تعديل المهمة (edit.php)

تشبه صفحة edit.php صفحة create.php ولكنها تملأ النموذج مسبقًا بالبيانات الحالية وتستخدم استعلام UPDATE بدلاً من INSERT.

الخطوة 9: الحذف - حذف المهمة (delete.php)

<?php
require_once 'config/database.php';

// الحصول على معرف المهمة
$task_id = $_GET['id'] ?? 0;

// التحقق من وجود المهمة
$stmt = $conn->prepare("SELECT id FROM tasks WHERE id = ?");
$stmt->bind_param("i", $task_id);
$stmt->execute();
$result = $stmt->get_result();

if ($result->num_rows === 0) {
    header("Location: index.php");
    exit;
}

// حذف المهمة
$stmt = $conn->prepare("DELETE FROM tasks WHERE id = ?");
$stmt->bind_param("i", $task_id);

if ($stmt->execute()) {
    header("Location: index.php?deleted=1");
} else {
    header("Location: index.php?error=1");
}

$stmt->close();
exit;
?>
نصيحة: لتطبيقات الإنتاج، نفّذ "الحذف الناعم" بإضافة عمود deleted_at بدلاً من إزالة السجلات نهائيًا.
ملاحظات أمنية:
  • يتم التحقق من صحة جميع مدخلات المستخدم وتعقيمها
  • تمنع العبارات المحضرة حقن SQL
  • htmlspecialchars() يمنع هجمات XSS
  • طريقة HTTP POST لتعديل البيانات (الإنشاء والتحديث والحذف)
  • مطالبات التأكيد قبل الإجراءات المدمرة

تمرين: تحسين مدير المهام

  1. أضف ترقيمًا لقائمة المهام (10 مهام في كل صفحة)
  2. نفّذ وظيفة البحث (البحث حسب العنوان أو الوصف)
  3. أضف خيارات الفرز (حسب التاريخ والأولوية والحالة)
  4. أنشئ زر إجراء سريع "وضع علامة كمكتمل"
  5. أضف فئات أو وسوم للمهام
  6. نفّذ مصادقة المستخدم (عدة مستخدمين بمهامهم الخاصة)
  7. أضف مرفقات ملفات للمهام
  8. أنشئ لوحة معلومات مع إحصائيات المهام
  9. أضف إشعارات البريد الإلكتروني لتواريخ الاستحقاق
  10. نفّذ نقطة نهاية API لتصدير المهام كـ JSON

أفضل ممارسات تطبيقات CRUD

  • التحقق: تحقق دائمًا من مدخلات المستخدم على جانب الخادم
  • التعقيم: هرب المخرجات بـ htmlspecialchars()
  • العبارات المحضرة: لا تدمج أبدًا مدخلات المستخدم في استعلامات SQL
  • معالجة الأخطاء: اعرض رسائل خطأ واضحة للمستخدم
  • إعادة التوجيه: استخدم نمط POST-Redirect-GET لمنع التقديم المكرر
  • التأكيد: اطلب التأكيد قبل حذف البيانات
  • التفويض: تحقق من أن المستخدمين يمكنهم تعديل بياناتهم فقط
  • التسجيل: سجّل الإجراءات المهمة للمراجعة

الملخص

  • تطبيقات CRUD تنفذ عمليات الإنشاء والقراءة والتحديث والحذف
  • استخدم العبارات المحضرة لجميع عمليات قاعدة البيانات
  • تحقق من صحة جميع مدخلات المستخدم وعقّمها
  • نفّذ معالجة الأخطاء المناسبة وتعليقات المستخدم
  • استخدم نمط POST-Redirect-GET لإرسال النماذج
  • هرب المخرجات دائمًا لمنع هجمات XSS
  • هيكل كودك مع مكونات قابلة لإعادة الاستخدام (الرؤوس والتذييلات والتكوين)
  • اختبر جميع عمليات CRUD بدقة