التعامل مع API في Laravel – الجزء الرابع: ما هو API Versioning وكيفية تطبيقه في Laravel

عند بناء API لتطبيق حقيقي، قد نصل بعد فترة إلى مرحلة نحتاج فيها إلى تغيير شكل البيانات أو إضافة خصائص جديدة أو تعديل طريقة عمل بعض Endpoints.

المشكلة أن تطبيقات الهاتف أو أنظمة Frontend القديمة قد تكون ما زالت تعتمد على النسخة الحالية من API، وبالتالي فإن تغيير API مباشرة قد يؤدي إلى توقف هذه التطبيقات عن العمل.

هنا يظهر مفهوم مهم جدًا في تصميم واجهات البرمجة يسمى:

API Versioning

في هذا الجزء من سلسلة Laravel API سنتعرف على مفهوم Versioning، ولماذا نحتاج إليه، وكيفية إنشاء أكثر من إصدار من API داخل Laravel بطريقة منظمة وقابلة للصيانة.

جدول المحتويات

  1. ما هو API Versioning؟
  2. لماذا نحتاج إلى Versioning؟
  3. ما هي Breaking Changes؟
  4. إضافة Version إلى URL
  5. متى يجب إنشاء إصدار جديد؟
  6. متى لا نحتاج إلى إصدار جديد؟
  7. طرق API Versioning
  8. URL Versioning
  9. تنظيم Versions داخل Laravel
  10. إنشاء Controllers للإصدار V1
  11. إنشاء Routes للإصدار V1
  12. إنشاء الإصدار V2
  13. إنشاء Routes للإصدار V2
  14. استخدام ملف Routes واحد
  15. استخدام ملف مستقل لكل Version
  16. تسجيل ملفات Routes في bootstrap/app.php
  17. Versioning للـAPI Resources
  18. مشاركة Business Logic بين الإصدارات
  19. إنشاء V3
  20. إيقاف الإصدارات القديمة تدريجيًا
  21. أفضل الممارسات
  22. هيكل مشروع مقترح
  23. ملخص الدرس
  24. الخلاصة

ما هو API Versioning؟

API Versioning هو أسلوب يسمح لنا بإنشاء أكثر من إصدار من API في الوقت نفسه، بحيث تستطيع التطبيقات القديمة الاستمرار في استخدام الإصدار القديم، بينما تستخدم التطبيقات الحديثة الإصدار الجديد.

على سبيل المثال:

GET /api/v1/brands

GET /api/v2/brands

لدينا هنا نفس Resource:

brands

لكن كل Endpoint ينتمي إلى Version مختلف.

قد يعيد V1 مثلًا:

{
    "id": 1,
    "name": "Apple"
}

بينما يعيد V2:

{
    "id": 1,
    "name": "Apple",
    "slug": "apple",
    "products_count": 25
}

بهذه الطريقة يمكن تطوير API دون إجبار جميع التطبيقات التي تستخدمه على التحديث في اللحظة نفسها.

لماذا نحتاج إلى API Versioning؟

لنفترض أن لدينا تطبيق Android وiOS يستخدمان:

/api/v1/brands

ثم قررنا بعد عدة أشهر تغيير بنية Response بالكامل.

إذا قمنا بتغيير Endpoint نفسه مباشرة فقد تواجه النسخ القديمة من التطبيق مشاكل لأنها تتوقع Response بالشكل السابق.

الحل هو الاحتفاظ بـV1 وإنشاء:

/api/v2/brands

فتصبح الصورة:

Old Mobile App
      |
      v
/api/v1/brands


New Mobile App
      |
      v
/api/v2/brands

وبذلك يستطيع الفريق نقل المستخدمين تدريجيًا إلى الإصدار الجديد.

ما هي Breaking Changes؟

ليس كل تعديل في API يحتاج إلى Version جديد.

نحتاج عادةً إلى التفكير في إصدار جديد عندما يؤدي التغيير إلى كسر Client يعتمد على الإصدار الحالي.

هذا النوع من التغييرات يسمى:

Breaking Change

من الأمثلة:

  • حذف حقل كان موجودًا في Response.
  • تغيير اسم حقل.
  • تغيير نوع البيانات من String إلى Object مثلًا.
  • تغيير بنية JSON بالكامل.
  • حذف Endpoint مستخدم.
  • تغيير قواعد Authentication بشكل غير متوافق.
  • تغيير معنى أو سلوك Endpoint موجود.
  • تغيير Parameters مطلوبة من Client.

مثلًا إذا كان V1 يعيد:

{
    "name": "Apple"
}

وقمنا في نفس Endpoint بتغيير الحقل إلى:

{
    "brand_name": "Apple"
}

فقد يتوقف Client القديم الذي يبحث عن:

name

عن العمل.

إضافة Version إلى URL

من أكثر الطرق وضوحًا لتطبيق Versioning وضع رقم الإصدار داخل URI.

مثل:

/api/v1/brands

/api/v2/brands

/api/v3/brands

وتكون Endpoints مثلًا:

GET    /api/v1/brands
POST   /api/v1/brands
GET    /api/v1/brands/{brand}
PUT    /api/v1/brands/{brand}
DELETE /api/v1/brands/{brand}

ثم الإصدار الثاني:

GET    /api/v2/brands
POST   /api/v2/brands
GET    /api/v2/brands/{brand}
PUT    /api/v2/brands/{brand}
DELETE /api/v2/brands/{brand}

متى يجب إنشاء Version جديد؟

يفضل إنشاء Version جديد عند وجود تغييرات غير متوافقة مع Clients التي تستخدم الإصدار الحالي.

مثل:

  • تغيير كبير في شكل Responses.
  • إزالة Fields مستخدمة.
  • تغيير Request Structure.
  • إعادة تصميم مجموعة كبيرة من Endpoints.
  • تغيير طريقة Authentication الأساسية.
  • تغيير Business Rules تؤثر على Contract الخاص بالـAPI.

متى لا نحتاج إلى Version جديد؟

لا ينبغي إنشاء:

v2
v3
v4
v5

عند كل تعديل صغير.

على سبيل المثال، إصلاح Bug داخلي لا يغير API Contract لا يحتاج عادةً إلى Version جديد.

وكذلك تحسين أداء Query أو إضافة Index إلى قاعدة البيانات لا يحتاج إلى Version جديد لأن Client لا يرى هذا التغيير أصلًا.

القاعدة الأساسية هي:

إذا لم ينكسر Contract الذي يعتمد عليه Client، فغالبًا لا تحتاج إلى Version جديد.

طرق API Versioning

هناك أكثر من طريقة لتحديد Version الخاص بـAPI.

من أشهرها:

  • URL Versioning.
  • Header Versioning.
  • Media Type Versioning.
  • Query Parameter Versioning.

في هذه السلسلة سنستخدم الطريقة الأبسط والأوضح:

URL Versioning

مثل:

/api/v1/brands

لماذا سنستخدم URL Versioning؟

الميزة الأساسية لهذه الطريقة أنها واضحة جدًا للمطور ولأي شخص يقرأ API Documentation.

فعندما نرى:

/api/v1/brands

نعرف مباشرة أننا نتعامل مع الإصدار الأول.

وعندما نرى:

/api/v2/brands

نعرف أننا نتعامل مع الإصدار الثاني.

وهي أيضًا طريقة سهلة التنظيم باستخدام Laravel Route Groups.

تنظيم Versions داخل Laravel

يمكن تنظيم Controllers داخل مجلدات مستقلة لكل Version.

مثلًا:

app/
└── Http/
    └── Controllers/
        └── Api/
            ├── V1/
            │   └── BrandController.php
            │
            └── V2/
                └── BrandController.php

وبهذا نستطيع أن يكون لدينا:

App\Http\Controllers\Api\V1\BrandController

App\Http\Controllers\Api\V2\BrandController

بدون تعارض بين الكلاسات.

إنشاء Controller للإصدار V1

يمكن إنشاء Controller داخل Namespace الخاص بـV1:

php artisan make:controller Api/V1/BrandController --api

سيتم إنشاء:

app/Http/Controllers/Api/V1/BrandController.php

وسيكون Namespace:

namespace App\Http\Controllers\Api\V1;

مثلًا:

<?php

namespace App\Http\Controllers\Api\V1;

use App\Http\Controllers\Controller;
use App\Http\Resources\Api\V1\BrandResource;
use App\Models\Brand;

class BrandController extends Controller
{
    public function index()
    {
        return BrandResource::collection(
            Brand::all()
        );
    }


    public function show(Brand $brand)
    {
        return new BrandResource($brand);
    }
}

إنشاء Routes للإصدار V1

يمكن في أبسط تنظيم استخدام Route Group داخل:

routes/api.php

مثل:

<?php

use App\Http\Controllers\Api\V1\BrandController;
use Illuminate\Support\Facades\Route;

Route::prefix('v1')->group(function () {

    Route::apiResource(
        'brands',
        BrandController::class
    );

});

وبما أن Laravel يضيف Prefix:

/api

إلى API Routes، فإن المسارات النهائية تصبح:

/api/v1/brands

/api/v1/brands/{brand}

مثلًا:

GET /api/v1/brands

إنشاء الإصدار V2

لنفترض أننا نريد الآن تطوير API وإضافة معلومات جديدة إلى Brand Response، لكننا لا نريد كسر التطبيقات التي تستخدم V1.

ننشئ Controller جديدًا:

php artisan make:controller Api/V2/BrandController --api

وسيتم إنشاء:

app/Http/Controllers/Api/V2/BrandController.php

مع Namespace:

namespace App\Http\Controllers\Api\V2;

يمكن لهذا Controller أن يستخدم Implementation مختلفة عن V1.

إنشاء Routes للإصدار V2

داخل routes/api.php:

use App\Http\Controllers\Api\V1\BrandController as V1BrandController;
use App\Http\Controllers\Api\V2\BrandController as V2BrandController;

Route::prefix('v1')->group(function () {

    Route::apiResource(
        'brands',
        V1BrandController::class
    );

});


Route::prefix('v2')->group(function () {

    Route::apiResource(
        'brands',
        V2BrandController::class
    );

});

الآن لدينا:

GET /api/v1/brands

GET /api/v2/brands

ويستطيع كل Version استخدام Controller وResource مختلفين.

الطريقة الأولى: استخدام ملف Routes واحد

إذا كان API صغيرًا أو متوسط الحجم، يمكن الاحتفاظ بجميع Versions داخل:

routes/api.php

باستخدام Groups:

Route::prefix('v1')->group(function () {

    // V1 routes

});


Route::prefix('v2')->group(function () {

    // V2 routes

});


Route::prefix('v3')->group(function () {

    // V3 routes

});

هذه الطريقة بسيطة ومناسبة إذا لم يكن عدد Routes كبيرًا.

الطريقة الثانية: استخدام ملف Routes مستقل لكل Version

إذا كان المشروع كبيرًا، فقد يصبح وضع جميع Versions داخل ملف واحد غير عملي.

يمكن بدل ذلك إنشاء:

routes/
├── api.php
├── api_v1.php
├── api_v2.php
└── api_v3.php

أو تنظيمها داخل مجلد:

routes/
└── api/
    ├── v1.php
    ├── v2.php
    └── v3.php

وهذا التنظيم يكون مفيدًا عندما يحتوي كل Version على عدد كبير من Endpoints.

تسجيل ملفات Routes في bootstrap/app.php

في Laravel الحديث يتم إعداد Routing من:

bootstrap/app.php

وبدل الاعتماد على تعديل RouteServiceProvider بالطريقة المستخدمة في الإصدارات القديمة، يمكن تسجيل Routes إضافية من إعداد Routing.

على سبيل المثال يمكن استخدام Closure إضافية:

<?php

use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Support\Facades\Route;

return Application::configure(
    basePath: dirname(__DIR__)
)
    ->withRouting(
        web: __DIR__.'/../routes/web.php',

        api: __DIR__.'/../routes/api.php',

        commands: __DIR__.'/../routes/console.php',

        health: '/up',

        then: function () {

            Route::middleware('api')
                ->prefix('api/v1')
                ->group(
                    base_path('routes/api/v1.php')
                );

            Route::middleware('api')
                ->prefix('api/v2')
                ->group(
                    base_path('routes/api/v2.php')
                );

        },
    )
    ->withMiddleware(function (Middleware $middleware): void {
        //
    })
    ->withExceptions(function (Exceptions $exceptions): void {
        //
    })
    ->create();

بهذه الطريقة يصبح:

routes/api/v1.php

مسؤولًا عن:

/api/v1/*

و:

routes/api/v2.php

مسؤولًا عن:

/api/v2/*

هذه البنية مناسبة جدًا للأنظمة الكبيرة.

تطبيق Versioning على API Resources

ليس Controller هو الشيء الوحيد الذي قد يختلف بين V1 وV2.

أحد أكثر الأجزاء التي تتغير عند تطوير API هو شكل JSON Response.

لذلك من المنطقي في كثير من المشاريع عمل Versioning للـResources أيضًا.

مثل:

app/
└── Http/
    └── Resources/
        └── Api/
            ├── V1/
            │   └── BrandResource.php
            │
            └── V2/
                └── BrandResource.php

قد يكون V1:

public function toArray(Request $request): array
{
    return [
        'id' => $this->id,
        'name' => $this->name,
    ];
}

بينما V2:

public function toArray(Request $request): array
{
    return [
        'id' => $this->id,
        'name' => $this->name,
        'slug' => $this->slug,
        'products_count' => $this->products_count,
        'created_at' => $this->created_at,
    ];
}

وبذلك يمكن تغيير Response في V2 بدون التأثير على V1.

لا تكرر Business Logic بين Versions

من الأخطاء التي يمكن أن تحدث عند استخدام Versioning نسخ Controller كامل من V1 إلى V2 ثم تكرار جميع العمليات.

مع مرور الوقت قد يصبح لدينا:

V1 BrandController
V2 BrandController
V3 BrandController
V4 BrandController

وكل واحد يحتوي تقريبًا على Business Logic نفسها.

هذا يجعل الصيانة صعبة.

الأفضل أن تكون الأجزاء المشتركة مثل Business Logic داخل طبقة مستقلة عندما يكون ذلك مناسبًا، مثل:

  • Services.
  • Actions.
  • Repositories عند الحاجة.
  • Domain Classes.

ويكون دور Controller أقرب إلى:

Request
   |
   v
Controller
   |
   v
Service / Action
   |
   v
Model
   |
   v
Versioned Resource

بهذه الطريقة يمكن أن يستخدم V1 وV2 نفس Business Logic، بينما يختلف فقط شكل Response أو بعض السلوكيات الخاصة بكل Version.

إنشاء V3

إذا احتجنا في المستقبل إلى إصدار ثالث، تكون العملية نفسها.

إنشاء Controller

php artisan make:controller Api/V3/BrandController --api

سيصبح لدينا:

app/Http/Controllers/Api/V3/BrandController.php

إنشاء Resource

php artisan make:resource Api/V3/BrandResource

إنشاء Routes

إذا كنا نستخدم Route Group:

use App\Http\Controllers\Api\V3\BrandController;

Route::prefix('v3')->group(function () {

    Route::apiResource(
        'brands',
        BrandController::class
    );

});

سيصبح Endpoint:

GET /api/v3/brands

ماذا نفعل بالإصدارات القديمة؟

وجود Versioning لا يعني الاحتفاظ بكل الإصدارات إلى الأبد.

مع مرور الوقت قد يصبح V1 قديمًا جدًا ويصبح من الأفضل إيقافه.

هذه العملية تعرف عادةً باسم:

API Deprecation

ويفضل عدم إيقاف Version مستخدم فجأة.

يمكن اتباع سياسة مثل:

  1. إطلاق V2.
  2. الإبقاء على V1 فعالًا.
  3. إعلام Clients بأن V1 أصبح Deprecated.
  4. تحديد تاريخ واضح لإيقاف V1.
  5. إعطاء المطورين فترة للانتقال إلى V2.
  6. مراقبة استخدام V1.
  7. إيقافه عندما يصبح الانتقال آمنًا.

وهذه النقطة مهمة جدًا إذا كان API يستخدم من تطبيقات خارجية أو تطبيقات هاتف لا تستطيع إجبار جميع المستخدمين على تحديثها فورًا.

أفضل الممارسات عند استخدام API Versioning

لا تنشئ Version جديدًا لكل تغيير صغير

استخدم Version جديدًا للتغييرات التي تؤثر فعليًا على API Contract.

اجعل URLs متناسقة

استخدم:

/api/v1/brands
/api/v1/products
/api/v1/orders

بدل خليط مثل:

/api/v1/brands
/api/products-v1
/api/orders/version1

حافظ على الإصدار القديم مستقرًا

بعد إطلاق V2 لا تقم بتغيير V1 بطريقة تكسر التطبيقات القديمة.

استخدم Resources لعزل Response Format

API Resources مناسبة جدًا لتغيير شكل Response بين الإصدارات المختلفة.

قلل تكرار الكود

لا تنسخ جميع Services وModels وBusiness Logic إلى كل Version بدون سبب.

وثّق كل Version

يجب أن يعرف Client:

  • ما Version الحالي؟
  • ما Versions المدعومة؟
  • ما الذي تغير بين V1 وV2؟
  • متى سيتم إيقاف Version قديم؟

لا تربط Version بإصدار التطبيق نفسه

إذا كان تطبيق الهاتف في الإصدار:

5.7.2

فهذا لا يعني أن API يجب أن يكون:

/api/v5.7.2/

إصدار API يمثل Contract الخاص بالـAPI وليس رقم إصدار تطبيق الهاتف.

هيكل مشروع مقترح

في مشروع كبير يمكن أن يكون التنظيم مثل:

app/
├── Http/
│   │
│   ├── Controllers/
│   │   └── Api/
│   │       ├── V1/
│   │       │   ├── BrandController.php
│   │       │   └── ProductController.php
│   │       │
│   │       └── V2/
│   │           ├── BrandController.php
│   │           └── ProductController.php
│   │
│   └── Resources/
│       └── Api/
│           ├── V1/
│           │   ├── BrandResource.php
│           │   └── ProductResource.php
│           │
│           └── V2/
│               ├── BrandResource.php
│               └── ProductResource.php
│
routes/
└── api/
    ├── v1.php
    └── v2.php

أما Business Logic المشتركة فلا تحتاج بالضرورة إلى Versioning ويمكن وضعها في طبقات مستقلة.

ملخص الدرس

المفهومالوصف
API Versioningإدارة أكثر من إصدار من API مع الحفاظ على التوافق مع Clients القديمة.
V1الإصدار الأول من API.
V2إصدار أحدث يمكن أن يحتوي على تغييرات غير متوافقة مع V1.
Breaking Changeتغيير يؤدي إلى كسر Client الذي يعتمد على Contract سابق.
URL Versioningوضع Version داخل URI مثل /api/v1.
Route::prefix()إضافة Prefix مشترك لمجموعة من Routes.
Route::apiResource()إنشاء RESTful API Routes للـResource.
Versioned Controllersفصل Controllers الخاصة بكل Version.
Versioned Resourcesفصل شكل JSON Response بين Versions.
Deprecationالإعلان عن أن Version قديم سيتم إيقافه مستقبلًا.
bootstrap/app.phpالمكان الحديث لإعداد Routing العام في تطبيق Laravel.

الخلاصة

API Versioning من المفاهيم المهمة عند بناء API يُتوقع أن يستمر ويتطور لفترة طويلة.

بدل تعديل API الحالي بطريقة تؤدي إلى توقف التطبيقات القديمة، يمكن الاحتفاظ بالإصدار الأول:

/api/v1/brands

ثم إنشاء إصدار جديد:

/api/v2/brands

وبهذا يمكن لكل Client استخدام Version المتوافق معه.

تعلمنا كذلك كيفية تنظيم Controllers داخل:

Api/V1
Api/V2
Api/V3

وكيفية استخدام:

Route::prefix('v1')

مع:

Route::apiResource()

لإنشاء Endpoints منظمة لكل Version.

كما تعرفنا على إمكانية فصل Routes في ملفات مستقلة وتسجيلها من خلال إعداد Routing الحديث في:

bootstrap/app.php

والأهم أن Versioning لا يعني نسخ المشروع كاملًا لكل إصدار، بل يجب Versioning فقط للأجزاء التي تحتاج إلى اختلاف، مع إعادة استخدام Business Logic المشتركة قدر الإمكان.

وأخيرًا، يجب أن يكون الانتقال من Version إلى آخر عملية مدروسة تتضمن Documentation واضحة وسياسة Deprecation تسمح للتطبيقات القديمة بالانتقال تدريجيًا دون توقف مفاجئ.