تخطَّ إلى المحتوى

بناء API بصيغة JSON في Laravel

الدرس 10 من 11 · Laravel

مدونتك حتى الآن ترد على المتصفح بصفحات HTML. لكن ماذا لو أردت تطبيق موبايل يعرض المقالات نفسها؟ التطبيق لا يريد صفحة جاهزة، بل يريد البيانات وحدها ليعرضها بطريقته. في هذا الدرس ستبني API: مسارات ترد بصيغة JSON يفهمها أي تطبيق، سواء كُتب بـ Flutter أو JavaScript أو غيرهما.

الـ API (واجهة برمجة التطبيقات) طريقة يتحدث بها برنامج مع برنامج آخر. في المطعم أنت لا تدخل المطبخ: تطلب من النادل بلغة متفق عليها، فيعود إليك بالطبق. الـ API هو النادل، والمطبخ هو قاعدة بياناتك، والطبق بيانات بصيغة JSON: نص منظم من مفاتيح وقيم.

الصفحات تبقى في routes/web.php، والـ API له ملف مستقل تفعّله بأمر واحد:

نافذة طرفيّة
php artisan install:api

الأمر ينشئ الملف routes/api.php ويسجّله في bootstrap/app.php، ويثبّت حزمة Sanctum التي تحمي الـ API لاحقاً. ولمسارات هذا الملف ثلاث خصائص:

  • كل مسار يبدأ تلقائياً بـ /api.
  • لا جلسات ولا كوكيز، ولذلك لا تحتاج @csrf.
  • الطلب لا يحمل «ذاكرة»: كل طلب مستقل بنفسه.

في Laravel يكفي أن تُرجع مصفوفة ليحوّلها إلى JSON:

routes/api.php
Route::get('/ping', function () {
return ['message' => 'الخادم يعمل', 'time' => now()->format('H:i')];
});

شغّل php artisan serve وافتح http://localhost:8000/api/ping:

الناتج
{"message":"الخادم يعمل","time":"10:30"}

والأمر نفسه مع Eloquent: لو أرجعت Post::latest()->get() لحصلت على كل المقالات بكل أعمدتها.

أي API يدير «شيئاً» يحتاج العمليات الخمس نفسها: عرض الكل، وعرض واحد، وإضافة، وتعديل، وحذف. أنشئ متحكماً مجهزاً لها:

نافذة طرفيّة
php artisan make:controller Api/PostController --api

ثم سجّل مساراته كلها بسطر واحد:

routes/api.php
use App\Http\Controllers\Api\PostController;
Route::apiResource('posts', PostController::class);

وتأكد بالأمر php artisan route:list --path=api/posts:

الناتج
GET|HEAD api/posts ......... posts.index › Api\PostController@index
POST api/posts ......... posts.store › Api\PostController@store
GET|HEAD api/posts/{post} .. posts.show › Api\PostController@show
PUT|PATCH api/posts/{post} .. posts.update › Api\PostController@update
DELETE api/posts/{post} .. posts.destroy › Api\PostController@destroy

لاحظ أن الرابط api/posts نفسه يعني «اعرض» مع GET و«أضف» مع POST. طريقة الطلب هي الفعل، والرابط هو الشيء.

app/Http/Controllers/Api/PostController.php
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Post;
use Illuminate\Http\Request;
class PostController extends Controller
{
public function index()
{
return Post::latest()->paginate(10);
}
public function show(Post $post)
{
return $post;
}
public function store(Request $request)
{
$validated = $request->validate([
'title' => 'required|min:5|max:100',
'body' => 'required|min:20',
]);
$post = Post::create($validated);
return response()->json($post, 201);
}
public function update(Request $request, Post $post)
{
$validated = $request->validate([
'title' => 'required|min:5|max:100',
'body' => 'required|min:20',
]);
$post->update($validated);
return $post;
}
public function destroy(Post $post)
{
$post->delete();
return response()->noContent();
}
}

فيه ثلاثة أشياء جديدة:

  • ربط الـ Model بالمسار (Route Model Binding): كتبنا Post $post بدل int $id. Laravel يرى أن اسم المتغير يطابق {post} في المسار، فيجلب المقال بنفسه. وإن لم يجده ردّ بـ 404 دون أن تكتب findOrFail.
  • paginate(10): يُرجع عشرة مقالات فقط، ومعها معلومات الصفحات. تطبيق الموبايل يطلب الصفحة التالية بـ ?page=2.
  • رموز الحالة: الرد لا يحمل البيانات فقط، بل رقماً يلخص ما حدث.
الرمز معناه متى نرسله
200 نجح الطلب العرض والتعديل (الافتراضي)
201 أُنشئ عنصر جديد بعد الإضافة
204 نجح ولا محتوى بعد الحذف
404 غير موجود مقال برقم لا وجود له
422 بيانات غير صالحة فشل التحقق

أرسل طلب POST إلى /api/posts بعنوان قصير وبلا محتوى. في تطبيق الصفحات كان Laravel يعيدك إلى النموذج، أما هنا فيرد بالرمز 422 وهذا الجسم:

الناتج
{
"message": "The title field must be at least 5 characters. (and 1 more error)",
"errors": {
"title": ["The title field must be at least 5 characters."],
"body": ["The body field is required."]
}
}

قواعد التحقق لم تتغير، لكن الرد صار بيانات يعرضها التطبيق تحت كل حقل.

هكذا تجرّب الإضافة من الطرفية:

نافذة طرفيّة
curl -X POST http://localhost:8000/api/posts \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{"title":"abc"}'

والأسهل برنامج بواجهة رسومية مثل Postman أو Insomnia: تختار طريقة الطلب وتكتب JSON وترى الرد منسقاً.

إرجاع الـ Model مباشرة يكشف كل أعمدة الجدول، وأي تغيير في قاعدة البيانات يكسر التطبيقات التي تعتمد على الشكل القديم. الحل طبقة بينهما تحدد أنت فيها شكل الرد:

نافذة طرفيّة
php artisan make:resource PostResource
app/Http/Resources/PostResource.php
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'body' => $this->body,
'created' => $this->created_at->format('Y-m-d'),
];
}

ثم استخدمه في المتحكم (مع use App\Http\Resources\PostResource; في أعلى الملف):

public function index()
{
return PostResource::collection(Post::latest()->paginate(10));
}
public function show(Post $post)
{
return new PostResource($post);
}

الآن GET /api/posts/1 يرد بالحقول التي اخترتها فقط، داخل المفتاح data:

الناتج
{
"data": {
"id": 1,
"title": "أول مقال في المدونة",
"body": "مرحباً بكم في مدونتي الجديدة عن البرمجة.",
"created": "2026-11-12"
}
}

وفي store أرجع (new PostResource($post))->response()->setStatusCode(201) ليبقى الرمز 201. أما قائمة المقالات فتأتي مصفوفة داخل data، وبجانبها المفتاحان links و meta وفيهما رقم الصفحة الحالية وعدد المقالات الكلي.

  1. أضف مسار /api/ping وافتحه في المتصفح، ثم افتح /api/posts وقارن الرد قبل استخدام PostResource وبعده.
  2. اطلب /api/posts/999 مرة من المتصفح ومرة بـ curl مع ترويسة Accept: application/json، وقارن الردين.
  3. أضف إلى الرد عدد التعليقات: استخدم Post::withCount('comments') في المتحكم، و 'comments_count' => $this->comments_count في الـ Resource.
  4. تحدٍّ: أضف Route::apiResource('posts.comments', ...) بمتحكم جديد يعرض تعليقات مقال ويضيف إليها.
  • كتابة المسارات في web.php: مسارات الصفحات تطلب @csrf مع كل POST، فيفشل طلب التطبيق بالخطأ 419. مكان الـ API هو routes/api.php.
  • نسيان البادئة: المسار المكتوب /posts في api.php عنوانه الفعلي /api/posts.
  • إرجاع 200 دائماً: التطبيق يقرأ الرمز قبل الجسم. استخدم 201 للإضافة و 204 للحذف، واترك 404 و 422 لـ Laravel.
  • إرجاع الـ Model كما هو: قد يتسرب عمود لم تقصد نشره. حدد الحقول بـ Resource.

سؤال

كتبت في routes/api.php المسار Route::get('/posts', ...). ما العنوان الذي يطلبه التطبيق؟

سؤال

أرسل التطبيق مقالاً جديداً بعنوان قصير جداً، ومعه الترويسة Accept: application/json. بماذا يرد Laravel؟

  • الـ API يرد ببيانات JSON بدل صفحات HTML، ومساراته في routes/api.php وتبدأ بـ /api.
  • Route::apiResource يسجّل مسارات العرض والإضافة والتعديل والحذف بسطر واحد.
  • كتابة Post $post في الدالة تجلب المقال تلقائياً، وترد بـ 404 إن لم يوجد.
  • رموز الحالة جزء من الرد: 201 للإنشاء، و 204 للحذف، و 422 لفشل التحقق.
  • API Resources تحدد شكل الرد، وتفصل تطبيقاتك عن شكل قاعدة البيانات.