تطوير واجهات برمجة التطبيقات المتقدمة
تطوير واجهات برمجة التطبيقات المتقدمة
بناء واجهات برمجة تطبيقات جاهزة للإنتاج يتطلب تقنيات متقدمة تتجاوز عمليات CRUD الأساسية. في هذا الدرس، سنستكشف إصدارات API، مبادئ HATEOAS، التفاوض على المحتوى، استراتيجيات تقييد المعدل المتطورة، وممارسات توثيق API الشاملة التي تجعل واجهات برمجة التطبيقات الخاصة بك قابلة للصيانة وسهلة الاستخدام للمطورين.
استراتيجيات إصدارات API
إصدارات API تسمح لك بإدخال تغييرات كاسرة دون تعطيل العملاء الحاليين. يوفر Laravel آليات توجيه مرنة لتنفيذ استراتيجيات إصدار مختلفة.
<?php
// routes/api.php - إصدار URI (الأكثر شيوعاً)
Route::prefix('v1')->group(function () {
Route::apiResource('users', App\Http\Controllers\Api\V1\UserController::class);
Route::apiResource('posts', App\Http\Controllers\Api\V1\PostController::class);
});
Route::prefix('v2')->group(function () {
Route::apiResource('users', App\Http\Controllers\Api\V2\UserController::class);
Route::apiResource('posts', App\Http\Controllers\Api\V2\PostController::class);
});
// إصدار الرأس (Accept header)
// routes/api.php
Route::middleware(['api.version:v1'])->group(function () {
Route::apiResource('users', App\Http\Controllers\Api\V1\UserController::class);
});
Route::middleware(['api.version:v2'])->group(function () {
Route::apiResource('users', App\Http\Controllers\Api\V2\UserController::class);
});
// app/Http/Middleware/ApiVersion.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class ApiVersion
{
public function handle(Request $request, Closure $next, string $version)
{
$acceptHeader = $request->header('Accept', '');
if (str_contains($acceptHeader, "version={$version}")) {
return $next($request);
}
return response()->json([
'message' => "API version {$version} required in Accept header",
'example' => "Accept: application/json;version={$version}"
], 406);
}
}
// إصدار معامل الاستعلام
// /api/users?version=v1
Route::middleware(['api.version.query'])->group(function () {
Route::apiResource('users', function (Request $request) {
$version = $request->query('version', 'v1');
$controller = "App\Http\Controllers\Api\{$version}\UserController";
return app($controller)->index($request);
});
});
HATEOAS (الوسائط الفائقة كمحرك لحالة التطبيق)
HATEOAS هو قيد REST يجعل واجهات برمجة التطبيقات ذاتية التوثيق من خلال تضمين روابط الوسائط الفائقة في الاستجابات، مما يسمح للعملاء باكتشاف الإجراءات المتاحة ديناميكياً.
<?php
// app/Http/Resources/UserResource.php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class UserResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'created_at' => $this->created_at->toIso8601String(),
'links' => [
'self' => [
'href' => route('api.users.show', $this->id),
'method' => 'GET',
],
'update' => [
'href' => route('api.users.update', $this->id),
'method' => 'PUT',
],
'delete' => [
'href' => route('api.users.destroy', $this->id),
'method' => 'DELETE',
],
'posts' => [
'href' => route('api.users.posts.index', $this->id),
'method' => 'GET',
],
],
'_embedded' => [
'latest_post' => new PostResource($this->whenLoaded('latestPost')),
],
];
}
}
// app/Http/Resources/PostCollection.php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\ResourceCollection;
class PostCollection extends ResourceCollection
{
public function toArray($request)
{
return [
'data' => $this->collection,
'links' => [
'self' => [
'href' => $request->fullUrl(),
],
'first' => [
'href' => $this->url(1),
],
'last' => [
'href' => $this->url($this->lastPage()),
],
'prev' => [
'href' => $this->previousPageUrl(),
],
'next' => [
'href' => $this->nextPageUrl(),
],
],
'meta' => [
'current_page' => $this->currentPage(),
'last_page' => $this->lastPage(),
'per_page' => $this->perPage(),
'total' => $this->total(),
],
];
}
}
// الاستخدام في Controller
public function show(User $user)
{
$user->load('latestPost');
return new UserResource($user);
}
// مثال على استجابة JSON:
{
"id": 1,
"name": "أحمد محمد",
"email": "ahmad@example.com",
"created_at": "2024-01-15T10:30:00Z",
"links": {
"self": {"href": "https://api.example.com/v1/users/1", "method": "GET"},
"update": {"href": "https://api.example.com/v1/users/1", "method": "PUT"},
"delete": {"href": "https://api.example.com/v1/users/1", "method": "DELETE"},
"posts": {"href": "https://api.example.com/v1/users/1/posts", "method": "GET"}
},
"_embedded": {
"latest_post": {
"id": 42,
"title": "مقالتي الأخيرة",
"links": {...}
}
}
}
التفاوض على المحتوى
التفاوض على المحتوى يسمح لـ API الخاص بك بتقديم تنسيقات استجابة مختلفة (JSON، XML، CSV) بناءً على تفضيلات العميل المحددة في رأس Accept.
<?php
// app/Http/Middleware/ContentNegotiation.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Response;
class ContentNegotiation
{
public function handle(Request $request, Closure $next)
{
$response = $next($request);
$acceptHeader = $request->header('Accept', 'application/json');
// تحليل رأس Accept
if (str_contains($acceptHeader, 'application/xml')) {
return $this->convertToXml($response);
} elseif (str_contains($acceptHeader, 'text/csv')) {
return $this->convertToCsv($response);
}
// الافتراضي إلى JSON
return $response;
}
protected function convertToXml($response)
{
$data = $response->getData(true);
$xml = new \SimpleXMLElement('<?xml version="1.0"?><root></root>');
$this->arrayToXml($data, $xml);
return Response::make($xml->asXML(), $response->status())
->header('Content-Type', 'application/xml');
}
protected function arrayToXml($data, &$xml)
{
foreach ($data as $key => $value) {
if (is_array($value)) {
$subNode = $xml->addChild($key);
$this->arrayToXml($value, $subNode);
} else {
$xml->addChild($key, htmlspecialchars($value));
}
}
}
protected function convertToCsv($response)
{
$data = $response->getData(true);
// تسطيح البيانات إذا لزم الأمر
if (isset($data['data']) && is_array($data['data'])) {
$data = $data['data'];
}
$csv = fopen('php://temp', 'r+');
// كتابة الرؤوس
if (!empty($data)) {
fputcsv($csv, array_keys(reset($data)));
// كتابة الصفوف
foreach ($data as $row) {
fputcsv($csv, $row);
}
}
rewind($csv);
$output = stream_get_contents($csv);
fclose($csv);
return Response::make($output, $response->status())
->header('Content-Type', 'text/csv')
->header('Content-Disposition', 'attachment; filename="export.csv"');
}
}
// تسجيل middleware في app/Http/Kernel.php
protected $middlewareGroups = [
'api' => [
\App\Http\Middleware\ContentNegotiation::class,
// ... middleware أخرى
],
];
استراتيجيات تقييد المعدل المتقدمة
يمكن تخصيص middleware throttle في Laravel لتقييد معدل متطور بناءً على مستويات المستخدمين وحساسية نقاط النهاية والحدود الديناميكية.
<?php
// app/Providers/RouteServiceProvider.php
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
public function boot()
{
// تقييد المعدل بناءً على المستوى
RateLimiter::for('api', function (Request $request) {
$user = $request->user();
if (!$user) {
// المستخدمون المجهولون: 60 طلباً في الدقيقة
return Limit::perMinute(60)->by($request->ip());
}
// تقييد المعدل بناءً على مستوى المستخدم
return match($user->tier) {
'free' => Limit::perMinute(100)->by($user->id),
'pro' => Limit::perMinute(1000)->by($user->id),
'enterprise' => Limit::perMinute(10000)->by($user->id),
default => Limit::perMinute(100)->by($user->id),
};
});
// تقييد المعدل الخاص بنقطة النهاية
RateLimiter::for('expensive-operations', function (Request $request) {
return Limit::perMinute(10)->by($request->user()?->id ?: $request->ip())
->response(function (Request $request, array $headers) {
return response()->json([
'message' => 'عمليات مكلفة كثيرة جداً. يرجى المحاولة مرة أخرى لاحقاً.',
'retry_after' => $headers['Retry-After'],
], 429, $headers);
});
});
// تقييد المعدل الديناميكي مع حدود متعددة
RateLimiter::for('flexible', function (Request $request) {
return [
Limit::perMinute(60)->by($request->ip()),
Limit::perDay(1000)->by($request->user()?->id ?: $request->ip()),
];
});
// تقييد المعدل المستند إلى الوقت (ساعات العمل مقابل خارج الساعات)
RateLimiter::for('time-based', function (Request $request) {
$hour = now()->hour;
$isBusinessHours = $hour >= 9 && $hour < 17;
$limit = $isBusinessHours ? 100 : 500; // أكثر سخاءً خارج الساعات
return Limit::perMinute($limit)->by($request->user()?->id ?: $request->ip());
});
}
// routes/api.php - تطبيق تقييد المعدل
Route::middleware(['throttle:api'])->group(function () {
Route::get('/users', [UserController::class, 'index']);
Route::get('/posts', [PostController::class, 'index']);
});
Route::middleware(['throttle:expensive-operations'])->group(function () {
Route::post('/reports/generate', [ReportController::class, 'generate']);
Route::post('/exports/large', [ExportController::class, 'exportLarge']);
});
// middleware throttle مخصص مع Redis
// app/Http/Middleware/ApiThrottle.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Redis;
class ApiThrottle
{
public function handle(Request $request, Closure $next, int $maxAttempts = 60)
{
$key = $this->resolveRequestSignature($request);
$attempts = Redis::incr($key);
if ($attempts === 1) {
Redis::expire($key, 60); // نافذة دقيقة واحدة
}
if ($attempts > $maxAttempts) {
$retryAfter = Redis::ttl($key);
return response()->json([
'message' => 'تجاوز حد المعدل.',
'retry_after' => $retryAfter,
], 429, [
'X-RateLimit-Limit' => $maxAttempts,
'X-RateLimit-Remaining' => 0,
'Retry-After' => $retryAfter,
]);
}
$response = $next($request);
return $response->withHeaders([
'X-RateLimit-Limit' => $maxAttempts,
'X-RateLimit-Remaining' => max(0, $maxAttempts - $attempts),
]);
}
protected function resolveRequestSignature(Request $request): string
{
$user = $request->user();
$identifier = $user ? "user:{$user->id}" : "ip:{$request->ip()}";
return "throttle:{$identifier}:{$request->path()}";
}
}
توثيق API مع OpenAPI/Swagger
التوثيق الشامل لـ API ضروري لتجربة المطورين. يتكامل Laravel بسلاسة مع مواصفات OpenAPI باستخدام حزم مثل L5-Swagger.
# تثبيت L5-Swagger
composer require darkaonline/l5-swagger
# نشر التكوين
php artisan vendor:publish --provider "L5Swagger\L5SwaggerServiceProvider"
# توليد التوثيق
php artisan l5-swagger:generate
<?php
// app/Http/Controllers/Api/V1/UserController.php
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use App\Http\Resources\UserResource;
use App\Models\User;
use Illuminate\Http\Request;
/**
* @OA\Info(
* title="واجهة برمجة تطبيقات تطبيقي",
* version="1.0.0",
* description="واجهة برمجة تطبيقات RESTful لتطبيقي",
* @OA\Contact(
* email="api@example.com"
* )
* )
* @OA\Server(
* url="https://api.example.com/v1",
* description="خادم API الإنتاجي"
* )
* @OA\SecurityScheme(
* securityScheme="bearerAuth",
* type="http",
* scheme="bearer",
* bearerFormat="JWT"
* )
*/
class UserController extends Controller
{
/**
* @OA\Get(
* path="/users",
* summary="الحصول على قائمة المستخدمين",
* tags={"Users"},
* security={{"bearerAuth":{}}},
* @OA\Parameter(
* name="page",
* in="query",
* description="رقم الصفحة",
* required=false,
* @OA\Schema(type="integer", example=1)
* ),
* @OA\Parameter(
* name="per_page",
* in="query",
* description="العناصر لكل صفحة",
* required=false,
* @OA\Schema(type="integer", example=15)
* ),
* @OA\Response(
* response=200,
* description="عملية ناجحة",
* @OA\JsonContent(
* @OA\Property(property="data", type="array",
* @OA\Items(ref="#/components/schemas/User")
* ),
* @OA\Property(property="links", type="object"),
* @OA\Property(property="meta", type="object")
* )
* ),
* @OA\Response(
* response=401,
* description="غير مصادق عليه"
* ),
* @OA\Response(
* response=429,
* description="طلبات كثيرة جداً"
* )
* )
*/
public function index(Request $request)
{
$users = User::paginate($request->input('per_page', 15));
return UserResource::collection($users);
}
/**
* @OA\Post(
* path="/users",
* summary="إنشاء مستخدم جديد",
* tags={"Users"},
* security={{"bearerAuth":{}}},
* @OA\RequestBody(
* required=true,
* @OA\JsonContent(
* required={"name","email","password"},
* @OA\Property(property="name", type="string", example="أحمد محمد"),
* @OA\Property(property="email", type="string", format="email", example="ahmad@example.com"),
* @OA\Property(property="password", type="string", format="password", example="secret123")
* )
* ),
* @OA\Response(
* response=201,
* description="تم إنشاء المستخدم بنجاح",
* @OA\JsonContent(ref="#/components/schemas/User")
* ),
* @OA\Response(
* response=422,
* description="خطأ في التحقق من الصحة"
* )
* )
*/
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users',
'password' => 'required|min:8',
]);
$user = User::create($validated);
return new UserResource($user);
}
}
/**
* @OA\Schema(
* schema="User",
* type="object",
* title="User",
* required={"id", "name", "email"},
* @OA\Property(property="id", type="integer", example=1),
* @OA\Property(property="name", type="string", example="أحمد محمد"),
* @OA\Property(property="email", type="string", format="email", example="ahmad@example.com"),
* @OA\Property(property="created_at", type="string", format="date-time"),
* @OA\Property(property="links", type="object",
* @OA\Property(property="self", type="object",
* @OA\Property(property="href", type="string"),
* @OA\Property(property="method", type="string")
* )
* )
* )
*/
تمرين 1: تنفيذ إصدارات API
أنشئ API بإصدارات مع نسختين:
- الإصدار 1: يُرجع بيانات المستخدم مع الحقول الأساسية (id، name، email)
- الإصدار 2: يُرجع بيانات المستخدم مع حقول إضافية (profile_photo، bio، social_links)
- نفذ إصدار يعتمد على URI (/api/v1/users و /api/v2/users)
- أضف تحذيرات الإهمال المناسبة في استجابات v1
- اختبر أن كلا الإصدارين يُرجعان هياكل بيانات مختلفة
تمرين 2: بناء موارد متوافقة مع HATEOAS
حوّل موارد API الحالية لتكون متوافقة مع HATEOAS:
- أنشئ PostResource يتضمن روابط الوسائط الفائقة (self، update، delete، author، comments)
- أضف روابط مشروطة بناءً على أذونات المستخدم (اعرض رابط الحذف فقط إذا كان المستخدم يمكنه الحذف)
- نفذ CommentCollection مع روابط الترقيم
- أضف موارد مضمنة للبيانات ذات الصلة
- اختبر أن العملاء يمكنهم التنقل في API الخاص بك باستخدام الروابط المقدمة فقط
تمرين 3: تقييد معدل متقدم
نفذ نظام تقييد معدل متعدد المستويات:
- أنشئ ثلاثة مستويات مستخدمين: free (100 req/min)، pro (1000 req/min)، enterprise (غير محدود)
- نفذ حدود خاصة بنقطة النهاية: عمليات مكلفة (10 req/min)، عمليات قياسية (على أساس المستوى)
- أضف رؤوس حد المعدل (X-RateLimit-Limit، X-RateLimit-Remaining، Retry-After) إلى جميع الاستجابات
- أنشئ محدد معدل مخصص يتتبع الحصص اليومية بالإضافة إلى الحدود في الدقيقة
- اختبر تقييد المعدل عبر طلبات متعددة وتحقق من استجابات 429 المناسبة
الخلاصة
في هذا الدرس، أتقنت تقنيات تطوير API المتقدمة بما في ذلك استراتيجيات الإصدار للتوافق مع الإصدارات السابقة، مبادئ HATEOAS لواجهات برمجة التطبيقات ذاتية التوثيق، التفاوض على المحتوى لتنسيقات استجابة مرنة، تقييد معدل متطور لحماية الموارد، وتوثيق OpenAPI الشامل لتجربة مطور ممتازة. هذه الأنماط ضرورية لبناء واجهات برمجة تطبيقات على مستوى المؤسسة التي تتوسع وتتطور بسلاسة.