العمل مع REST API وApiResources في Laravel
ما هو API
الـ API اختصار لـ Application Programming Interface، وهو ببساطة وسيط يقدّم خدمة لبرنامج معين، حيث يتواصل برنامجك مع هذا الوسيط لكي يترجم له مجموعة من الأمور التي يحتاجها حتى يفهمها ويتعامل معها.
لا يمكن اليوم الاستغناء عن الـ API في أي مشروع تقريبًا؛ فكل موقع تقريبًا يستخدمه بشكل أو بآخر. على سبيل المثال، عندما يدعم موقع ما خاصية التسجيل عبر Facebook، فإن عملية التسجيل هذه تمر عبر API خاص بفيسبوك، وهذا الوسيط هو من يقوم بالرد على السيرفر ليخبره فيما إذا كانت البيانات المُدخلة صحيحة أم لا.
ما هو REST API
REST اختصار لـ Representational State Transfer، وهو أحد أنواع الـ API الأكثر شيوعًا، حيث يقوم بنقل البيانات بين العميل (Client) والخادم (Server) عبر بروتوكول HTTP. جميع العمليات تتم عبر هذا البروتوكول، ونقصد بالعمليات هنا العمليات الأساسية والشائعة في عالم البرمجة، وهي: Create، Read، Update، Delete، ويُختصر لها بـ CRUD.
يوفّر بروتوكول HTTP مجموعة من الـ Methods التي من خلالها يُترجَم نوع الطلب المُرسل من الـ Client إلى الـ Server. فمن خلال مسار الرابط (URL) مع نوع الـ Method، يفهم الـ API ما هو المطلوب بالضبط ويقوم بمعالجة الطلب. تتلخص أهم Methods هذا البروتوكول فيما يلي:
- GET: تُستخدم لجلب البيانات من السيرفر (قراءة البيانات - Read).
- POST: تُستخدم لإضافة بيانات جديدة (Create).
- PUT: تُستخدم لتعديل بيانات موجودة مسبقًا (Update).
- DELETE: تُستخدم لحذف بيانات (Delete).
فالـ API بشكله الافتراضي، عندما يستقبل طلبًا بنمط POST سيفهم أنك تريد Create أي إضافة بيانات جديدة. وعندما يستقبل طلبًا بنمط GET سيفهم أنك تريد Read أي جلب وقراءة بيانات. وعندما يستقبل طلبًا بنمط PUT سيفهم أنك تريد التعديل على بيانات موجودة مسبقًا في قاعدة البيانات.
التعامل مع API باستخدام Laravel
في هذا المثال سنقوم بجلب جميع السجلات من جدول brands.
إنشاء Route
بداية، نحتاج لإنشاء route، حيث توفر Laravel ملفًا مخصصًا لروابط الـ API في المسار routes/api.php:
Route::get('brands', [BrandController::class, 'index']);جلب البيانات داخل الكونترولر
public function index()
{
return Brand::get();
}عرض البيانات
لعرض البيانات، نحتاج لإضافة كلمة api إلى الرابط. فإذا كنا نريد مثلًا عرض الطلاب:
http://www.test.test/api/students
ولعرض brands:
http://www.test.test/api/brands
سنحصل على نتيجة مشابهة لما يلي:
[
{
"id": 11,
"name": "possimus",
"created_at": "2021-05-09T04:25:19.000000Z",
"updated_at": "2021-05-09T04:25:19.000000Z"
},
{
"id": 12,
"name": "dolores",
"created_at": "2021-05-09T04:25:19.000000Z",
"updated_at": "2021-05-09T04:25:19.000000Z"
}
]عرض تفاصيل brand معين
Route::get('brands/{brand}', [BrandController::class, 'show']);public function show(Brand $brand)
{
return $brand;
}http://www.test.test/api/brands/11
{
"id": 11,
"name": "possimus",
"created_at": "2021-05-09T04:25:19.000000Z",
"updated_at": "2021-05-09T04:25:19.000000Z"
}كما نلاحظ، تم إرجاع جميع بيانات brand كاملة كما هي مخزنة في قاعدة البيانات. وبما أننا نستخدم Route Model Binding هنا، فليس من السهل التحكم بالحقول المُرجَعة عبر select مباشرة داخل الكونترولر بأسلوب مرن. لحل هذه المشكلة، نلجأ إلى apiResources.
لماذا يجب وضع api بعد الدومين في الرابط
http://www.test.test/api/brands
الكلاس المسؤول عن هذا السلوك هو RouteServiceProvider، حيث يحتوي على الدالة boot، وبداخلها يتم تحديد api كـ prefix، بحيث يتم التعامل مع كل الروابط الموجودة في ملف routes/api.php تحت هذا البادئة:
$this->routes(function () {
Route::prefix('api')
->middleware('api')
->namespace($this->namespace)
->group(base_path('routes/api.php'));
});كما يحتوي هذا التعريف على إعدادات أخرى، مثل تحديد middleware باسم api يُطبَّق تلقائيًا على كل هذه الروابط.
ما هو apiResources
يمكن تخيّل apiResources على أنه طبقة وسيطة بين الـ Model وبين استجابة JSON التي يتم إرجاعها من الـ API، حيث يوفر طريقة سهلة ومنظمة للتحكم بالبيانات المُرجَعة على شكل JSON، بدلاً من إرجاع الـ Model كما هو مباشرة.
يتكون apiResource من عنصرين أساسيين:
- Resource Class: يُستخدم لتحويل سجل واحد (Model واحد) إلى JSON، مثلًا لجلب brand معين له id محدد.
- Resource Collection: يُستخدم لإرجاع مجموعة من السجلات، مثل جميع الـ brands.
طريقة تطبيق apiResource
لإنشاء Resource جديد، نستخدم الأمر:
php artisan make:resource ResourceName
لإنشاء Resource خاص بـ brands:
php artisan make:resource BrandResource
بعد تنفيذ الأمر، يتم إنشاء كلاس جديد باسم BrandResource داخل المسار app/Http/Resources/BrandResource.php:
class BrandResource extends JsonResource
{
public function toArray($request)
{
return parent::toArray($request);
}
}وبداخل الدالة toArray، نحدد بالضبط البيانات التي نريد إرجاعها. لنفترض أننا نريد إرجاع id وname وcreated_at فقط:
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'created_at' => $this->created_at,
];
}بعد ذلك، يجب تعديل دوال الكونترولر BrandController لإرجاع الـ Resource بدلاً من إرجاع الـ object مباشرة:
public function show(Brand $brand)
{
return new BrandResource($brand);
}الآن عند زيارة الرابط، سنحصل على النتيجة التالية:
http://www.test.test/api/brands/11
{
"data": {
"id": 11,
"name": "possimus",
"created_at": "2021-05-09T04:25:19.000000Z"
}
}كما نلاحظ، حصلنا فقط على الحقول التي حددناها، ولاحظ أيضًا أنه تمت إضافة طبقة جديدة تلقائيًا باسم data تحيط بالبيانات المُرجَعة.
تحويل دالة index لاستخدام Resource Collection
public function index()
{
$brands = Brand::get();
return BrandResource::collection($brands);
}http://www.test.test/api/brands
{
"data": [
{
"id": 11,
"name": "possimus",
"created_at": "2021-05-09T04:25:19.000000Z"
},
{
"id": 12,
"name": "dolores",
"created_at": "2021-05-09T04:25:19.000000Z"
}
]
}التعامل مع السجلات غير الموجودة
ماذا لو أردنا جلب بيانات brand معين، لكن هذا الـ brand غير موجود أصلًا في قاعدة البيانات؟
http://www.test.test/api/brands/2222
إذا فتحنا هذا الرابط مباشرة من المتصفح، سيتم إرجاع خطأ:
404 NOT FOUND
أما إذا كنا نختبر الـ API عبر أداة مثل Postman، فإن الاستجابة الافتراضية ستكون صفحة HTML وليست JSON، وهذا غير مناسب على الإطلاق للتطبيقات الحقيقية التي تتوقع استجابة JSON دائمًا. لتفادي هذه المشكلة، يجب تحديد الـ Header التالي في الطلب:
Key = Accept Value = application/json
لتحديد ذلك في Postman، نذهب إلى تبويب Headers ونضيف القيمتين أعلاه. بهذا الشكل، سيقوم Laravel بإرجاع استجابة JSON منسقة بشكل صحيح حتى في حالات الأخطاء، بدلاً من صفحة HTML.
استخدام العلاقات في apiResources
لنفترض أن لدينا جدول products يحتوي على حقل brand_id، ونريد جلب المنتجات مع اسم الـ brand التابع لكل منتج، وذلك باستخدام apiResource.
تعريف العلاقة
أولًا نُعرّف العلاقة بين Product وBrand، حيث كل منتج ينتمي إلى brand واحد:
public function brand()
{
return $this->belongsTo(Brand::class);
}إنشاء Resource للـ Product
class ProductResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'price' => $this->price,
'qty' => $this->qty,
'brand' => new BrandResource($this->brand),
];
}
}كما نلاحظ، تم استخدام BrandResource داخل ProductResource نفسه، وتم تمرير العلاقة brand إليه مباشرة. بهذا الشكل، يمكن التحكم بشكل البيانات المُرجَعة لكل من Product وBrand بشكل مستقل ومتناسق في آن واحد.
دالة index في ProductController
public function index()
{
$products = Product::with('brand')->get();
return ProductResource::collection($products);
}ملاحظة مهمة: تم استخدام with('brand') هنا لتحميل العلاقة مسبقًا (Eager Loading)، وهذا ضروري لتفادي مشكلة N+1 queries التي قد تحدث لو تم الاعتماد على تحميل العلاقة بشكل كسول (Lazy Loading) لكل منتج على حدة داخل الـ Resource.
النتيجة النهائية ستكون بالشكل التالي:
{
"data": [
{
"id": 1,
"name": "tenetur",
"price": 260,
"qty": 301,
"brand": {
"id": 14,
"name": "omnis",
"created_at": "2021-05-09T04:25:19.000000Z"
}
},
{
"id": 2,
"name": "error",
"price": 432,
"qty": 270,
"brand": {
"id": 19,
"name": "ab",
"created_at": "2021-05-09T04:25:19.000000Z"
}
}
]
}جدول ملخّص للمفاهيم
| العنصر | الوصف |
|---|---|
| routes/api.php | ملف الروابط المخصص للـ API، ويُضاف له تلقائيًا بادئة api |
| GET / POST / PUT / DELETE | Methods بروتوكول HTTP المقابلة لعمليات Read / Create / Update / Delete |
| make:resource | أمر Artisan لإنشاء Resource Class جديد |
| Resource Class | يحوّل سجلًا واحدًا إلى JSON بشكل مخصص |
| Resource::collection() | يحوّل مجموعة من السجلات (Collection) إلى JSON |
| Accept: application/json | Header ضروري لضمان إرجاع استجابات JSON حتى في حالات الأخطاء (مثل 404) |
| with() داخل Resource | ضروري عند إرجاع علاقة داخل Resource، لتفادي مشكلة N+1 queries |
الخلاصة
يوفّر Laravel بنية جاهزة ومنظمة للتعامل مع REST API، بدءًا من تعريف الروابط في routes/api.php، مرورًا بفصل منطق تنسيق الاستجابة عن الـ Model باستخدام apiResources، وصولًا إلى التعامل مع العلاقات بين النماذج داخل استجابة JSON واحدة متسقة.
استخدام Resource Classes بدلاً من إرجاع الـ Model مباشرة يمنح تحكمًا كاملًا بشكل البيانات المُرجَعة، ويسهّل الحفاظ على استجابة API مستقرة وموحّدة حتى لو تغيّرت بنية قاعدة البيانات لاحقًا، وهو ما يجعله من أفضل الممارسات عند بناء أي API احترافي في Laravel.
لقد انشات InvoicesResource وكتبت الكود return new InvoicesResource($invoices); تم ارجاع جميع الداتا null , لماذا ؟
شكرًا لسؤالك. هذه المشكلة شائعة جدًا، وسببها في الغالب واحد من الاحتمالات التالية:
public function toArray($request) { return [ 'id' => $this->id, 'total' => $this->total, ]; }لو تأكدت من هذه النقاط الثلاث ولا تزال المشكلة قائمة، شارك جزءًا من الكود (الكونترولر وكلاس Resource) وسأساعدك في تحديد السبب بدقة.
شكراً الشرح واضح أكثر من التطبيق العملي باليوتيوب ^_^
شكرًا جزيلًا لك، سعيد أن الشرح المكتوب أفادك أكثر. أحاول دائمًا أن أشرح كل خطوة بالتفصيل مع توضيح السبب وليس فقط الكود.
ازاي اعمل documentation ع النت بيبقى فيه الداتا و ال methods اللي بعملها بحيث ان بتوع الفرونت إند يقدروا يستخدموا ال url و يجيبوا الداتا عادي زي ده كده ? https://fakestoreapi.com/docs
سؤال ممتاز، وهذا فعلًا موضوع مهم لأي API يُستخدم من فريق Frontend منفصل. باختصار، هناك عدة أدوات وطرق شائعة لتحقيق ذلك في مشاريع Laravel:
لو أردت، يمكنني لاحقًا تخصيص مقال كامل يشرح خطوة بخطوة كيفية إعداد Scribe أو Swagger على مشروع Laravel حقيقي.
الله يعطيك العافيه
الله يعافيك، شكرًا لمتابعتك.
شكرا جزيلا
العفو، وشكرًا لك على وقتك في القراءة.
السلام عليكم استاذ ، لدي سؤال. ماهو اصدار لارافيل الذي تعمل عليه . وهل ممكن تظع لنا الكود كامل على Github
وعليكم السلام ورحمة الله وبركاته. الأمثلة في هذا المقال مبنية على Laravel 8، وتعمل بنفس الشكل تقريبًا على الإصدارات 9 وما بعدها دون تعديل يُذكر، باستثناء بعض التغييرات الطفيفة في هيكلية المشروع الافتراضية بدءًا من Laravel 11.
بخصوص الكود الكامل على GitHub، حاليًا لا يوجد repo جاهز مرفق مع هذا المقال بالذات، لكنني آخذ الفكرة بعين الاعتبار لمقالات لاحقة. في الوقت الحالي، الكود الموجود في المقال (الـ Migration والـ Model والـ Controller وResource) كافٍ لإعادة بناء نفس المثال (brands وproducts) من الصفر لو أردت تجربته محليًا.