التعامل مع API في Laravel – الجزء الرابع: ما هو API Versioning وكيفية تطبيقه في Laravel
عند بناء API لتطبيق حقيقي، قد نصل بعد فترة إلى مرحلة نحتاج فيها إلى تغيير شكل البيانات أو إضافة خصائص جديدة أو تعديل طريقة عمل بعض Endpoints.
المشكلة أن تطبيقات الهاتف أو أنظمة Frontend القديمة قد تكون ما زالت تعتمد على النسخة الحالية من API، وبالتالي فإن تغيير API مباشرة قد يؤدي إلى توقف هذه التطبيقات عن العمل.
هنا يظهر مفهوم مهم جدًا في تصميم واجهات البرمجة يسمى:
API Versioningفي هذا الجزء من سلسلة Laravel API سنتعرف على مفهوم Versioning، ولماذا نحتاج إليه، وكيفية إنشاء أكثر من إصدار من API داخل Laravel بطريقة منظمة وقابلة للصيانة.
جدول المحتويات
- ما هو API Versioning؟
- لماذا نحتاج إلى Versioning؟
- ما هي Breaking Changes؟
- إضافة Version إلى URL
- متى يجب إنشاء إصدار جديد؟
- متى لا نحتاج إلى إصدار جديد؟
- طرق API Versioning
- URL Versioning
- تنظيم Versions داخل Laravel
- إنشاء Controllers للإصدار V1
- إنشاء Routes للإصدار V1
- إنشاء الإصدار V2
- إنشاء Routes للإصدار V2
- استخدام ملف Routes واحد
- استخدام ملف مستقل لكل Version
- تسجيل ملفات Routes في bootstrap/app.php
- Versioning للـAPI Resources
- مشاركة Business Logic بين الإصدارات
- إنشاء V3
- إيقاف الإصدارات القديمة تدريجيًا
- أفضل الممارسات
- هيكل مشروع مقترح
- ملخص الدرس
- الخلاصة
ما هو 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.
إنشاء 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 مستخدم فجأة.
يمكن اتباع سياسة مثل:
- إطلاق V2.
- الإبقاء على V1 فعالًا.
- إعلام Clients بأن V1 أصبح Deprecated.
- تحديد تاريخ واضح لإيقاف V1.
- إعطاء المطورين فترة للانتقال إلى V2.
- مراقبة استخدام V1.
- إيقافه عندما يصبح الانتقال آمنًا.
وهذه النقطة مهمة جدًا إذا كان 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 تسمح للتطبيقات القديمة بالانتقال تدريجيًا دون توقف مفاجئ.
السلام عليكم يعطيك العافية م. إيثار شروف اولا أتقدم بالشكر الخالص لك لما تقدمه من مقالات مفيدة لمجتمعنا العربي ،والتى لطالما قمت بالرجوع الى تلك المقالات للاستفادة منها . كان هناك طلب لتطوير موقعك \"etharshrouf.com\" ، وهو يتمثل في إضافة تواريخ للمقالات بالاضافة الي فلتر بحث لترتيب المقالات حسب الاحدث والاقدم ،مع وبدون اختيار القسم. خالص تحياتي