تصميم REST API باستخدام Laravel
يوفر Laravel أدوات شاملة لبناء واجهات برمجية قوية وآمنة وقابلة للتطوير تتبع معايير REST. في هذا الدرس، سنستكشف اصطلاحات تصميم API، والمصادقة، والإصدارات، وتحديد المعدل، وأفضل الممارسات لإنشاء واجهات برمجية جاهزة للإنتاج.
اصطلاحات RESTful
يتبع REST (نقل الحالة التمثيلية) اصطلاحات محددة لتصميم API. يجعل Laravel من السهل تطبيق هذه المعايير:
<!-- طرق HTTP وأغراضها -->
GET /api/users # عرض جميع المستخدمين (index)
GET /api/users/{id} # عرض مستخدم واحد (show)
POST /api/users # إنشاء مستخدم جديد (store)
PUT /api/users/{id} # تحديث مستخدم كامل (update)
PATCH /api/users/{id} # تحديث جزئي للمستخدم (update)
DELETE /api/users/{id} # حذف مستخدم (destroy)
<!-- الموارد المتداخلة -->
GET /api/users/{id}/posts # الحصول على منشورات المستخدم
POST /api/users/{id}/posts # إنشاء منشور للمستخدم
DELETE /api/posts/{id} # حذف منشور محدد
أفضل ممارسات REST:
- استخدم الأسماء للموارد (وليس الأفعال)
- استخدم الأسماء بصيغة الجمع للمجموعات (/users وليس /user)
- استخدم طرق HTTP لتحديد الإجراءات
- أرجع رموز حالة HTTP المناسبة
- قم بإصدار API الخاص بك من البداية
إعداد مسارات API
يوفر Laravel ملف routes/api.php مخصص لمسارات API. تحتوي هذه المسارات تلقائياً على بادئة /api وهي بدون حالة:
// routes/api.php
use App\Http\Controllers\Api\UserController;
use App\Http\Controllers\Api\PostController;
use Illuminate\Support\Facades\Route;
// مسارات API العامة
Route::get('/status', function () {
return response()->json([
'status' => 'online',
'version' => '1.0.0',
'timestamp' => now()->toIso8601String()
]);
});
// مسارات API المحمية
Route::middleware(['auth:sanctum'])->group(function () {
// موارد المستخدم
Route::apiResource('users', UserController::class);
// الموارد المتداخلة
Route::apiResource('users.posts', PostController::class)
->shallow(); // استخدام التوجيه الضحل للموارد المتداخلة
// نقاط النهاية المخصصة
Route::post('/users/{user}/follow', [UserController::class, 'follow']);
Route::delete('/users/{user}/unfollow', [UserController::class, 'unfollow']);
});
apiResource مقابل resource: تنشئ طريقة apiResource() مسارات بدون طرق create و edit، حيث لا تقدم واجهات API عادة نماذج HTML. تتضمن فقط: index, store, show, update, destroy.
متحكمات API
أنشئ متحكمات خاصة بـ API مع تنسيق استجابة مناسب ومعالجة أخطاء:
// app/Http/Controllers/Api/UserController.php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Resources\UserResource;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
class UserController extends Controller
{
/**
* عرض قائمة المستخدمين.
*/
public function index(Request $request)
{
$users = User::query()
->when($request->search, function ($query, $search) {
$query->where('name', 'like', "%{$search}%");
})
->paginate($request->per_page ?? 15);
return UserResource::collection($users);
}
/**
* تخزين مستخدم جديد.
*/
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users',
'password' => 'required|min:8|confirmed'
]);
$validated['password'] = bcrypt($validated['password']);
$user = User::create($validated);
return new UserResource($user);
}
/**
* عرض مستخدم محدد.
*/
public function show(User $user)
{
return new UserResource($user->load(['posts', 'followers']));
}
/**
* تحديث مستخدم محدد.
*/
public function update(Request $request, User $user)
{
$this->authorize('update', $user);
$validated = $request->validate([
'name' => 'sometimes|string|max:255',
'email' => 'sometimes|email|unique:users,email,' . $user->id,
'bio' => 'nullable|string|max:1000'
]);
$user->update($validated);
return new UserResource($user);
}
/**
* حذف مستخدم محدد.
*/
public function destroy(User $user)
{
$this->authorize('delete', $user);
$user->delete();
return response()->json([
'message' => 'تم حذف المستخدم بنجاح'
], Response::HTTP_OK);
}
}
موارد API
توفر موارد API طبقة تحويل بين نماذج Eloquent واستجابات JSON:
// إنشاء مورد: php artisan make:resource UserResource
// app/Http/Resources/UserResource.php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class UserResource extends JsonResource
{
/**
* تحويل المورد إلى مصفوفة.
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'bio' => $this->bio,
'avatar_url' => $this->avatar_url,
'created_at' => $this->created_at->toIso8601String(),
'updated_at' => $this->updated_at->toIso8601String(),
// السمات الشرطية
'email_verified' => $this->when($this->email_verified_at, true),
'phone' => $this->when($request->user()?->isAdmin(), $this->phone),
// العلاقات
'posts' => PostResource::collection($this->whenLoaded('posts')),
'followers_count' => $this->when($this->relationLoaded('followers'),
fn() => $this->followers->count()
),
// السمات المحسوبة
'is_following' => $this->when(
$request->user(),
fn() => $request->user()->isFollowing($this->resource)
),
// الروابط
'links' => [
'self' => route('api.users.show', $this->id),
'posts' => route('api.users.posts.index', $this->id)
]
];
}
/**
* الحصول على بيانات إضافية لاستجابة المورد.
*/
public function with(Request $request): array
{
return [
'meta' => [
'version' => '1.0.0',
'timestamp' => now()->toIso8601String()
]
];
}
}
إصدارات API
نفذ إصدارات API للسماح بالتوافق العكسي والانتقالات السلسة:
// الطريقة 1: إصدار مسار URL (موصى به)
// routes/api.php
Route::prefix('v1')->group(function () {
Route::apiResource('users', Api\V1\UserController::class);
Route::apiResource('posts', Api\V1\PostController::class);
});
Route::prefix('v2')->group(function () {
Route::apiResource('users', Api\V2\UserController::class);
Route::apiResource('posts', Api\V2\PostController::class);
});
// URLs: /api/v1/users, /api/v2/users
// الطريقة 2: إصدار الرأس
// 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)
{
$acceptedVersion = $request->header('Accept-Version', '1.0');
if ($acceptedVersion !== $version) {
return response()->json([
'error' => 'عدم تطابق إصدار API',
'requested' => $acceptedVersion,
'supported' => ['1.0', '2.0']
], 406);
}
return $next($request);
}
}
// تسجيل البرنامج الوسيط
Route::middleware(['api.version:2.0'])->group(function () {
// مسارات V2
});
// الطريقة 3: التفاوض على المحتوى
// Accept: application/vnd.myapp.v2+json
أفضل ممارسات الإصدار:
- استخدم إصدار مسار URL للبساطة والوضوح
- قم بزيادة الإصدارات فقط للتغييرات الكبيرة
- حافظ على الإصدارات القديمة لفترة إهمال
- وثق الاختلافات بين الإصدارات بوضوح
- استخدم الإصدار الدلالي (major.minor.patch)
تحديد المعدل
احمِ API الخاص بك من الإساءة باستخدام تحديد المعدل المدمج في Laravel:
// app/Providers/RouteServiceProvider.php
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
public function boot(): void
{
// تكوين محددات المعدل
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
// حدود المستخدم المصادق عليه
RateLimiter::for('api-authenticated', function (Request $request) {
return $request->user()
? Limit::perMinute(120)->by($request->user()->id)
: Limit::perMinute(20)->by($request->ip());
});
// حدود المستوى المميز
RateLimiter::for('api-premium', function (Request $request) {
if ($request->user()?->isPremium()) {
return Limit::perMinute(1000)->by($request->user()->id);
}
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
// حدود متعددة
RateLimiter::for('api-strict', function (Request $request) {
return [
Limit::perMinute(10), // 10 في الدقيقة
Limit::perHour(100), // 100 في الساعة
Limit::perDay(1000), // 1000 في اليوم
];
});
// استجابة مخصصة عند تجاوز الحد
RateLimiter::for('api-custom', function (Request $request) {
return Limit::perMinute(60)
->by($request->user()?->id ?: $request->ip())
->response(function (Request $request, array $headers) {
return response()->json([
'error' => 'تم تجاوز حد المعدل',
'message' => 'طلبات كثيرة جداً. يرجى التباطؤ.',
'retry_after' => $headers['Retry-After']
], 429, $headers);
});
});
}
// تطبيق على المسارات
// routes/api.php
Route::middleware(['throttle:api-premium'])->group(function () {
Route::apiResource('users', UserController::class);
});
// تطبيق على نقاط نهاية محددة
Route::post('/expensive-operation', [ApiController::class, 'process'])
->middleware('throttle:5,1'); // 5 طلبات في الدقيقة
رؤوس حد المعدل: يضيف Laravel تلقائياً رؤوس X-RateLimit-Limit و X-RateLimit-Remaining و Retry-After إلى الاستجابات المحددة المعدل، مما يسهل على عملاء API التعامل مع الحدود بسلاسة.
تنسيق الاستجابة
أنشئ استجابات API متسقة وموحدة عبر تطبيقك:
// app/Traits/ApiResponse.php
namespace App\Traits;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Response;
trait ApiResponse
{
/**
* استجابة النجاح
*/
protected function success($data = null, string $message = null, int $code = Response::HTTP_OK): JsonResponse
{
$response = [
'success' => true,
'message' => $message,
'data' => $data,
];
return response()->json(array_filter($response), $code);
}
/**
* استجابة الخطأ
*/
protected function error(string $message, int $code = Response::HTTP_BAD_REQUEST, $errors = null): JsonResponse
{
$response = [
'success' => false,
'message' => $message,
'errors' => $errors,
];
return response()->json(array_filter($response), $code);
}
/**
* استجابة خطأ التحقق
*/
protected function validationError($errors, string $message = 'فشل التحقق'): JsonResponse
{
return response()->json([
'success' => false,
'message' => $message,
'errors' => $errors
], Response::HTTP_UNPROCESSABLE_ENTITY);
}
/**
* استجابة غير موجود
*/
protected function notFound(string $message = 'المورد غير موجود'): JsonResponse
{
return $this->error($message, Response::HTTP_NOT_FOUND);
}
/**
* استجابة غير مصرح
*/
protected function unauthorized(string $message = 'غير مصرح'): JsonResponse
{
return $this->error($message, Response::HTTP_UNAUTHORIZED);
}
/**
* استجابة مقسمة إلى صفحات
*/
protected function paginated($data, string $message = null): JsonResponse
{
return response()->json([
'success' => true,
'message' => $message,
'data' => $data->items(),
'meta' => [
'current_page' => $data->currentPage(),
'last_page' => $data->lastPage(),
'per_page' => $data->perPage(),
'total' => $data->total(),
'from' => $data->firstItem(),
'to' => $data->lastItem(),
],
'links' => [
'first' => $data->url(1),
'last' => $data->url($data->lastPage()),
'prev' => $data->previousPageUrl(),
'next' => $data->nextPageUrl(),
]
]);
}
}
// الاستخدام في المتحكم
class UserController extends Controller
{
use ApiResponse;
public function store(Request $request)
{
$validated = $request->validate([...]);
$user = User::create($validated);
return $this->success(
new UserResource($user),
'تم إنشاء المستخدم بنجاح',
Response::HTTP_CREATED
);
}
public function index()
{
$users = User::paginate(15);
return $this->paginated($users, 'تم استرجاع المستخدمين بنجاح');
}
}
تكوين CORS
قم بتكوين مشاركة الموارد عبر الأصول للسماح بالوصول إلى API الخاص بك من نطاقات مختلفة:
// config/cors.php
return [
'paths' => ['api/*', 'sanctum/csrf-cookie'],
'allowed_methods' => ['*'],
'allowed_origins' => [
'https://example.com',
'https://app.example.com',
],
// للتطوير فقط
'allowed_origins_patterns' => [
'/^https?:\/\/localhost(:\d+)?$/',
],
'allowed_headers' => ['*'],
'exposed_headers' => [
'X-RateLimit-Limit',
'X-RateLimit-Remaining',
],
'max_age' => 0,
'supports_credentials' => true,
];
// إضافة برنامج CORS الوسيط لمسارات api
// app/Http/Kernel.php
protected $middlewareGroups = [
'api' => [
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\HandleCors::class, // أضف هذا
],
];
// أو استخدم معالجة CORS المدمجة في Laravel
// تأكد من أن \Fruitcake\Cors\HandleCors موجود في $middleware
أمان CORS: لا تستخدم أبداً 'allowed_origins' => ['*'] في الإنتاج! حدد دائماً النطاقات الدقيقة. استخدم 'supports_credentials' => true فقط عند الضرورة، حيث يتطلب أصولاً محددة (وليس أحرف بدل).
توثيق API
وثق API الخاص بك باستخدام أدوات Laravel المدمجة أو الحزم الشائعة:
// تثبيت Scribe لتوثيق API
composer require --dev knuckleswtf/scribe
// إنشاء التوثيق
php artisan scribe:generate
// إضافة docblocks للمتحكمات
/**
* سرد جميع المستخدمين
*
* يعيد قائمة مقسمة إلى صفحات من المستخدمين مع تصفية بحث اختيارية.
*
* @group إدارة المستخدم
*
* @queryParam search string تصفية المستخدمين حسب الاسم. مثال: john
* @queryParam per_page int عدد العناصر في الصفحة. مثال: 20
*
* @response {
* "data": [
* {
* "id": 1,
* "name": "John Doe",
* "email": "john@example.com"
* }
* ],
* "links": {...},
* "meta": {...}
* }
*/
public function index(Request $request)
{
// ...
}
// بديل: OpenAPI/Swagger
composer require darkaonline/l5-swagger
// إنشاء توثيق Swagger
php artisan l5-swagger:generate
// إضافة تعليقات Swagger التوضيحية
/**
* @OA\Get(
* path="/api/users",
* summary="الحصول على قائمة المستخدمين",
* tags={"Users"},
* @OA\Parameter(
* name="page",
* in="query",
* description="رقم الصفحة",
* required=false,
* @OA\Schema(type="integer")
* ),
* @OA\Response(
* response=200,
* description="عملية ناجحة"
* )
* )
*/
التمرين 1: بناء مورد API كامل
أنشئ Post API بالمتطلبات التالية:
- أنشئ PostController مع جميع عمليات CRUD
- أنشئ PostResource مع تحويل البيانات المناسب
- نفذ التصفية حسب الفئة والمؤلف
- أضف تحديد المعدل (60 طلباً في الدقيقة)
- قم بتضمين التحقق المناسب لـ store/update
- أرجع استجابات JSON موحدة
- أضف ترقيم الصفحات مع معلومات meta
التمرين 2: تنفيذ إصدارات API
أنشئ نسختين من User API:
- V1: يعيد المستخدم مع id, name, email
- V2: يعيد المستخدم مع uuid (بدلاً من id), full_name (بدلاً من name), email, created_at
- قم بإعداد المسارات لكلا الإصدارين
- أنشئ متحكمات منفصلة أو استخدم اكتشاف الإصدار
- وثق الاختلافات بين الإصدارات
التمرين 3: تحديد معدل متقدم
نفذ تحديد معدل متدرج:
- المستخدمون المجانيون: 30 طلباً في الدقيقة
- المستخدمون المميزون: 120 طلباً في الدقيقة
- المستخدمون الإداريون: لا حدود
- المستخدمون المجهولون: 10 طلبات في الدقيقة حسب IP
- أرجع رسائل خطأ مخصصة مع معلومات الترقية
- أضف رأس X-RateLimit-Tier مخصص
الخلاصة
في هذا الدرس، تعلمت كيفية تصميم وبناء واجهات برمجية RESTful باستخدام Laravel. استكشفت اصطلاحات توجيه API، ومتحكمات الموارد والتحويلات، واستراتيجيات الإصدار، وتحديد المعدل، وتكوين CORS، وتوثيق API. تمكنك هذه المهارات من إنشاء واجهات برمجية احترافية وقابلة للتطوير تتبع أفضل ممارسات الصناعة.
في الدرس التالي، سنغوص في Policies & Gates في Laravel للتحكم الدقيق في التفويض.