إطار Laravel

موارد API والتحويلات

18 دقيقة الدرس 22 من 45

موارد API والتحويلات

عند بناء واجهات برمجة التطبيقات، تحتاج إلى التحكم بالضبط في البيانات المعروضة وكيفية تنسيقها. توفر موارد API في Laravel طبقة تحويل بين نماذج Eloquent واستجابات JSON المرسلة إلى العملاء. يغطي هذا الدرس كيفية إنشاء استجابات API مرنة وقابلة للصيانة.

فهم موارد API

تعمل موارد API كطبقة تحويل تقع بين نماذجك واستجابات API الخاصة بك. تسمح لك بـ:

  • التحكم في خصائص النموذج المعروضة
  • تحويل تنسيقات البيانات (التواريخ، الأرقام، إلخ)
  • تضمين أو استبعاد البيانات المرتبطة بشكل مشروط
  • إضافة خصائص محسوبة
  • الحفاظ على هياكل استجابة API متسقة
إنشاء مورد:
// إنشاء فئة مورد
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
{
    /**
     * Transform the resource into an array.
     *
     * @return array<string, mixed>
     */
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'email' => $this->email,
            'created_at' => $this->created_at->toDateTimeString(),
            'updated_at' => $this->updated_at->toDateTimeString(),
        ];
    }
}

// الاستخدام في المتحكم
use App\Http\Resources\UserResource;
use App\Models\User;

public function show($id)
{
    $user = User::findOrFail($id);
    return new UserResource($user);
}
مهم: داخل فئة المورد، $this يشير إلى نموذج instance الأساسي. يمكنك الوصول إلى جميع خصائص وطرق النموذج باستخدام $this->propertyName.

مجموعات الموارد

عند إرجاع موارد متعددة، يوفر Laravel موارد مجموعة للتعامل مع مصفوفات البيانات:

إنشاء واستخدام المجموعات:
// إنشاء مورد مجموعة
php artisan make:resource UserCollection

// app/Http/Resources/UserCollection.php
namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;

class UserCollection extends ResourceCollection
{
    /**
     * Transform the resource collection into an array.
     *
     * @return array<int|string, mixed>
     */
    public function toArray(Request $request): array
    {
        return [
            'data' => $this->collection,
            'meta' => [
                'total' => $this->collection->count(),
                'generated_at' => now()->toIso8601String(),
            ],
        ];
    }
}

// الاستخدام في المتحكم
public function index()
{
    $users = User::all();
    return new UserCollection($users);
}

// أو استخدام طريقة collection() على مورد واحد
public function index()
{
    return UserResource::collection(User::all());
}
نصيحة سريعة: للمجموعات البسيطة، استخدم ResourceClass::collection() بدلاً من إنشاء فئة مجموعة منفصلة. احتفظ بفئات المجموعة عندما تحتاج إلى بيانات وصفية مخصصة أو تعديلات على الغلاف.

الخصائص الشرطية

يمكن للموارد تضمين أو استبعاد الخصائص بناءً على الشروط، مما يوفر مرونة في استجابات API:

التضمين الشرطي:
namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class PostResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'title' => $this->title,
            'content' => $this->content,
            'author' => $this->author->name,

            // تضمين فقط عندما يكون الشرط صحيحاً
            'edit_url' => $this->when(
                $request->user()?->can('update', $this->resource),
                route('posts.edit', $this->id)
            ),

            // تضمين مع قيمة افتراضية
            'views' => $this->when($this->views !== null, $this->views, 0),

            // دمج الخصائص بشكل مشروط
            $this->mergeWhen($request->user()?->isAdmin(), [
                'internal_notes' => $this->notes,
                'flagged' => $this->is_flagged,
            ]),

            // تضمين مورد مرتبط بشكل مشروط
            'comments' => CommentResource::collection(
                $this->whenLoaded('comments')
            ),

            'category' => new CategoryResource(
                $this->whenLoaded('category')
            ),

            // تضمين بيانات pivot إذا كانت موجودة
            'pivot' => $this->whenPivotLoaded('post_tag', function () {
                return [
                    'created_at' => $this->pivot->created_at,
                ];
            }),

            'created_at' => $this->created_at->toDateTimeString(),
            'updated_at' => $this->updated_at->toDateTimeString(),
        ];
    }
}
تحذير استعلام N+1: استخدم دائماً whenLoaded() للعلاقات لتجنب مشاكل استعلام N+1. يقوم فقط بتضمين العلاقة إذا تم تحميلها بشغف مع with().

إضافة البيانات الوصفية والروابط

يمكن للموارد تضمين بيانات وصفية إضافية وروابط hypermedia لجعل API أكثر إفادة:

البيانات الوصفية والروابط:
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,
        ];
    }

    /**
     * Add additional data to the resource response.
     *
     * @return array<string, mixed>
     */
    public function with(Request $request): array
    {
        return [
            'meta' => [
                'version' => '1.0',
                'generated_at' => now()->toIso8601String(),
            ],
        ];
    }

    /**
     * Add links to the resource response.
     *
     * @return array<string, string>
     */
    public function withResponse(Request $request, $response): void
    {
        $response->header('X-Resource-Version', '1.0');
    }
}

// مفتاح غلاف مخصص
class UserResource extends JsonResource
{
    public static $wrap = 'user';  // الافتراضي هو 'data'

    // أو تعطيل الغلاف تماماً
    public static $wrap = null;
}

// ستبدو الاستجابة كالتالي:
{
    "user": {
        "id": 1,
        "name": "John Doe",
        "email": "john@example.com"
    },
    "meta": {
        "version": "1.0",
        "generated_at": "2026-02-14T10:30:00Z"
    }
}

الترقيم مع الموارد

تعمل الموارد بسلاسة مع ترقيم Laravel، وتتضمن تلقائياً البيانات الوصفية للترقيم:

الموارد المرقمة:
// المتحكم
public function index()
{
    $users = User::paginate(15);
    return UserResource::collection($users);
}

// هيكل الاستجابة
{
    "data": [
        {
            "id": 1,
            "name": "John Doe",
            "email": "john@example.com"
        },
        // ... المزيد من المستخدمين
    ],
    "links": {
        "first": "http://example.com/api/users?page=1",
        "last": "http://example.com/api/users?page=10",
        "prev": null,
        "next": "http://example.com/api/users?page=2"
    },
    "meta": {
        "current_page": 1,
        "from": 1,
        "last_page": 10,
        "path": "http://example.com/api/users",
        "per_page": 15,
        "to": 15,
        "total": 150
    }
}

// تخصيص بيانات الترقيم الوصفية في المجموعة
class UserCollection extends ResourceCollection
{
    public function toArray(Request $request): array
    {
        return [
            'data' => $this->collection,
            'pagination' => [
                'total' => $this->total(),
                'count' => $this->count(),
                'per_page' => $this->perPage(),
                'current_page' => $this->currentPage(),
                'total_pages' => $this->lastPage(),
            ],
        ];
    }
}

الموارد المتداخلة والعلاقات

يمكن أن تحتوي الموارد على موارد أخرى، مما يسمح لك ببناء هياكل متداخلة معقدة:

هياكل الموارد المتداخلة:
// UserResource مع موارد متداخلة
namespace App\Http\Resources;

class UserResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'email' => $this->email,

            // مورد متداخل لعلاقة واحدة
            'profile' => new ProfileResource($this->whenLoaded('profile')),

            // مجموعة متداخلة للعلاقات المتعددة
            'posts' => PostResource::collection($this->whenLoaded('posts')),

            // مورد متداخل شرطي
            'company' => $this->when(
                $this->relationLoaded('company') && $this->company,
                new CompanyResource($this->company)
            ),
        ];
    }
}

// المتحكم مع التحميل المسبق
public function show($id)
{
    $user = User::with(['profile', 'posts', 'company'])->findOrFail($id);
    return new UserResource($user);
}
نصيحة للأداء: قم دائماً بتحميل العلاقات بشغف التي ستضمنها في الموارد. استخدم whenLoaded() لتضمينها بشكل مشروط فقط عندما تكون موجودة، متجنباً استعلامات N+1.

الخصائص المحسوبة والمخصصة

يمكن للموارد إضافة قيم محسوبة غير موجودة في قاعدة البيانات:

إضافة خصائص محسوبة:
namespace App\Http\Resources;

class ProductResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'price' => $this->price,
            'sale_price' => $this->sale_price,

            // خصائص محسوبة
            'discount_percentage' => $this->calculateDiscountPercentage(),
            'is_on_sale' => $this->sale_price < $this->price,
            'formatted_price' => '$' . number_format($this->price, 2),

            // استدعاء طرق النموذج
            'average_rating' => round($this->averageRating(), 1),
            'stock_status' => $this->getStockStatus(),

            // قيم محسوبة معقدة
            'savings' => $this->when($this->sale_price, function () {
                return [
                    'amount' => $this->price - $this->sale_price,
                    'percentage' => $this->calculateDiscountPercentage(),
                ];
            }),
        ];
    }

    protected function calculateDiscountPercentage()
    {
        if (!$this->sale_price || $this->sale_price >= $this->price) {
            return 0;
        }

        return round((($this->price - $this->sale_price) / $this->price) * 100);
    }
}

استجابات الموارد وأكواد الحالة

يمكن للموارد تضمين أكواد حالة HTTP مخصصة ورؤوس:

استجابات مخصصة:
// في المتحكم - أكواد حالة مخصصة
public function store(Request $request)
{
    $user = User::create($request->validated());

    return (new UserResource($user))
        ->response()
        ->setStatusCode(201)
        ->header('X-Resource-Created', true);
}

public function update(Request $request, $id)
{
    $user = User::findOrFail($id);
    $user->update($request->validated());

    return (new UserResource($user))
        ->response()
        ->setStatusCode(200);
}

// غلاف استجابة إضافي
public function show($id)
{
    $user = User::findOrFail($id);

    return (new UserResource($user))
        ->additional([
            'message' => 'User retrieved successfully',
            'status' => 'success'
        ]);
}

// الاستجابة:
{
    "data": {
        "id": 1,
        "name": "John Doe"
    },
    "message": "User retrieved successfully",
    "status": "success"
}
تمرين عملي 1: أنشئ BlogPostResource يتضمن: id، title، excerpt، author (مورد متداخل)، اسم الفئة، تاريخ النشر (منسق كـ "F j, Y")، وقت القراءة (محسوب من طول المحتوى)، وعدد التعليقات. قم بتضمين edit_url فقط إذا كان المستخدم الحالي هو المؤلف.
تمرين عملي 2: ابنِ OrderResource يتضمن: رقم الطلب، الحالة، الإجمالي، العناصر (مجموعة متداخلة)، تفاصيل العميل، وعنوان الشحن. أضف قيمة boolean محسوبة "can_cancel" بناءً على حالة الطلب وتاريخ الإنشاء. قم بتضمين كائن "tracking" فقط إذا تم شحن الطلب.
تمرين عملي 3: أنشئ UserProfileResource مع ترقيم يعيد المستخدمين مع عدد منشوراتهم وعدد المتابعين وعدد المتابعين. أضف بيانات وصفية مخصصة تتضمن total_active_users و average_posts_per_user و generated_at timestamp. خصص غلاف الترقيم لاستخدام "users" بدلاً من "data".

الخلاصة

توفر موارد API طبقة تحويل قوية لاستجابات API الخاصة بك:

  • هيكل متسق: الحفاظ على تنسيقات استجابة API يمكن التنبؤ بها
  • خصائص شرطية: تضمين/استبعاد البيانات بناءً على الأذونات أو السياق
  • قيم محسوبة: إضافة خصائص محسوبة دون تعديل النماذج
  • إدارة العلاقات: التعامل مع الموارد المتداخلة وتجنب استعلامات N+1
  • دعم الترقيم: بيانات وصفية تلقائية للنتائج المرقمة
  • المرونة: إضافة بيانات وصفية مخصصة وروابط ورؤوس

في الدرس التالي، سنستكشف Service Container في Laravel وحقن التبعيات لفهم كيفية إدارة Laravel لتبعيات الفئات.