404 Not Found

404 Not Found


nginx

موارد API في Laravel وتطوير واجهات RESTful

موارد API هي "غلاف البيانات" في Laravel — تتحكم في الحقول المكشوفة للعملاء، وكيفية تنسيقها، وكيفية تداخل العلاقات، مما يضمن أن مخرجات API تتبع معيارًا موحدًا.

1. ما ستتعلمه


2. قصة حقيقية لمطور واجهة أمامية

(1) مشكلة: تنسيق JSON الذي تعيده API يتسبب في تعطل الواجهة الأمامية

واجهت Alice كابوسًا أثناء التكامل مع API لـ ShopMetrics — أعاد /shops النتيجة {shops: [...]}، لكن /orders أعاد {data: [...]}، و/products أعاد مصفوفة ببساطة. أسماء الحقول لم تكن متسقة أيضًا: بعضها استخدم created_at، وبعضها استخدم createdAt، وأخرى استخدمت createdDate. كما تم كشف كلمات المرور والمعرفات الداخلية في JSON. كان هناك كود تكيف في الواجهة الأمامية أكثر من منطق الأعمال.

(2) حل موارد API

تتحكم موارد API بشكل موحد في تنسيق مخرجات JSON — مما يضمن أسماء حقول متسقة، وإخفاء الحقول الحساسة، وتداخل شروط الارتباط، والالتزام بتنسيقات التقسيم القياسية.

PHP
// app/Http/Resources/ShopResource.php
class ShopResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'slug' => $this->slug,
            'status' => $this->status,
            'products' => ProductResource::collection($this->whenLoaded('products')),
            'created_at' => $this->created_at->toISOString(),
        ];
    }
}

(3) العائد

بعد أن بدأت Alice في استخدام موارد API، أصبحت جميع نقاط النهاية موحدة، وتم تقليل كود التكيف في الواجهة الأمامية بنسبة 80%، وتم إخفاء الحقول الحساسة مثل كلمات المرور تلقائيًا.


3. فئة Resource وResourceCollection

(1) إنشاء Resource

BASH
php artisan make:resource ShopResource
php artisan make:resource ProductResource
php artisan make:resource OrderResource
# ينشئ: app/Http/Resources/ShopResource.php

(2) فئة Resource (سجل واحد)

PHP
// app/Http/Resources/ShopResource.php
class ShopResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'slug' => $this->slug,
            'description' => $this->description,
            'status' => $this->status,
            'revenue' => (float) $this->revenue,
            'created_at' => $this->created_at->toISOString(),
            'updated_at' => $this->updated_at->toISOString(),
        ];
    }
}

// الاستخدام — مورد واحد
return new ShopResource($shop);
// {"data": {"id": 1, "name": "Alice Store", ...}}

(3) ResourceCollection (سجلات متعددة)

PHP
// استخدام مجموعة الموارد
return ShopResource::collection($shops);
// {"data": [...], "links": {...}, "meta": {...}}

// مجموعة مخصصة
php artisan make:resource ShopCollection
class ShopCollection extends ResourceCollection
{
    public function toArray(Request $request): array
    {
        return [
            'data' => $this->collection,
            'meta' => [
                'total_shops' => $this->collection->count(),
            ],
        ];
    }
}
النوع تنسيق الإرجاع مناسب لـ
new Resource($model) {data: {...}} سجل واحد
Resource::collection($models) {data: [...], links, meta} قائمة + تقسيم
CustomCollection تنسيق مخصص يتطلب meta إضافي

(1) ▶ مثال:ShopResource لـ ShopMetrics

PHP
// app/Http/Resources/ShopResource.php
class ShopResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'slug' => $this->slug,
            'description' => $this->whenNotNull($this->description),
            'status' => $this->status,
            'revenue' => [
                'raw' => (float) $this->revenue,
                'formatted' => $this->revenue_formatted,
            ],
            'products_count' => $this->whenCounted('products'),
            'orders_count' => $this->whenCounted('orders'),
            'products' => ProductResource::collection($this->whenLoaded('products')),
            'latest_order' => new OrderResource($this->whenLoaded('latestOrder')),
            'links' => [
                'self' => route('api.v1.shops.show', $this->id),
                'products' => route('api.v1.products.index', ['shop_id' => $this->id]),
            ],
            'created_at' => $this->created_at->toISOString(),
        ];
    }
}

الناتج:

TEXT
// التنفيذ ناجح

4. الموارد المرتبطة المتداخلة

(1) التحميل المشروط عبر whenLoaded()

PHP
// تضمين العلاقة فقط إذا تم تحميلها مسبقًا
'products' => ProductResource::collection($this->whenLoaded('products')),

// إذا لم يتم تحميل المنتجات مع with()، فإن هذا يعيد null ويتم حذفه
// Shop::find(1) → لا يوجد مفتاح products في JSON
// Shop::with('products')->find(1) → يتم تضمين المنتجات

// علاقة واحدة
'user' => new UserResource($this->whenLoaded('user')),

// العدد فقط (بدون بيانات)
'products_count' => $this->whenCounted('products'),

(2) تعريف الموارد المتداخلة

PHP
// app/Http/Resources/OrderResource.php
class OrderResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'order_number' => $this->order_number,
            'status' => $this->status,
            'total' => (float) $this->total,
            'items' => OrderItemResource::collection($this->whenLoaded('items')),
            'shop' => new ShopBriefResource($this->whenLoaded('shop')),
            'user' => new UserBriefResource($this->whenLoaded('user')),
            'created_at' => $this->created_at->toISOString(),
        ];
    }
}

// مورد مختصر — بيانات حد أدنى للتداخل
class ShopBriefResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'slug' => $this->slug,
        ];
    }
}
الطريقة الوصف كيفية تجنب المشاكل
whenLoaded() الإخراج فقط بعد تحميل الانضمامات تجنب N+1 مع التحميل الكسول
whenCounted() الإخراج فقط بعد العد تجنب الاستعلامات الإضافية
whenNotNull() الإخراج فقط إذا لم يكن null تنظيف الحقول الفارغة
when() إخراج مشروط تحكم مرن
مورد مختصر استخدام النسخة الخفيفة للتداخل تجنب الحلقات المتداخلة

(1) ▶ مثال:الموارد المتداخلة لـ ShopMetrics

PHP
// app/Http/Resources/OrderItemResource.php
class OrderItemResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'product' => new ProductBriefResource($this->whenLoaded('product')),
            'quantity' => $this->quantity,
            'unit_price' => (float) $this->price,
            'subtotal' => (float) ($this->price * $this->quantity),
        ];
    }
}

// app/Http/Resources/ProductBriefResource.php
class ProductBriefResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'sku' => $this->sku,
        ];
    }
}

الناتج:

TEXT
// التنفيذ ناجح

5. مواصفات تصميم واجهة RESTful API

(1) URIs وأفعال HTTP

العملية HTTP URI الوصف
قائمة GET /api/v1/shops يعيد مجموعة
إنشاء POST /api/v1/shops إنشاء مورد
تفاصيل GET /api/v1/shops/{id} يعيد سجلاً واحدًا
تحديث كامل PUT /api/v1/shops/{id} استبدال المورد
تحديث جزئي PATCH /api/v1/shops/{id} تعديل حقول
حذف DELETE /api/v1/shops/{id} حذف المورد

(2) رموز حالة HTTP

رمز الحالة المعنى حالة الاستخدام
200 OK استجابة ناجحة
201 Created تم إنشاء المورد بنجاح
204 No Content تم الحذف بنجاح
400 Bad Request تنسيق طلب غير صالح
401 Unauthorized غير مصدق
403 Forbidden بدون صلاحية
404 Not Found المورد غير موجود
422 Unprocessable Entity فشل التحقق
429 Too Many Requests تحديد المعدل
500 Internal Server Error خطأ في الخادم

(3) تنسيق استجابة الخطأ

JSON
{
    "success": false,
    "message": "Validation failed.",
    "errors": {
        "name": ["The name field is required."],
        "email": ["The email must be a valid email address."]
    }
}

(1) ▶ مثال:تصميم نقطة نهاية RESTful API لـ ShopMetrics

PHP
// routes/api.php
Route::prefix('v1')->middleware('auth:sanctum')->group(function () {
    // المتاجر
    Route::apiResource('shops', Api\V1\ShopController::class);
    Route::get('shops/{shop}/analytics', [Api\V1\ShopAnalyticsController::class, 'show']);

    // المنتجات (متداخلة ثم سطحية)
    Route::apiResource('shops.products', Api\V1\ProductController::class)->shallow();

    // الطلبات
    Route::apiResource('orders', Api\V1\OrderController::class)->only(['index', 'show', 'update']);
    Route::post('orders/{order}/cancel', [Api\V1\OrderController::class, 'cancel']);

    // الفئات
    Route::apiResource('categories', Api\V1\CategoryController::class)->only(['index', 'show']);

    // التحليلات
    Route::get('analytics/overview', [Api\V1\AnalyticsController::class, 'overview']);
});

الناتج:

TEXT
// التنفيذ ناجح

6. تغليف التصفية والفرز والتقسيم

(1) فئة أساسية لتصفية الاستعلامات

PHP
// app/Filters/QueryFilter.php
abstract class QueryFilter
{
    public function __construct(protected Request $request) {}

    public function apply(Builder $query): Builder
    {
        foreach ($this->filters() as $filter => $value) {
            if (method_exists($this, $filter) && $value !== null) {
                $this->$filter($query, $value);
            }
        }
        return $query;
    }

    protected function filters(): array
    {
        return $this->request->all();
    }
}

(2) فلتر محدد

PHP
// app/Filters/ShopFilter.php
class ShopFilter extends QueryFilter
{
    public function search(Builder $query, string $value): Builder
    {
        return $query->where('name', 'like', "%{$value}%");
    }

    public function status(Builder $query, string $value): Builder
    {
        return $query->where('status', $value);
    }

    public function min_revenue(Builder $query, float $value): Builder
    {
        return $query->where('revenue', '>=', $value);
    }

    public function sort(Builder $query, string $value): Builder
    {
        $direction = str_starts_with($value, '-') ? 'desc' : 'asc';
        $field = ltrim($value, '-');
        return $query->orderBy($field, $direction);
    }
}

(1) ▶ مثال:نقاط نهاية API لـ ShopMetrics مع الفلاتر

PHP
// app/Http/Controllers/Api/V1/ShopController.php
class ShopController extends Controller
{
    public function index(ShopFilter $filter): JsonResponse
    {
        $shops = Shop::where('tenant_id', tenant()->id)
            ->filter($filter)
            ->withCount(['products', 'orders'])
            ->paginate(request()->integer('per_page', 15));

        return ShopResource::collection($shops);
    }

    public function store(StoreShopRequest $request): JsonResponse
    {
        $shop = Shop::create(array_merge($request->validated(), ['tenant_id' => tenant()->id]));
        return response()->json([
            'message' => 'Shop created.',
            'data' => new ShopResource($shop),
        ], 201);
    }

    public function show(Shop $shop): JsonResponse
    {
        $shop->load(['products' => fn ($q) => $q->active()->latest()->take(10)]);
        return new ShopResource($shop);
    }

    public function update(UpdateShopRequest $request, Shop $shop): JsonResponse
    {
        $shop->update($request->validated());
        return new ShopResource($shop);
    }

    public function destroy(Shop $shop): Response
    {
        $shop->delete();
        return response()->noContent();
    }
}

الناتج:

TEXT
// التنفيذ ناجح

7. التحكم في إصدارات API

(1) إصدار مجموعة المسارات

PHP
// routes/api.php
Route::prefix('v1')->group(function () {
    Route::apiResource('shops', Api\V1\ShopController::class);
});

Route::prefix('v2')->group(function () {
    Route::apiResource('shops', Api\V2\ShopController::class);
});

(2) وراثة الموارد

PHP
// V1 ShopResource
namespace App\Http\Resources\Api\V1;
class ShopResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'status' => $this->status,
        ];
    }
}

// V2 ShopResource — يرث ويضيف حقولاً
namespace App\Http\Resources\Api\V2;
class ShopResource extends \App\Http\Resources\Api\V1\ShopResource
{
    public function toArray(Request $request): array
    {
        return array_merge(parent::toArray($request), [
            'revenue' => (float) $this->revenue,
            'products_count' => $this->whenCounted('products'),
            'links' => [
                'self' => route('api.v2.shops.show', $this->id),
            ],
        ]);
    }
}
استراتيجية الإصدار النهج الإيجابيات والسلبيات
بادئة URL /api/v1/، /api/v2/ ✅ بسيطة وواضحة
رأس Accept: application/vnd.api.v2+json أكثر توافقًا مع REST لكن أكثر تعقيدًا
استعلام ?version=2 غير موصى به، ليس RESTful

(1) ▶ مثال:مسارات إصدارات API لـ ShopMetrics

PHP
// routes/api.php
Route::prefix('v1')->middleware('auth:sanctum')->group(function () {
    Route::apiResource('shops', Api\V1\ShopController::class);
    Route::apiResource('products', Api\V1\ProductController::class);
    Route::apiResource('orders', Api\V1\OrderController::class)->only(['index', 'show']);
});

Route::prefix('v2')->middleware('auth:sanctum')->group(function () {
    Route::apiResource('shops', Api\V2\ShopController::class);
    Route::apiResource('products', Api\V2\ProductController::class);
    Route::apiResource('orders', Api\V2\OrderController::class);
    // V2 يضيف CRUD كامل للطلبات + التحليلات
    Route::get('analytics', [Api\V2\AnalyticsController::class, 'overview']);
});

الناتج:

TEXT
// التنفيذ ناجح

8. مثال شامل: العملية الكاملة لموارد API لـ ShopMetrics

PHP
// ============================================
// شامل: موارد API لـ ShopMetrics
// يغطي: الموارد، العلاقات، التصفية، التقسيم، الإصدارات
// ============================================

// app/Http/Resources/Api/V1/OrderResource.php
class OrderResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'order_number' => $this->order_number,
            'status' => $this->status,
            'subtotal' => (float) $this->subtotal,
            'discount' => (float) $this->discount,
            'total' => (float) $this->total,
            'items_count' => $this->whenCounted('items'),
            'items' => OrderItemResource::collection($this->whenLoaded('items')),
            'shop' => new ShopBriefResource($this->whenLoaded('shop')),
            'user' => [
                'id' => $this->whenLoaded('user')?->id,
                'name' => $this->whenLoaded('user')?->name,
            ],
            'created_at' => $this->created_at->toISOString(),
            'updated_at' => $this->updated_at->toISOString(),
        ];
    }
}

// وحدة تحكم مع خط أنابيب كامل
class OrderController extends Controller
{
    public function index(OrderFilter $filter): JsonResponse
    {
        $orders = Order::where('tenant_id', tenant()->id)
            ->filter($filter)
            ->with(['shop', 'user'])
            ->withCount('items')
            ->latest()
            ->paginate(request()->integer('per_page', 15));

        return OrderResource::collection($orders);
    }

    public function show(Order $order): JsonResponse
    {
        $order->load(['items.product', 'shop', 'user']);
        return new OrderResource($order);
    }

    public function update(UpdateOrderRequest $request, Order $order): JsonResponse
    {
        $order->update($request->validated());
        return new OrderResource($order->fresh()->load('items.product'));
    }
}

❓ أسئلة شائعة

س ما الفرق بين استخدام Resource وإرجاع JSON النموذج مباشرة؟
ج إرجاع النموذج مباشرة يكشف جميع الحقول (بما في ذلك البيانات الحساسة مثل كلمات المرور)، ولا يمكن التحكم في التنسيق؛ Resource يسمح بالتحكم الدقيق في حقول الإخراج، والقيم المنسقة، والعلاقات المتداخلة. يجب أن تستخدم واجهات API Resource.
س ما الفرق بين whenLoaded والوصول المباشر إلى العلاقة؟
ج الوصول المباشر إلى العلاقة يؤدي إلى التحميل الكسول (مشكلة N+1)؛ whenLoaded يُخرج العلاقة فقط إذا تم تحميلها مسبقًا؛ إذا لم يتم تحميلها، يعيد null ويتم إزالته تلقائيًا من JSON.
س كم عدد إصدارات API المطلوبة؟
ج عادةً، يتم الاحتفاظ بإصدارين فقط (الحالي والسابق). عند إصدار إصدار جديد، يُمنح المستخدمون فترة ترحيل مدتها 6 أشهر؛ بعد انتهاء تلك الفترة، يعيد الإصدار القديم حالة 410 Gone. تجنب الحفاظ على إصدارات كثيرة في نفس الوقت.
س هل يمكن تخصيص تنسيق التقسيم لـ ResourceCollection؟
ج نعم. أنشئ فئة Collection مخصصة وتجاوز طريقة toArray()، أو استخدم طريقة paginationResponse() الخاصة بـ JsonResource::collection() لتخصيص تنسيق التقسيم.
س كيف أصل إلى المستخدمين المصادق عليهم في Resource؟
ج استخدم $request->user() أو auth()->user(). تقبل طريقة toArray() في Resource معامل Request، مما يسمح لك بتحديد الحقول المراد إخراجها بناءً على صلاحيات المستخدم.
س ماذا أفعل إذا كان هناك كود مكرر بين Brief Resource وFull Resource؟
ج اجعل Brief Resource يرث من Full Resource وتجاوز فقط toArray() لإخراج حقول أقل؛ أو استخدم $this->when() في Full Resource لإخراج الحقول ديناميكيًا بناءً على السياق.

📖 ملخص


📝 تمارين

  1. تمرين أساسي (⭐): أنشئ ثلاث فئات موارد — ShopResource وProductResource وOrderResource — لـ ShopMetrics. في وحدة التحكم، استبدل الإرجاع المباشر للنموذج لضمان تنسيق JSON متسق.

  2. تمرين متقدم (⭐⭐): نفذ فلتر استعلام ShopFilter لدعم معاملات البحث والحالة وmin_revenue والفرز. استخدمه في ShopController::index واختبر وظيفة التصفية باستخدام Postman.

  3. تحدي (⭐⭐⭐): صمم إصدارين من API، V1 وV2 — V1 يعيد الحقول الأساسية فقط، بينما V2 يعيد إضافيًا الإيرادات وproducts_count والروابط. نفذ ذلك باستخدام وراثة الموارد، مع التأكد من أن نقطة نهاية V1 لا تكسر العملاء الحاليين.

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%