تطوير واجهات REST API
بناء واجهة برمجة تطبيقات RESTful كاملة - الجزء 2
بناء واجهة برمجة تطبيقات RESTful كاملة - الجزء 2
في الجزء 2، سننفذ المصادقة باستخدام JWT، وننشئ المستودعات والإجراءات، ونبني نقاط نهاية CRUD للمنتجات والفئات، ونتعامل مع تحميلات الملفات لصور المنتجات.
إعداد مصادقة JWT
أولاً، قم بتثبيت حزمة JWT:
تثبيت tymon/jwt-auth:
composer require tymon/jwt-auth
php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"
php artisan jwt:secret
config/auth.php - تكوين JWT Guard:
<?php
return [
'defaults' => [
'guard' => 'api',
'passwords' => 'users',
],
'guards' => [
'api' => [
'driver' => 'jwt',
'provider' => 'users',
],
],
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\Models\User::class,
],
],
];
app/Models/User.php - تنفيذ JWTSubject:
<?php
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Tymon\JWTAuth\Contracts\JWTSubject;
class User extends Authenticatable implements JWTSubject
{
protected $fillable = [
'name',
'email',
'password',
'phone',
'role',
];
protected $hidden = [
'password',
'remember_token',
];
protected $casts = [
'email_verified_at' => 'datetime',
];
/**
* الحصول على معرف JWT
*/
public function getJWTIdentifier()
{
return $this->getKey();
}
/**
* الحصول على مطالبات JWT المخصصة
*/
public function getJWTCustomClaims()
{
return [
'role' => $this->role,
];
}
/**
* التحقق من كون المستخدم مسؤولاً
*/
public function isAdmin(): bool
{
return $this->role === 'admin';
}
}
وحدة تحكم المصادقة
app/Http/Controllers/Api/AuthController.php:
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\RegisterRequest;
use App\Http\Requests\LoginRequest;
use App\Http\Resources\UserResource;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Hash;
class AuthController extends Controller
{
/**
* تسجيل مستخدم جديد
*/
public function register(RegisterRequest $request): JsonResponse
{
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
'phone' => $request->phone,
]);
$token = auth()->login($user);
return $this->respondWithToken($token, $user, 201);
}
/**
* تسجيل دخول المستخدم
*/
public function login(LoginRequest $request): JsonResponse
{
$credentials = $request->only(['email', 'password']);
if (!$token = auth()->attempt($credentials)) {
return response()->json([
'message' => 'بيانات اعتماد غير صالحة',
], 401);
}
return $this->respondWithToken($token, auth()->user());
}
/**
* الحصول على المستخدم المصادق عليه
*/
public function me(): UserResource
{
return new UserResource(auth()->user());
}
/**
* تسجيل خروج المستخدم
*/
public function logout(): JsonResponse
{
auth()->logout();
return response()->json([
'message' => 'تم تسجيل الخروج بنجاح',
]);
}
/**
* تحديث الرمز
*/
public function refresh(): JsonResponse
{
$token = auth()->refresh();
return $this->respondWithToken($token, auth()->user());
}
/**
* الرد بالرمز
*/
protected function respondWithToken(string $token, User $user, int $status = 200): JsonResponse
{
return response()->json([
'access_token' => $token,
'token_type' => 'bearer',
'expires_in' => auth()->factory()->getTTL() * 60,
'user' => new UserResource($user),
], $status);
}
}
طلبات النموذج للتحقق
app/Http/Requests/RegisterRequest.php:
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class RegisterRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
'password' => ['required', 'string', 'min:8', 'confirmed'],
'phone' => ['nullable', 'string', 'max:20'],
];
}
}
app/Http/Requests/StoreProductRequest.php:
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreProductRequest extends FormRequest
{
public function authorize(): bool
{
return auth()->user()?->isAdmin();
}
public function rules(): array
{
return [
'category_id' => ['required', 'exists:categories,id'],
'name' => ['required', 'string', 'max:255'],
'slug' => ['required', 'string', 'max:255', 'unique:products'],
'description' => ['nullable', 'string'],
'price' => ['required', 'numeric', 'min:0'],
'sale_price' => ['nullable', 'numeric', 'min:0', 'lt:price'],
'sku' => ['required', 'string', 'unique:products'],
'stock_quantity' => ['required', 'integer', 'min:0'],
'is_active' => ['boolean'],
'is_featured' => ['boolean'],
'images' => ['nullable', 'array', 'max:5'],
'images.*' => ['image', 'mimes:jpg,jpeg,png,webp', 'max:2048'],
];
}
}
مستودع المنتجات
app/Repositories/ProductRepository.php:
<?php
namespace App\Repositories;
use App\Models\Product;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Collection;
class ProductRepository
{
/**
* الحصول على المنتجات المقسمة بالفلاتر
*/
public function paginate(array $filters = [], int $perPage = 15): LengthAwarePaginator
{
$query = Product::with(['category', 'images']);
// تصفية حسب الفئة
if (!empty($filters['category_id'])) {
$query->where('category_id', $filters['category_id']);
}
// تصفية حسب الحالة النشطة
if (!empty($filters['is_active'])) {
$query->active();
}
// تصفية حسب المميز
if (!empty($filters['is_featured'])) {
$query->featured();
}
// البحث بالاسم أو الوصف
if (!empty($filters['search'])) {
$search = $filters['search'];
$query->where(function($q) use ($search) {
$q->where('name', 'LIKE', "%{$search}%")
->orWhere('description', 'LIKE', "%{$search}%");
});
}
// نطاق السعر
if (!empty($filters['min_price'])) {
$query->where('price', '>=', $filters['min_price']);
}
if (!empty($filters['max_price'])) {
$query->where('price', '<=', $filters['max_price']);
}
// الترتيب
$sortBy = $filters['sort_by'] ?? 'created_at';
$sortOrder = $filters['sort_order'] ?? 'desc';
$query->orderBy($sortBy, $sortOrder);
return $query->paginate($perPage);
}
/**
* العثور على المنتج مع العلاقات
*/
public function findWithRelations(int $id): ?Product
{
return Product::with(['category', 'images', 'reviews.user'])
->find($id);
}
/**
* إنشاء منتج
*/
public function create(array $data): Product
{
return Product::create($data);
}
/**
* تحديث منتج
*/
public function update(Product $product, array $data): bool
{
return $product->update($data);
}
/**
* حذف منتج
*/
public function delete(Product $product): bool
{
return $product->delete();
}
/**
* الحصول على المنتجات المميزة
*/
public function getFeatured(int $limit = 10): Collection
{
return Product::with(['category', 'images'])
->active()
->featured()
->inStock()
->limit($limit)
->get();
}
}
خدمة تحميل الصور
app/Services/ImageUploadService.php:
<?php
namespace App\Services;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Intervention\Image\Facades\Image;
class ImageUploadService
{
/**
* تحميل وتغيير حجم الصورة
*/
public function upload(
UploadedFile $file,
string $directory = 'products',
int $width = 800,
int $height = 800
): string {
// إنشاء اسم ملف فريد
$filename = time() . '_' . uniqid() . '.' . $file->extension();
$path = "{$directory}/{$filename}";
// تغيير الحجم وتحسين الصورة
$image = Image::make($file)
->fit($width, $height, function ($constraint) {
$constraint->upsize();
})
->encode($file->extension(), 80);
// تخزين الصورة
Storage::disk('public')->put($path, $image);
return $path;
}
/**
* تحميل صور متعددة
*/
public function uploadMultiple(
array $files,
string $directory = 'products'
): array {
$paths = [];
foreach ($files as $file) {
$paths[] = $this->upload($file, $directory);
}
return $paths;
}
/**
* حذف صورة
*/
public function delete(string $path): bool
{
if (Storage::disk('public')->exists($path)) {
return Storage::disk('public')->delete($path);
}
return false;
}
/**
* حذف صور متعددة
*/
public function deleteMultiple(array $paths): void
{
foreach ($paths as $path) {
$this->delete($path);
}
}
}
نقاط نهاية CRUD للمنتجات
app/Http/Controllers/Api/ProductController.php:
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreProductRequest;
use App\Http\Requests\UpdateProductRequest;
use App\Http\Resources\ProductResource;
use App\Models\Product;
use App\Repositories\ProductRepository;
use App\Services\ImageUploadService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
class ProductController extends Controller
{
public function __construct(
private ProductRepository $productRepository,
private ImageUploadService $imageUploadService
) {
$this->middleware('auth:api')->except(['index', 'show']);
$this->middleware('admin')->only(['store', 'update', 'destroy']);
}
/**
* عرض قائمة المنتجات
*/
public function index(Request $request): AnonymousResourceCollection
{
$filters = $request->only([
'category_id',
'is_active',
'is_featured',
'search',
'min_price',
'max_price',
'sort_by',
'sort_order',
]);
$perPage = $request->input('per_page', 15);
$products = $this->productRepository->paginate($filters, $perPage);
return ProductResource::collection($products);
}
/**
* عرض المنتج المحدد
*/
public function show(int $id): ProductResource|JsonResponse
{
$product = $this->productRepository->findWithRelations($id);
if (!$product) {
return response()->json([
'message' => 'المنتج غير موجود',
], 404);
}
return new ProductResource($product);
}
/**
* تخزين منتج جديد
*/
public function store(StoreProductRequest $request): JsonResponse
{
$data = $request->except('images');
// إنشاء المنتج
$product = $this->productRepository->create($data);
// تحميل الصور إذا تم توفيرها
if ($request->hasFile('images')) {
$this->uploadProductImages($product, $request->file('images'));
}
return (new ProductResource($product->load(['category', 'images'])))
->response()
->setStatusCode(201);
}
/**
* تحديث المنتج المحدد
*/
public function update(UpdateProductRequest $request, Product $product): ProductResource
{
$data = $request->except('images');
$this->productRepository->update($product, $data);
// تحميل صور جديدة إذا تم توفيرها
if ($request->hasFile('images')) {
$this->uploadProductImages($product, $request->file('images'));
}
return new ProductResource($product->fresh(['category', 'images']));
}
/**
* إزالة المنتج المحدد
*/
public function destroy(Product $product): JsonResponse
{
// حذف صور المنتج
$imagePaths = $product->images->pluck('image_path')->toArray();
$this->imageUploadService->deleteMultiple($imagePaths);
// حذف المنتج
$this->productRepository->delete($product);
return response()->json([
'message' => 'تم حذف المنتج بنجاح',
]);
}
/**
* تحميل صور المنتج
*/
private function uploadProductImages(Product $product, array $images): void
{
foreach ($images as $index => $image) {
$path = $this->imageUploadService->upload($image, 'products');
$product->images()->create([
'image_path' => $path,
'is_primary' => $index === 0, // الصورة الأولى هي الأساسية
'order' => $index,
]);
}
}
}
موارد API لتنسيق الرد
app/Http/Resources/ProductResource.php:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class ProductResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'category' => new CategoryResource($this->whenLoaded('category')),
'name' => $this->name,
'slug' => $this->slug,
'description' => $this->description,
'price' => (float) $this->price,
'sale_price' => $this->sale_price ? (float) $this->sale_price : null,
'current_price' => (float) $this->current_price,
'sku' => $this->sku,
'stock_quantity' => $this->stock_quantity,
'in_stock' => $this->in_stock,
'is_active' => $this->is_active,
'is_featured' => $this->is_featured,
'images' => ProductImageResource::collection($this->whenLoaded('images')),
'reviews_count' => $this->whenCounted('reviews'),
'average_rating' => $this->when(
$this->relationLoaded('reviews'),
fn() => round($this->reviews->avg('rating'), 1)
),
'created_at' => $this->created_at->toIso8601String(),
'updated_at' => $this->updated_at->toIso8601String(),
];
}
}
app/Http/Resources/ProductImageResource.php:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Facades\Storage;
class ProductImageResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'url' => Storage::disk('public')->url($this->image_path),
'is_primary' => $this->is_primary,
'order' => $this->order,
];
}
}
مسارات API
routes/api.php:
<?php
use App\Http\Controllers\Api\AuthController;
use App\Http\Controllers\Api\ProductController;
use App\Http\Controllers\Api\CategoryController;
use Illuminate\Support\Facades\Route;
Route::prefix('v1')->group(function () {
// مسارات المصادقة
Route::prefix('auth')->group(function () {
Route::post('register', [AuthController::class, 'register']);
Route::post('login', [AuthController::class, 'login']);
Route::post('logout', [AuthController::class, 'logout'])->middleware('auth:api');
Route::post('refresh', [AuthController::class, 'refresh'])->middleware('auth:api');
Route::get('me', [AuthController::class, 'me'])->middleware('auth:api');
});
// مسارات المنتجات
Route::apiResource('products', ProductController::class);
// مسارات الفئات
Route::apiResource('categories', CategoryController::class);
});
Middleware المسؤول
app/Http/Middleware/AdminMiddleware.php:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class AdminMiddleware
{
public function handle(Request $request, Closure $next)
{
if (!auth()->check() || !auth()->user()->isAdmin()) {
return response()->json([
'message' => 'غير مصرح. يتطلب وصول المسؤول.',
], 403);
}
return $next($request);
}
}
ملاحظة أمنية: تحقق دائمًا من تحميلات الملفات - تحقق من نوع الملف والحجم وفحص البرامج الضارة في الإنتاج. قم بتخزين الملفات المحملة خارج جذر الويب أو استخدم التخزين السحابي مع ضوابط الوصول المناسبة.
تمرين تطبيقي:
- قم بتنفيذ CategoryController مع CRUD الكامل
- أنشئ وحدات تحكم Cart و CartItem
- ابنِ تدفق الدفع للطلب
- أضف نقاط نهاية مراجعة المنتج
- قم بتنفيذ الترقيم والتصفية والترتيب لجميع نقاط نهاية القائمة
الملخص
في الجزء 2، قمنا بتنفيذ:
- مصادقة JWT مع تسجيل الدخول والتسجيل وتحديث الرمز
- نمط المستودع للوصول إلى البيانات
- خدمة تحميل الصور مع تغيير الحجم والتحسين
- عمليات CRUD كاملة للمنتجات
- موارد API لتنسيق رد متسق
- التحقق من طلب النموذج
- Middleware المسؤول للتفويض
في الجزء 3، سنضيف اختبارًا شاملاً، وننشئ توثيق API، وننشر إلى الإنتاج، ونراجع أفضل الممارسات.