تطبيق متكامل: إدارة المستخدمين
كل ما تعلمته في تطبيق واحد: المشروع الرسمي للمقرر. ⏱ نحو 45 دقيقة.
- تخطط تطبيق
CRUD: الصفحات والطلبات والطرق. - تبني وحدة بيانات تجمع استعلامات الكيان وقواعده.
- تنفّذ القراءة والبحث والإضافة والتعديل والحذف بأمان مع
PRG. - تستفيد من قيود القاعدة
(UNIQUE)وتحوّل مخالفتها رسالة مفهومة.
١من الطلب إلى التطبيق
المشروع الرسمي لهذا المقرر في الخطة: «واجهة خلفية لإدارة بيانات مستخدمين: إضافتهم وتعديل بياناتهم وحذفهم». في هذا الدرس نبنيه كاملًا بكل ما تعلمته: الوحدات، والتحقق، والجلسات، وقاعدة البيانات الآمنة، ونمط PRG. وهذه العمليات الأربع لها اسم مشهور: CRUD.
| الحرف | العملية | SQL | الصفحة | الطلب |
|---|---|---|---|---|
| C | Create إنشاء | INSERT | create.php | GET للنموذج، POST للحفظ |
| R | Read قراءة | SELECT | index.php | GET (مع ?q= للبحث) |
| U | Update تعديل | UPDATE | edit.php | GET للنموذج، POST للحفظ |
| D | Delete حذف | DELETE | delete.php | POST فقط |
لماذا الحذف بـ POST لا برابط GET؟ لأن الروابط يتبعها أي شيء: محركات البحث، ومعاينة الروابط في تطبيقات المحادثة، وتحميل الصفحات المسبق. فرابط حذف قد يحذف بياناتك دون أن يضغطه أحد!
٢بنية التطبيق
users-app/├── db.php ← الاتصال (الدرس 19)├── users.php ← كل استعلامات المستخدمين وقواعدهم (وحدة البيانات)├── index.php ← القائمة والبحث (R)├── create.php ← نموذج الإضافة وحفظها (C)├── edit.php ← نموذج التعديل وحفظه (U) ← تحدّي الدرس└── delete.php ← الحذف (D)
الصفحات لا تكتب SQL بنفسها، بل تستدعي دوال users.php. فإن انتقلنا من SQLite إلى MySQL، أو أضفنا عمودًا، نعدّل وحدة واحدة.
٣التطبيق يعمل
تتبّع السجل: نعرض القائمة، ثم نحاول إضافة مستخدم ببريد مكرر (422 مع رسالة)، ثم نضيف مستخدمًا صحيحًا (303 إلى القائمة)، ثم نبحث، ثم نحذف:
<?phprequire __DIR__ . '/users.php';$q = trim($_GET['q'] ?? '');$users = all_users($pdo, $q);$e = fn($s) => htmlspecialchars((string) $s);?><h2>المستخدمون (<?= count($users) ?>)</h2><form><input name="q" value="<?= $e($q) ?>" placeholder="بحث بالاسم أو البريد"> <button>بحث</button></form><p><a href="create.php">➕ مستخدم جديد</a></p><table border="1" cellpadding="5"> <tr><th>#</th><th>الاسم</th><th>البريد</th><th>الصلاحية</th><th></th></tr> <?php foreach ($users as $u): ?> <tr> <td><?= $u['id'] ?></td><td><?= $e($u['name']) ?></td><td><?= $e($u['email']) ?></td><td><?= ROLES[$u['role']] ?></td> <td> <form method="post" action="delete.php" style="display:inline"> <input type="hidden" name="id" value="<?= $u['id'] ?>"><button>🗑 حذف</button> </form> </td> </tr> <?php endforeach; ?></table>
<?phprequire __DIR__ . '/users.php';$in = $_POST + ['name' => '', 'email' => '', 'role' => 'trainee', 'city' => ''];$err = []; if ($_SERVER['REQUEST_METHOD'] === 'POST') { $err = validate_user($in); if (!$err) { try { create_user($pdo, $in); header('Location: index.php', true, 303); exit; } catch (PDOException $ex) { if ($ex->getCode() !== '23000') throw $ex; // 23000 = مخالفة قيد (UNIQUE) $err['email'] = 'هذا البريد مسجّل من قبل'; } } http_response_code(422);}$e = fn($s) => htmlspecialchars((string) $s);?><h2>مستخدم جديد</h2><form method="post"> <p>الاسم: <input name="name" value="<?= $e($in['name']) ?>"> <small style="color:red"><?= $err['name'] ?? '' ?></small></p> <p>البريد: <input name="email" value="<?= $e($in['email']) ?>"> <small style="color:red"><?= $err['email'] ?? '' ?></small></p> <p>الصلاحية: <select name="role"> <?php foreach (ROLES as $k => $v): ?><option value="<?= $k ?>" <?= $in['role'] === $k ? 'selected' : '' ?>><?= $v ?></option><?php endforeach; ?> </select></p> <p>المدينة: <input name="city" value="<?= $e($in['city']) ?>"></p> <button>حفظ</button> <a href="index.php">رجوع</a></form>
<?phprequire __DIR__ . '/users.php';if ($_SERVER['REQUEST_METHOD'] !== 'POST') { http_response_code(405); exit;}delete_user($pdo, (int) ($_POST['id'] ?? 0));header('Location: index.php', true, 303);
<?php// users.php: كل استعلامات المستخدمين في وحدة واحدةrequire_once __DIR__ . '/db.php'; const ROLES = ['admin' => 'مدير', 'trainer' => 'مدرب', 'trainee' => 'متدرب']; function all_users(PDO $pdo, string $q = ''): array{ $s = $pdo->prepare('SELECT * FROM users WHERE name LIKE ? OR email LIKE ? ORDER BY id'); $s->execute(["%$q%", "%$q%"]); return $s->fetchAll();} function find_user(PDO $pdo, int $id): ?array{ $s = $pdo->prepare('SELECT * FROM users WHERE id = ?'); $s->execute([$id]); return $s->fetch() ?: null;} function validate_user(array $in): array{ $err = []; if (mb_strlen(trim($in['name'] ?? '')) < 3) $err['name'] = 'الاسم 3 حروف على الأقل'; if (!filter_var($in['email'] ?? '', FILTER_VALIDATE_EMAIL)) $err['email'] = 'بريد غير صحيح'; if (!isset(ROLES[$in['role'] ?? ''])) $err['role'] = 'اختر صلاحية'; return $err;} function create_user(PDO $pdo, array $in): int{ $s = $pdo->prepare('INSERT INTO users (name, email, role, city) VALUES (?, ?, ?, ?)'); $s->execute([trim($in['name']), trim($in['email']), $in['role'], trim($in['city'] ?? '')]); return (int) $pdo->lastInsertId();} function delete_user(PDO $pdo, int $id): bool{ $s = $pdo->prepare('DELETE FROM users WHERE id = ?'); $s->execute([$id]); return $s->rowCount() > 0;}
<?php$pdo = new PDO('sqlite:' . __DIR__ . '/app.sqlite');// $pdo = new PDO('mysql:host=localhost;dbname=app;charset=utf8mb4', 'root', '');$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
البريد المكرر رفضته قاعدة البيانات نفسها بقيد UNIQUE، فالتقطنا الاستثناء وحوّلناه رسالة مفهومة. قيود القاعدة خط دفاع أخير حتى لو نسي الكود فحصًا.
٤تفاصيل تستحق الانتباه
$_POST + [القيم الافتراضية]: عامل + على المصفوفات يضيف المفاتيح الناقصة فقط، فيصير النموذج جاهزًا فيGETوPOSTمعًا.fn($s) => htmlspecialchars(…)دالة سهمية قصيرة مخزنة في متغير، لنكتب$e($x)بدل الاسم الطويل.- البحث بـ
LIKEمعprepare: علامات % تُضاف إلى القيمة لا إلى نص الأمر. $ex->getCode() === '23000': رمزSQLSTATEلمخالفة القيود. وأي خطأ آخر نعيد رميه ليُسجَّل.- قبل نشر التطبيق: احمِ كل الصفحات بتسجيل الدخول وصلاحية المدير (الدرس 16).
توقّع وتحقّق
حرف U في CRUD يقابل في SQL:
Update.لماذا يكون الحذف بطلب POST؟
رمز SQLSTATE لمخالفة قيد UNIQUE:
Integrity constraint violation.['a' => 1] + ['a' => 9, 'b' => 2] تعطي:
الخلاصة
CRUD= إنشاء(INSERT)وقراءة(SELECT)وتعديل(UPDATE)وحذف(DELETE).- الصفحات تستدعي وحدة البيانات ولا تكتب
SQLبنفسها. - الإضافة والتعديل: نموذج
GET، ثمPOST← تحقق ← حفظ ← تحويل 303، أو 422 مع القيم. - الحذف بـ
POSTدائمًا، والصفحات الإدارية محمية بالدخول. UNIQUEفي القاعدة + التقاط 23000 = لا تكرار حتى مع التزامن.
تحدٍّ برمجي: أكمل حرف U: صفحة التعديل
متقدمالتطبيق ينقصه التعديل. اكتب edit.php مستخدمًا دوال users.php.
GET edit.php?id=3يعرض<h2>تعديل: خالد العتيبي</h2>ونموذجًا معبأً ببياناته الحالية. ومستخدم غير موجود ← 404.- POST صحيح ← UPDATE ثم تحويل 303 إلى index.php، وتظهر القيم الجديدة في القائمة.
- POST ببيانات خاطئة ←
422 (validate_user)، وبريد مستخدم آخر ← 422 والنص «هذا البريد مسجّل من قبل». - استعلام مُعدّ فقط.
👀 اعرض الاستجابة المطلوبة
GET /edit.php?id=3GET /edit.php?id=99POST /edit.php?id=3(name=خالد المطيري&email=khalid.m@tvtc.sa&role=trainer&city=ينبع)GET /index.phpPOST /edit.php?id=3(name=خ&email=bad&role=trainee)POST /edit.php?id=3(name=خالد&email=noura@tvtc.sa&role=trainee)
- الكود يعمل بلا أخطاء أو تحذيرات
- GET ← عنوان التعديل والقيم الحالية
- مستخدم غير موجود ← 404
- POST صحيح ← 303
- التحويل إلى index.php
- القائمة تعرض البيانات الجديدة
- بيانات خاطئة ← 422
- بريد مكرر ← الرسالة
- UPDATE باستعلام مُعدّ
💡 تلميح 1
$in = $_POST + $user في POST.💡 تلميح 2
حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.
<?phprequire __DIR__ . '/users.php';$q = trim($_GET['q'] ?? '');$users = all_users($pdo, $q);$e = fn($s) => htmlspecialchars((string) $s);?><h2>المستخدمون (<?= count($users) ?>)</h2><form><input name="q" value="<?= $e($q) ?>" placeholder="بحث بالاسم أو البريد"> <button>بحث</button></form><p><a href="create.php">➕ مستخدم جديد</a></p><table border="1" cellpadding="5"> <tr><th>#</th><th>الاسم</th><th>البريد</th><th>الصلاحية</th><th></th></tr> <?php foreach ($users as $u): ?> <tr> <td><?= $u['id'] ?></td><td><?= $e($u['name']) ?></td><td><?= $e($u['email']) ?></td><td><?= ROLES[$u['role']] ?></td> <td> <form method="post" action="delete.php" style="display:inline"> <input type="hidden" name="id" value="<?= $u['id'] ?>"><button>🗑 حذف</button> </form> </td> </tr> <?php endforeach; ?></table>
<?php// users.php: كل استعلامات المستخدمين في وحدة واحدةrequire_once __DIR__ . '/db.php'; const ROLES = ['admin' => 'مدير', 'trainer' => 'مدرب', 'trainee' => 'متدرب']; function all_users(PDO $pdo, string $q = ''): array{ $s = $pdo->prepare('SELECT * FROM users WHERE name LIKE ? OR email LIKE ? ORDER BY id'); $s->execute(["%$q%", "%$q%"]); return $s->fetchAll();} function find_user(PDO $pdo, int $id): ?array{ $s = $pdo->prepare('SELECT * FROM users WHERE id = ?'); $s->execute([$id]); return $s->fetch() ?: null;} function validate_user(array $in): array{ $err = []; if (mb_strlen(trim($in['name'] ?? '')) < 3) $err['name'] = 'الاسم 3 حروف على الأقل'; if (!filter_var($in['email'] ?? '', FILTER_VALIDATE_EMAIL)) $err['email'] = 'بريد غير صحيح'; if (!isset(ROLES[$in['role'] ?? ''])) $err['role'] = 'اختر صلاحية'; return $err;} function create_user(PDO $pdo, array $in): int{ $s = $pdo->prepare('INSERT INTO users (name, email, role, city) VALUES (?, ?, ?, ?)'); $s->execute([trim($in['name']), trim($in['email']), $in['role'], trim($in['city'] ?? '')]); return (int) $pdo->lastInsertId();} function delete_user(PDO $pdo, int $id): bool{ $s = $pdo->prepare('DELETE FROM users WHERE id = ?'); $s->execute([$id]); return $s->rowCount() > 0;}
<?php$pdo = new PDO('sqlite:' . __DIR__ . '/app.sqlite');// $pdo = new PDO('mysql:host=localhost;dbname=app;charset=utf8mb4', 'root', '');$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
<?phprequire __DIR__ . '/users.php';$id = (int) ($_GET['id'] ?? 0);$user = find_user($pdo, $id);if (!$user) { http_response_code(404); exit('المستخدم غير موجود');}$in = $_SERVER['REQUEST_METHOD'] === 'POST' ? $_POST + $user : $user;$err = []; if ($_SERVER['REQUEST_METHOD'] === 'POST') { $err = validate_user($in); if (!$err) { try { $s = $pdo->prepare('UPDATE users SET name = ?, email = ?, role = ?, city = ? WHERE id = ?'); $s->execute([trim($in['name']), trim($in['email']), $in['role'], trim($in['city'] ?? ''), $id]); header('Location: index.php', true, 303); exit; } catch (PDOException $ex) { if ($ex->getCode() !== '23000') throw $ex; $err['email'] = 'هذا البريد مسجّل من قبل'; } } http_response_code(422);}$e = fn($s) => htmlspecialchars((string) $s);?><h2>تعديل: <?= $e($user['name']) ?></h2><form method="post"> <p>الاسم: <input name="name" value="<?= $e($in['name']) ?>"> <small><?= $err['name'] ?? '' ?></small></p> <p>البريد: <input name="email" value="<?= $e($in['email']) ?>"> <small><?= $err['email'] ?? '' ?></small></p> <p>الصلاحية: <select name="role"> <?php foreach (ROLES as $k => $v): ?><option value="<?= $k ?>" <?= $in['role'] === $k ? 'selected' : '' ?>><?= $v ?></option><?php endforeach; ?> </select></p> <p>المدينة: <input name="city" value="<?= $e($in['city']) ?>"></p> <button>حفظ</button></form>