(API)بناء واجهة برمجة REST API
الواجهة التي تتحدث إليها تطبيقات الجوال وواجهات React. ⏱ نحو 40 دقيقة.
- تشرح مبادئ
REST: الموارد والعناوين والطرق ورموز الحالة. - تبني واجهة
JSONكاملة لمورد: قراءة وإنشاء وحذف. - تطبّق قواعد تصميم الواجهات: أخطاء
JSON، ورموز دقيقة، وأعمدة مختارة. - تحمي الواجهة برمز في ترويسة
Authorization.
١ما REST؟
في الدرس 13 كتبت أول نقطة ترد بـ JSON. حين تكبر الواجهة نحتاج طريقة منظمة متفقًا عليها لتصميمها، وأشهرها أسلوب REST. فكرته بسيطة: البيانات موارد Resources لها عناوين (المستخدمون، المنتجات)، وطريقة HTTP تحدد ما تفعله بالمورد:
| الطريقة | العنوان | العملية | رمز النجاح |
|---|---|---|---|
GET | /api/users | كل المستخدمين | 200 |
GET | /api/users/3 | المستخدم 3 | 200 أو 404 |
POST | /api/users | إنشاء مستخدم (البيانات JSON في الجسم) | 201 |
PUT / PATCH | /api/users/3 | تعديل المستخدم 3 (كاملًا / جزئيًّا) | 200 |
DELETE | /api/users/3 | حذف المستخدم 3 | 204 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' => 'طريقة غير مدعومة']);
<?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);
تتبّع الرموز: 200، 200، 404، 201 (مع ترويسة Location للمورد الجديد)، 422، 204 بلا جسم، 404 (حُذف من قبل)، 405 (مع ترويسة Allow).
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 نستخدم:
رمز النجاح الشائع لـ DELETE بلا محتوى في الرد:
No Content.أي عنوان أقرب لأسلوب REST؟
طريقة غير مدعومة على المورد:
Method Not Allowed.الخلاصة
REST: المورد اسم في العنوان، والعملية طريقةHTTP.GET 200،POST 201 + Location،DELETE 204، والأخطاء 404 و 405 و 409 و 422.- كل الردود
JSONحتى الأخطاء، معContent-Typeمناسب. - اختر الأعمدة بالاسم، وتحقق كما في الصفحات، واحمِ الواجهة برمز.
- دالة
respond()واحدة تختصر الرمز والطباعة والخروج.
تحدٍّ برمجي: واجهة المنتجات
متقدماكتب 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.
👀 اعرض الاستجابة المطلوبة
GET /api/products.phpGET /api/products.php?id=3GET /api/products.php?id=500POST /api/products.php({"name":"زعفران","price":60})POST /api/products.php({"name":"","price":-1})GET /api/products.php?id=13DELETE /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
💡 تلميح 2
حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.
<?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']);
<?php// db.php: الاتصال بقاعدة البيانات في مكان واحد// في المختبر نستخدم SQLite (ملف). وعلى XAMPP بدّل السطر بسطر MySQL الذي تحته$pdo = new PDO('sqlite:' . __DIR__ . '/store.sqlite');// $pdo = new PDO('mysql:host=localhost;dbname=store;charset=utf8mb4', 'root', ''); $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); // الأخطاء استثناءات$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); // الصفوف مصفوفات ترابطية