العمل مع 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 / DELETEMethods بروتوكول HTTP المقابلة لعمليات Read / Create / Update / Delete
make:resourceأمر Artisan لإنشاء Resource Class جديد
Resource Classيحوّل سجلًا واحدًا إلى JSON بشكل مخصص
Resource::collection()يحوّل مجموعة من السجلات (Collection) إلى JSON
Accept: application/jsonHeader ضروري لضمان إرجاع استجابات JSON حتى في حالات الأخطاء (مثل 404)
with() داخل Resourceضروري عند إرجاع علاقة داخل Resource، لتفادي مشكلة N+1 queries

الخلاصة

يوفّر Laravel بنية جاهزة ومنظمة للتعامل مع REST API، بدءًا من تعريف الروابط في routes/api.php، مرورًا بفصل منطق تنسيق الاستجابة عن الـ Model باستخدام apiResources، وصولًا إلى التعامل مع العلاقات بين النماذج داخل استجابة JSON واحدة متسقة.

استخدام Resource Classes بدلاً من إرجاع الـ Model مباشرة يمنح تحكمًا كاملًا بشكل البيانات المُرجَعة، ويسهّل الحفاظ على استجابة API مستقرة وموحّدة حتى لو تغيّرت بنية قاعدة البيانات لاحقًا، وهو ما يجعله من أفضل الممارسات عند بناء أي API احترافي في Laravel.