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

بناء واجهة برمجة REST API

الواجهة التي تتحدث إليها تطبيقات الجوال وواجهات React. ⏱ نحو 40 دقيقة.

بعد هذا الدرس ستستطيع أن:
  • تشرح مبادئ REST: الموارد والعناوين والطرق ورموز الحالة.
  • تبني واجهة JSON كاملة لمورد: قراءة وإنشاء وحذف.
  • تطبّق قواعد تصميم الواجهات: أخطاء JSON، ورموز دقيقة، وأعمدة مختارة.
  • تحمي الواجهة برمز في ترويسة Authorization.

١ما REST؟

في الدرس 13 كتبت أول نقطة ترد بـ JSON. حين تكبر الواجهة نحتاج طريقة منظمة متفقًا عليها لتصميمها، وأشهرها أسلوب REST. فكرته بسيطة: البيانات موارد Resources لها عناوين (المستخدمون، المنتجات)، وطريقة HTTP تحدد ما تفعله بالمورد:

الطريقةالعنوانالعمليةرمز النجاح
GET/api/usersكل المستخدمين200
GET/api/users/3المستخدم 3200 أو 404
POST/api/usersإنشاء مستخدم (البيانات JSON في الجسم)201
PUT / PATCH/api/users/3تعديل المستخدم 3 (كاملًا / جزئيًّا)200
DELETE/api/users/3حذف المستخدم 3204 No Content

لاحظ: العنوان اسم (users) لا فعل (getUsers أو deleteUser)؛ الفعل هو طريقة HTTP. وهذا تطبيق مباشر لجدول CRUD في الدرس السابق: C=POST، R=GET، U=PUT، D=DELETE.

في المختبر نكتب /api/users.php?id=3 بدل /api/users/3، لأن الروابط الجميلة تحتاج .htaccess (الدرس 6). المنطق نفسه تمامًا.

٢واجهة المستخدمين كاملة

ملف واحد يفحص طريقة الطلب، ويرد دائمًا بـ JSON ورمز حالة دقيق. والدالة respond() تجمع ضبط الرمز والطباعة والخروج في سطر:

<?php// api/users.php: واجهة REST للمستخدمينrequire __DIR__ . '/../db.php';header('Content-Type: application/json; charset=utf-8'); function respond(int $status, $data = null): void{    http_response_code($status);    if ($data !== null) {        echo json_encode($data, JSON_UNESCAPED_UNICODE);    }    exit;} $method = $_SERVER['REQUEST_METHOD'];$id = isset($_GET['id']) ? (int) $_GET['id'] : null;$in = json_decode(file_get_contents('php://input'), true) ?? []; function find(PDO $pdo, int $id): ?array{    $s = $pdo->prepare('SELECT id, name, email, role, city FROM users WHERE id = ?');    $s->execute([$id]);    return $s->fetch() ?: null;} // GET /api/users.php            ← كل المستخدمين// GET /api/users.php?id=3       ← مستخدم واحدif ($method === 'GET') {    if ($id === null) {        $rows = $pdo->query('SELECT id, name, email, role FROM users ORDER BY id')->fetchAll();        respond(200, ['data' => $rows, 'count' => count($rows)]);    }    $u = find($pdo, $id) ?? respond(404, ['error' => 'المستخدم غير موجود']);    respond(200, $u);} // POST /api/users.php  {name, email, role}  ← إنشاءif ($method === 'POST') {    if (mb_strlen(trim($in['name'] ?? '')) < 3 || !filter_var($in['email'] ?? '', FILTER_VALIDATE_EMAIL)) {        respond(422, ['error' => 'name (3+) و email صحيح مطلوبان']);    }    try {        $s = $pdo->prepare('INSERT INTO users (name, email, role) VALUES (?, ?, ?)');        $s->execute([trim($in['name']), $in['email'], $in['role'] ?? 'trainee']);    } catch (PDOException $e) {        respond(409, ['error' => 'البريد مسجّل من قبل']);    }    header('Location: /api/users.php?id=' . $pdo->lastInsertId());    respond(201, find($pdo, (int) $pdo->lastInsertId()));} // DELETE /api/users.php?id=3  ← حذفif ($method === 'DELETE' && $id !== null) {    $s = $pdo->prepare('DELETE FROM users WHERE id = ?');    $s->execute([$id]);    $s->rowCount() ? respond(204) : respond(404, ['error' => 'المستخدم غير موجود']);} header('Allow: GET, POST, DELETE');respond(405, ['error' => 'طريقة غير مدعومة']);

تتبّع الرموز: 200، 200، 404، 201 (مع ترويسة Location للمورد الجديد)، 422، 204 بلا جسم، 404 (حُذف من قبل)، 405 (مع ترويسة Allow).

🧪 مختبر الخادمجرّب «📮 طلب يدوي»: أرسل GET و POST بجسم JSON (اختر نوع البيانات JSON) و DELETE ?id=4، ولاحظ الرموز.

٣قواعد تصميم واجهة جيدة

القاعدةلماذا؟
كل الردود JSON، حتى الأخطاء: {"error": "…"}العميل يقرأ شكلًا واحدًا دائمًا.
رمز الحالة يصف النتيجة بدقةالعميل يقرر بالرمز قبل قراءة الجسم: 2xx نجح، 4xx خطؤه، 5xx خطأ الخادم.
لا تُرجع أعمدة حساسةلا password_hash ولا بيانات داخلية؛ اختر الأعمدة بالاسم لا بـ *.
تحقّق كما في الصفحات تمامًاالواجهة يصل إليها أي برنامج، فالتحقق والصلاحيات أهم هنا لا أقل.
الإصدار في العنوان: /api/v1/usersتغيّر الواجهة لاحقًا دون كسر التطبيقات القديمة.
قوائم كبيرة؟ صفحات: ?page=2&limit=20لا ترسل مليون صف في رد واحد.

٤حماية الواجهة

تطبيق الجوال لا يحمل كوكي جلسة كالمتصفح عادة، فتُحمى الواجهات غالبًا بـرمز Token يُرسل في الترويسة Authorization:

<?phpheader('Content-Type: application/json; charset=utf-8');const API_TOKEN = 'tvtc-demo-7f3a9c';   // في الواقع: من متغير بيئة، ولكل مستخدم رمزه $auth = $_SERVER['HTTP_AUTHORIZATION'] ?? '';if ($auth !== 'Bearer ' . API_TOKEN) {    http_response_code(401);    header('WWW-Authenticate: Bearer');    exit(json_encode(['error' => 'رمز الدخول مفقود أو غير صحيح'], JSON_UNESCAPED_UNICODE));}echo json_encode(['secret' => 'تقارير المدير'], JSON_UNESCAPED_UNICODE);

الترويسة Authorization تصل في $_SERVER['HTTP_AUTHORIZATION']. وفي مقرر 242 ستتعلم JWT، وهو رمز موقّع يحمل هوية المستخدم.

توقّع وتحقّق

لإنشاء مورد جديد في REST نستخدم:

والرد 201.

رمز النجاح الشائع لـ DELETE بلا محتوى في الرد:

No Content.

أي عنوان أقرب لأسلوب REST؟

الفعل في الطريقة.

طريقة غير مدعومة على المورد:

Method Not Allowed.

الخلاصة

تحدٍّ برمجي: واجهة المنتجات

متقدم

اكتب api/products.php لقاعدة «متجر الواحة» بأسلوب REST، وكل الردود JSON.

المطلوب:
  • GET ← 200 و {"data": [...]} بالمنتجات المتوفرة فقط (stock > 0) بالأعمدة id و name و price مرتبة بـ id.
  • GET ?id=3 ← 200 والمنتج (id و name و price و stock)، أو 404 و {"error":"not found"}.
  • POST بجسم JSON فيه name و price ← 201 و {"id":13,"name":"…","price":…} (التصنيف «جديد» والمخزون 0). وبيانات ناقصة أو سعر غير موجب ← 422.
  • أي طريقة أخرى ← 405.
👀 اعرض الاستجابة المطلوبة
✓ التحقق الآلي: يرسل المختبر هذه الطلبات إلى خادمك بالترتيب ثم يفحص الاستجابات
  1. GET /api/products.php
  2. GET /api/products.php?id=3
  3. GET /api/products.php?id=500
  4. POST /api/products.php ({"name":"زعفران","price":60})
  5. POST /api/products.php ({"name":"","price":-1})
  6. GET /api/products.php?id=13
  7. DELETE /api/products.php
  • الكود يعمل بلا أخطاء أو تحذيرات
  • GET ← المتوفر فقط بالأعمدة المطلوبة
  • GET ?id=3 ← المنتج كاملًا
  • GET ?id=500 ← 404
  • POST صحيح ← 201
  • POST ← {"id":13,"name":"زعفران","price":60}
  • POST ناقص ← 422
  • المنتج الجديد صار في القاعدة
  • DELETE ← 405
  • الردود JSON

💡 تلميح 1
ابدأ بدالة out($status, $data) مثل respond في الدرس.
💡 تلميح 2
ترتيب الفحوص: GET مع id، ثم GET، ثم POST، ثم 405.

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

<?phprequire __DIR__ . '/../db.php';header('Content-Type: application/json; charset=utf-8'); function out(int $s, $d = null): void { http_response_code($s); if ($d !== null) echo json_encode($d, JSON_UNESCAPED_UNICODE); exit; } $m = $_SERVER['REQUEST_METHOD'];if ($m === 'GET' && isset($_GET['id'])) {    $s = $pdo->prepare('SELECT id, name, price, stock FROM products WHERE id = ?');    $s->execute([(int) $_GET['id']]);    $p = $s->fetch();    $p ? out(200, $p) : out(404, ['error' => 'not found']);}if ($m === 'GET') {    $rows = $pdo->query('SELECT id, name, price FROM products WHERE stock > 0 ORDER BY id')->fetchAll();    out(200, ['data' => $rows]);}if ($m === 'POST') {    $in = json_decode(file_get_contents('php://input'), true) ?? [];    $name = trim($in['name'] ?? '');    $price = $in['price'] ?? null;    if ($name === '' || !is_numeric($price) || $price <= 0) out(422, ['error' => 'name و price موجب مطلوبان']);    $s = $pdo->prepare("INSERT INTO products (name, category, price, stock) VALUES (?, 'جديد', ?, 0)");    $s->execute([$name, $price]);    out(201, ['id' => (int) $pdo->lastInsertId(), 'name' => $name, 'price' => $price + 0]);}header('Allow: GET, POST');out(405, ['error' => 'method not allowed']);
انتقل إلى تقييم الدرساختيار من متعدد، وصح وخطأ، وأكمل الفراغ، وتتبّع الناتج.ابدأ التقييم ←

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