مقدمة إلى إصدار الـ API
يعد إصدار واجهة برمجة التطبيقات ضروريًا للحفاظ على التوافق العكسي مع تطور واجهة برمجة التطبيقات الخاصة بك بمرور الوقت. مع نمو تطبيقك، ستحتاج إلى إجراء تغييرات جذرية، أو إضافة ميزات جديدة، أو إيقاف الوظائف القديمة—كل ذلك مع ضمان استمرار عمل العملاء الحاليين. في هذا الدرس الشامل، سنستكشف استراتيجيات الإصدار المختلفة ومقايضاتها وكيفية تنفيذها بفعالية في Laravel.
لماذا يهم إصدار الـ API
يتيح لك الإصدار تطوير واجهة برمجة التطبيقات الخاصة بك دون كسر التكاملات الموجودة:
فوائد إصدار الـ API:
- التوافق العكسي: يستمر العملاء الحاليون في العمل أثناء إضافة ميزات جديدة
- الترحيل التدريجي: يمكن للعملاء الترقية بالسرعة التي تناسبهم
- التغييرات الجذرية: يسمح بإدخال تغييرات من شأنها أن تكسر التكاملات الموجودة
- تطور الميزات: اختبار الميزات الجديدة دون التأثير على عملاء الإنتاج
- مسار إيقاف واضح: يوفر وقتًا لإيقاف الإصدارات القديمة
- دعم عملاء متعددين: يمكن لعملاء مختلفين استخدام إصدارات API مختلفة
متى تصدر إصدارًا لواجهة برمجة التطبيقات الخاصة بك
ليست كل التغييرات تتطلب إصدارًا جديدًا. فهم متى تصدر إصدارًا أمر بالغ الأهمية:
// التغييرات غير الجذرية (لا حاجة لإصدار جديد):
✓ إضافة نقاط نهاية جديدة
✓ إضافة معلمات طلب اختيارية جديدة
✓ إضافة حقول جديدة للاستجابات
✓ إضافة رموز خطأ جديدة
✓ جعل الحقول المطلوبة اختيارية
✓ تخفيف قواعد التحقق
// التغييرات الجذرية (تتطلب إصدارًا جديدًا):
✗ إزالة نقاط النهاية
✗ إزالة معلمات الطلب أو حقول الاستجابة
✗ تغيير أنواع الحقول (سلسلة إلى رقم)
✗ تغيير بنية الاستجابة
✗ إعادة تسمية الحقول
✗ جعل الحقول الاختيارية مطلوبة
✗ تغيير طرق المصادقة
✗ تغيير هياكل URL
نظرة عامة على استراتيجيات الإصدار
هناك أربع استراتيجيات رئيسية لإصدار واجهة برمجة التطبيقات، كل منها لها مزاياها الخاصة:
1. إصدار URI (الأكثر شيوعًا)
- مثال: /api/v1/posts، /api/v2/posts
- الإيجابيات: بسيط، مرئي، سهل الاختبار
- السلبيات: ينتهك مبادئ REST، يزدحم URLs
2. إصدار الرأس
- مثال: Accept: application/vnd.myapi.v2+json
- الإيجابيات: URLs نظيفة، يتبع مبادئ REST
- السلبيات: أقل قابلية للاكتشاف، أصعب في الاختبار في المتصفح
3. إصدار معلمة الاستعلام
- مثال: /api/posts?version=2
- الإيجابيات: بسيط، سهل الاختبار
- السلبيات: يمكن تجاهله، ليس RESTful
4. إصدار النطاق الفرعي
- مثال: v1.api.example.com، v2.api.example.com
- الإيجابيات: فصل واضح، موازنة تحميل سهلة
- السلبيات: تعقيد البنية التحتية، شهادات SSL
التوصية: إصدار URI هو النهج الأكثر شعبية وعملية لمعظم التطبيقات. إنه بسيط وواضح ويعمل بشكل جيد مع الأدوات الموجودة.
تنفيذ إصدار URI
النهج الأكثر وضوحًا: تضمين الإصدار في مسار URL.
إعداد إصدار URI الأساسي
<?php
// routes/api.php
use Illuminate\Support\Facades\Route;
// مسارات الإصدار 1
Route::prefix('v1')->group(function () {
Route::get('/posts', [App\Http\Controllers\Api\V1\PostController::class, 'index']);
Route::get('/posts/{id}', [App\Http\Controllers\Api\V1\PostController::class, 'show']);
Route::post('/posts', [App\Http\Controllers\Api\V1\PostController::class, 'store']);
});
// مسارات الإصدار 2 مع مساحة الاسم
Route::prefix('v2')->group(function () {
Route::get('/posts', [App\Http\Controllers\Api\V2\PostController::class, 'index']);
Route::get('/posts/{id}', [App\Http\Controllers\Api\V2\PostController::class, 'show']);
Route::post('/posts', [App\Http\Controllers\Api\V2\PostController::class, 'store']);
});
// الاستخدام:
// GET /api/v1/posts
// GET /api/v2/posts
</pre>
بنية الكنترولر المنظمة
app/
├── Http/
│ └── Controllers/
│ └── Api/
│ ├── V1/
│ │ ├── PostController.php
│ │ ├── UserController.php
│ │ └── CommentController.php
│ └── V2/
│ ├── PostController.php
│ ├── UserController.php
│ └── CommentController.php
مثال على كنترولر V1
<?php
// app/Http/Controllers/Api/V1/PostController.php
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use App\Http\Resources\V1\PostResource;
use App\Models\Post;
use Illuminate\Http\JsonResponse;
class PostController extends Controller
{
public function index(): JsonResponse
{
$posts = Post::with('author')->paginate(15);
return response()->json([
'data' => PostResource::collection($posts->items()),
'total' => $posts->total(),
'page' => $posts->currentPage(),
]);
}
public function show($id): JsonResponse
{
$post = Post::with('author')->findOrFail($id);
return response()->json(new PostResource($post));
}
}
</pre>
كنترولر V2 مع التغييرات الجذرية
<?php
// app/Http/Controllers/Api/V2/PostController.php
namespace App\Http\Controllers\Api\V2;
use App\Http\Controllers\Controller;
use App\Http\Resources\V2\PostResource;
use App\Models\Post;
use Illuminate\Http\JsonResponse;
class PostController extends Controller
{
public function index(): JsonResponse
{
$posts = Post::with('author', 'tags')->cursorPaginate(15);
// V2 يستخدم بنية استجابة مختلفة
return response()->json([
'success' => true,
'posts' => PostResource::collection($posts->items()),
'pagination' => [
'next_cursor' => $posts->nextCursor()?->encode(),
'prev_cursor' => $posts->previousCursor()?->encode(),
'has_more' => $posts->hasMorePages(),
],
]);
}
public function show(string $id): JsonResponse
{
// V2 يستخدم HashIds بدلاً من IDs رقمية
$post = Post::where('hash_id', $id)
->with('author', 'tags', 'comments')
->firstOrFail();
return response()->json([
'success' => true,
'post' => new PostResource($post),
]);
}
}
</pre>
الموارد الخاصة بالإصدار
<?php
// app/Http/Resources/V1/PostResource.php
namespace App\Http\Resources\V1;
use Illuminate\Http\Resources\Json\JsonResource;
class PostResource extends JsonResource
{
public function toArray($request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'content' => $this->content,
'author' => $this->author->name,
'created_at' => $this->created_at->toIso8601String(),
];
}
}
// app/Http/Resources/V2/PostResource.php
namespace App\Http\Resources\V2;
use Illuminate\Http\Resources\Json\JsonResource;
class PostResource extends JsonResource
{
public function toArray($request): array
{
return [
'hash_id' => $this->hash_id, // تم التغيير من id رقمي
'title' => $this->title,
'content' => $this->content,
'excerpt' => $this->excerpt, // حقل جديد
'author' => [ // تم التغيير من سلسلة إلى كائن
'id' => $this->author->id,
'name' => $this->author->name,
'avatar' => $this->author->avatar_url,
],
'tags' => $this->tags->pluck('name'), // حقل جديد
'published_at' => $this->published_at?->toIso8601String(), // تمت إعادة تسميته
];
}
}
</pre>
الإصدار القائم على الرأس
استخدام رؤوس مخصصة أو رأس Accept لتحديد الإصدار:
<?php
// app/Http/Middleware/ApiVersion.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class ApiVersion
{
public function handle(Request $request, Closure $next)
{
// التحقق من الرأس المخصص
$version = $request->header('X-API-Version', 'v1');
// أو تحليل رأس Accept
// Accept: application/vnd.myapi.v2+json
if ($request->header('Accept')) {
preg_match('/v(\d+)/', $request->header('Accept'), $matches);
$version = isset($matches[1]) ? "v{$matches[1]}" : 'v1';
}
// تخزين الإصدار في الطلب
$request->merge(['api_version' => $version]);
return $next($request);
}
}
// تسجيل الميدل وير
// app/Http/Kernel.php
protected $middlewareGroups = [
'api' => [
\App\Http\Middleware\ApiVersion::class,
// ... ميدل وير أخرى
],
];
// التوجيه الديناميكي للكنترولر بناءً على الإصدار
Route::get('/posts', function (Request $request) {
$version = $request->input('api_version', 'v1');
$controller = "App\\Http\\Controllers\\Api\\{$version}\\PostController";
return app($controller)->index($request);
});
// استخدام العميل:
// GET /api/posts
// الرؤوس: X-API-Version: v2
// أو: Accept: application/vnd.myapi.v2+json
</pre>
إصدار معلمة الاستعلام
النهج الأبسط ولكن أقل توصية:
<?php
// routes/api.php
Route::get('/posts', function (Request $request) {
$version = $request->query('version', 'v1');
$controller = match ($version) {
'v2' => App\Http\Controllers\Api\V2\PostController::class,
default => App\Http\Controllers\Api\V1\PostController::class,
};
return app($controller)->index($request);
});
// الاستخدام:
// GET /api/posts?version=v1
// GET /api/posts?version=v2
</pre>
الكود المشترك بين الإصدارات
تجنب ازدواجية الكود من خلال مشاركة المنطق المشترك:
<?php
// app/Http/Controllers/Api/BasePostController.php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Post;
abstract class BasePostController extends Controller
{
protected function getPost($id): Post
{
return Post::with($this->getRelations())->findOrFail($id);
}
protected function getPosts()
{
return Post::with($this->getRelations())
->when(request('status'), fn($q, $status) => $q->where('status', $status))
->latest();
}
abstract protected function getRelations(): array;
}
// كنترولر V1
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Api\BasePostController;
class PostController extends BasePostController
{
protected function getRelations(): array
{
return ['author'];
}
public function index()
{
$posts = $this->getPosts()->paginate(15);
return response()->json($posts);
}
}
// كنترولر V2
namespace App\Http\Controllers\Api\V2;
use App\Http\Controllers\Api\BasePostController;
class PostController extends BasePostController
{
protected function getRelations(): array
{
return ['author', 'tags', 'comments']; // المزيد من العلاقات في V2
}
public function index()
{
$posts = $this->getPosts()->cursorPaginate(15); // ترقيم مختلف
return response()->json([
'success' => true,
'data' => $posts,
]);
}
}
</pre>
التفاوض على الإصدار
توجيه الطلبات تلقائيًا إلى الإصدار الصحيح:
<?php
// app/Http/Middleware/ApiVersionNegotiation.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class ApiVersionNegotiation
{
protected array $supportedVersions = ['v1', 'v2'];
protected string $defaultVersion = 'v1';
protected string $latestVersion = 'v2';
public function handle(Request $request, Closure $next)
{
$version = $this->negotiateVersion($request);
// تعيين الإصدار عالميًا
config(['api.version' => $version]);
$request->merge(['api_version' => $version]);
return $next($request);
}
protected function negotiateVersion(Request $request): string
{
// التحقق من URI أولاً
if (preg_match('/\/(v\d+)\//', $request->path(), $matches)) {
return $this->validateVersion($matches[1]);
}
// التحقق من الرأس
if ($header = $request->header('X-API-Version')) {
return $this->validateVersion($header);
}
// التحقق من معلمة الاستعلام
if ($query = $request->query('version')) {
return $this->validateVersion($query);
}
// الإصدار الافتراضي
return $this->defaultVersion;
}
protected function validateVersion(string $version): string
{
$version = strtolower($version);
if (!in_array($version, $this->supportedVersions)) {
abort(400, "إصدار API غير مدعوم: {$version}. الإصدارات المدعومة: " . implode(', ', $this->supportedVersions));
}
return $version;
}
}
</pre>
استراتيجية الإيقاف
التواصل بشكل صحيح حول إيقاف الإصدار للعملاء:
<?php
// app/Http/Middleware/ApiDeprecation.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
class ApiDeprecation
{
protected array $deprecatedVersions = [
'v1' => [
'deprecated_at' => '2024-01-01',
'sunset_at' => '2024-06-01',
'message' => 'API v1 مهمل وسيتم إزالته في 2024-06-01. يرجى الترحيل إلى v2.',
],
];
public function handle(Request $request, Closure $next)
{
$version = $request->input('api_version', config('api.version'));
if (isset($this->deprecatedVersions[$version])) {
$info = $this->deprecatedVersions[$version];
// تسجيل استخدام الإيقاف
Log::warning("تم استخدام إصدار API مهمل: {$version}", [
'user_id' => auth()->id(),
'endpoint' => $request->path(),
'ip' => $request->ip(),
]);
// إضافة رؤوس الإيقاف
$response = $next($request);
return $response->withHeaders([
'X-API-Deprecated' => 'true',
'X-API-Sunset-Date' => $info['sunset_at'],
'X-API-Deprecation-Info' => $info['message'],
'Warning' => '299 - "' . $info['message'] . '"'
]);
}
return $next($request);
}
}
</pre>
اختبار واجهات برمجة التطبيقات المصنفة
اختبارات شاملة لجميع إصدارات API:
<?php
// tests/Feature/Api/V1/PostTest.php
namespace Tests\Feature\Api\V1;
use App\Models\Post;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class PostTest extends TestCase
{
use RefreshDatabase;
public function test_v1_returns_correct_response_structure()
{
Post::factory()->count(3)->create();
$response = $this->getJson('/api/v1/posts');
$response->assertOk()
->assertJsonStructure([
'data' => [
'*' => ['id', 'title', 'content', 'author', 'created_at']
],
'total',
'page',
]);
// تأكد من استخدام IDs رقمية في V1
$this->assertIsInt($response->json('data.0.id'));
}
public function test_v1_deprecation_headers()
{
$response = $this->getJson('/api/v1/posts');
$response->assertHeader('X-API-Deprecated', 'true')
->assertHeader('X-API-Sunset-Date');
}
}
// tests/Feature/Api/V2/PostTest.php
namespace Tests\Feature\Api\V2;
use App\Models\Post;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class PostTest extends TestCase
{
use RefreshDatabase;
public function test_v2_returns_correct_response_structure()
{
Post::factory()->count(3)->create();
$response = $this->getJson('/api/v2/posts');
$response->assertOk()
->assertJsonStructure([
'success',
'posts' => [
'*' => [
'hash_id',
'title',
'content',
'excerpt',
'author' => ['id', 'name', 'avatar'],
'tags',
'published_at'
]
],
'pagination',
]);
// تأكد من استخدام hash IDs في V2
$this->assertIsString($response->json('posts.0.hash_id'));
}
public function test_v2_no_deprecation_headers()
{
$response = $this->getJson('/api/v2/posts');
$response->assertHeaderMissing('X-API-Deprecated');
}
}
</pre>
وثائق لإصدارات متعددة
الحفاظ على وثائق منفصلة لكل إصدار:
docs/
├── api/
│ ├── v1/
│ │ ├── endpoints.md
│ │ ├── authentication.md
│ │ └── examples.md
│ └── v2/
│ ├── endpoints.md
│ ├── authentication.md
│ ├── examples.md
│ └── migration-guide.md # كيفية الترحيل من v1 إلى v2
// إضافة محدد الإصدار إلى وثائق API
// public/docs/index.html
<select id="version-selector">
<option value="v2">v2 (الأحدث)</option>
<option value="v1">v1 (مهمل)</option>
</select>
تمرين عملي:
- نفذ الإصدار القائم على URI لـ User API مع V1 باستخدام IDs رقمية و V2 باستخدام UUID/HashIds
- أنشئ ميدل وير تكتشف إصدار API من الرؤوس وتوجه ديناميكيًا إلى الكنترولر الصحيح
- قم ببناء نظام إيقاف يسجل استخدام الإصدار المهمل ويرسل رؤوس تحذير للعملاء
- نفذ موارد API خاصة بالإصدار تحول نفس بيانات النموذج بشكل مختلف لـ V1 و V2
- اكتب اختبارات شاملة تتحقق من بنية الاستجابة وأنواع الحقول ورؤوس الإيقاف لكلا الإصدارين
أفضل الممارسات
أفضل ممارسات إصدار الـ API:
- الإصدار فقط عند الضرورة: ليست كل التغييرات تتطلب إصدارًا جديدًا—تجنب التغييرات الجذرية عندما يكون ذلك ممكنًا
- دعم ما لا يقل عن إصدارين: الإصدار الحالي والسابق للترحيل التدريجي
- قم بالتواصل حول الإيقاف: أعط العملاء إشعارًا لمدة 6-12 شهرًا قبل إيقاف الإصدارات القديمة
- وثق بدقة: قدم أدلة الترحيل بين الإصدارات
- استخدم الإصدار الدلالي: v1، v2، v3 (وليس v1.2.3 لواجهات REST API)
- الافتراضي إلى الأحدث المستقر: إذا لم يتم تحديد إصدار، استخدم الأحدث المستقر (وليس الأحدث)
- راقب الاستخدام: تتبع الإصدارات المستخدمة لتخطيط الإيقاف
- اختبر جميع الإصدارات: حافظ على اختبارات لجميع الإصدارات المدعومة
الملخص
في هذا الدرس، أتقنت استراتيجيات إصدار API في Laravel. أنت الآن تفهم متى يكون الإصدار ضروريًا، والاختلافات بين URI ورأس ومعلمة الاستعلام للإصدار، وكيفية تنفيذ الكنترولرات والموارد الخاصة بالإصدار، وتقنيات مشاركة الكود بين الإصدارات، واستراتيجيات الإيقاف مع التواصل الصحيح مع العملاء، وأساليب الاختبار لإصدارات API متعددة. يعد الإصدار المناسب ضروريًا للحفاظ على واجهات برمجة تطبيقات احترافية يمكن أن تتطور بمرور الوقت دون كسر التكاملات الموجودة، مما يسمح لك بتحسين واجهة برمجة التطبيقات الخاصة بك مع احترام جداول عملائك الزمنية.