كيفية استخدام العلاقات في Laravel API Resource وتمرير علاقة hasMany كمصفوفة

عند بناء REST API باستخدام Laravel، نحتاج في كثير من الأحيان إلى إرجاع بيانات تحتوي على علاقات بين Models.

قد يكون لدينا مثلًا طلب شراء يحتوي على مجموعة من العناصر، وكل عنصر مرتبط بمنتج.

في هذه الحالة لا نريد فقط إرجاع بيانات الطلب، بل نريد Response منظمًا يحتوي على:

  • بيانات الطلب.
  • جميع عناصر الطلب.
  • بيانات المنتج المرتبط بكل عنصر.

المشكلة التي يقع فيها بعض المطورين هي التعامل مع علاقة:

hasMany

وكأنها Model واحد، بينما القيمة التي تعيدها هذه العلاقة تكون Collection تحتوي على مجموعة من Models.

في هذا المقال سنتعرف على كيفية التعامل مع علاقات Eloquent داخل Laravel API Resources، ولماذا يظهر الخطأ:

Property [product] does not exist on this collection instance.

ثم سنتعرف على الطريقة الأفضل لتنظيم العلاقة باستخدام Resource مستقل وwhenLoaded()، بالإضافة إلى طريقة بديلة باستخدام map().

جدول المحتويات

  1. المثال الذي سنعمل عليه
  2. العلاقات بين Models
  3. شكل Response المطلوب
  4. سبب ظهور الخطأ
  5. الفرق بين hasOne وhasMany داخل Resource
  6. تحميل العلاقات باستخدام Eager Loading
  7. تحميل العلاقات المتداخلة
  8. الحل الأول: Resource مستقل للعلاقة
  9. إنشاء UserCartItemResource
  10. استخدام whenLoaded
  11. إنشاء Resource للمنتج
  12. الحل الثاني: استخدام map
  13. أي الطريقتين أفضل؟
  14. تجنب مشكلة N+1
  15. اختيار Columns مع العلاقات
  16. التعامل مع علاقة Product غير موجودة
  17. تحسين أسماء الحقول
  18. مثال متكامل
  19. شكل JSON النهائي
  20. أفضل الممارسات
  21. ملخص المقال
  22. الخلاصة

المثال الذي سنعمل عليه

لنفترض أن لدينا جدولًا يمثل سلة أو طلب المستخدم:

user_carts

وكل UserCart يحتوي على عدة عناصر داخل:

user_cart_items

وكل عنصر مرتبط بمنتج:

products

يمكن تمثيل العلاقات بالشكل التالي:

UserCart
   |
   | hasMany
   v
UserCartItem
   |
   | belongsTo
   v
Product

تعريف العلاقات بين Models

داخل Model الخاص بالسلة:

class UserCart extends Model
{
    public function userCartItems()
    {
        return $this->hasMany(
            UserCartItem::class
        );
    }
}

أما داخل:

UserCartItem

فتكون علاقة المنتج:

class UserCartItem extends Model
{
    public function product()
    {
        return $this->belongsTo(
            Product::class
        );
    }
}

وبالتالي كل UserCart يمكن أن يحتوي على أكثر من UserCartItem، بينما كل UserCartItem مرتبط بمنتج واحد.

شكل Response المطلوب

نريد في النهاية إرجاع بيانات قريبة من:

{
    "data": [
        {
            "id": 44,
            "payment_method": 1,
            "received_type": 1,
            "received_location": 1,
            "status": 0,
            "created_at": "2026-08-30T10:30:00.000000Z",

            "items": [
                {
                    "product_name": "Product A",
                    "price": 2.5,
                    "qty": 2
                },
                {
                    "product_name": "Product B",
                    "price": 10,
                    "qty": 2
                }
            ]
        }
    ]
}

لاحظ أن:

items

عبارة عن Array تحتوي على أكثر من عنصر.

سبب ظهور الخطأ

قد يحاول المطور كتابة Resource بالشكل التالي:

public function toArray(Request $request): array
{
    return [
        'id' => $this->id,

        'items' => [
            'product_name' =>
                $this->userCartItems
                    ->product
                    ->name,

            'price' =>
                $this->userCartItems
                    ->price,

            'qty' =>
                $this->userCartItems
                    ->qty,
        ],
    ];
}

وقد يظهر الخطأ:

Property [product] does not exist on this collection instance.

السبب أن:

$this->userCartItems

ليست UserCartItem واحدة.

إنها:

Illuminate\Database\Eloquent\Collection

وتحتوي على عدة UserCartItem Models.

أي أن الشكل الحقيقي أقرب إلى:

$this->userCartItems[0]
$this->userCartItems[1]
$this->userCartItems[2]

وكل عنصر من هذه العناصر هو الذي يحتوي على:

product
price
qty

الفرق بين hasOne وhasMany داخل API Resource

هذه النقطة هي مفتاح فهم المشكلة.

علاقة تعيد Model واحدًا

مثل:

belongsTo
hasOne

يمكن التعامل معها كعنصر واحد:

$this->product->name

علاقة تعيد عدة Models

مثل:

hasMany
belongsToMany

تعيد Collection.

وبالتالي يجب التعامل معها كقائمة:

Resource::collection(...)

أو:

map(...)

تحميل العلاقات باستخدام Eager Loading

قبل استخدام العلاقات داخل API Resource يفضل تحميلها مسبقًا من Controller أو Query.

مثلًا:

$orders = UserCart::query()
    ->with('userCartItems')
    ->get();

بهذا يقوم Laravel بتحميل علاقة:

userCartItems

قبل تحويل البيانات إلى Resources.

تحميل العلاقات المتداخلة Nested Relationships

في مثالنا، لا نحتاج إلى:

userCartItems

فقط، بل نحتاج أيضًا إلى Product الخاص بكل Item.

يمكن استخدام Nested Eager Loading:

UserCart::query()
    ->with('userCartItems.product')
    ->get();

وهذا يعني:

UserCart
    └── userCartItems
            └── product

وهذه الصيغة أبسط وأوضح عندما لا نحتاج إلى تخصيص Query لكل Relationship.

الحل الأول: إنشاء Resource مستقل للعلاقة

هذه هي الطريقة التي يفضل استخدامها غالبًا عند التعامل مع Nested Resources.

بدل أن نجعل:

UserOrderResource

مسؤولًا عن تنظيم كل التفاصيل، ننشئ Resource آخر خاص بعناصر السلة.

مثل:

UserCartItemResource

إنشاء UserCartItemResource

نستخدم:

php artisan make:resource UserCartItemResource

ثم:

<?php

namespace App\Http\Resources;

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

class UserCartItemResource extends JsonResource
{
    public function toArray(
        Request $request
    ): array {

        return [
            'id' => $this->id,

            'product_name' =>
                $this->product?->name,

            'price' => $this->price,

            'qty' => $this->qty,
        ];
    }
}

هذا Resource أصبح مسؤولًا عن تحويل:

UserCartItem

واحدة فقط.

استخدام whenLoaded داخل Parent Resource

داخل:

UserOrderResource

نستخدم:

UserCartItemResource::collection(
    $this->whenLoaded('userCartItems')
)

ليصبح Resource:

<?php

namespace App\Http\Resources;

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

class UserOrderResource extends JsonResource
{
    public function toArray(
        Request $request
    ): array {

        return [
            'id' => $this->id,

            'payment_method' =>
                $this->payment_method,

            'received_type' =>
                $this->received_type,

            'received_location' =>
                $this->received_location,

            'status' => $this->status,

            'created_at' =>
                $this->created_at,

            'items' =>
                UserCartItemResource::collection(
                    $this->whenLoaded(
                        'userCartItems'
                    )
                ),
        ];
    }
}

الدالة:

whenLoaded()

لا تقوم بتحميل Relationship بنفسها.

بل تتحقق من أن Relationship تم تحميلها مسبقًا.

إذا كانت العلاقة محملة، يتم تضمينها في Response.

أما إذا لم يتم تحميلها، فيمكن لـLaravel حذف هذا الحقل من Resource Response.

هذه الطريقة تجعل Controller مسؤولًا عن تحديد العلاقات المطلوبة، وResource مسؤولًا عن طريقة عرضها.

استخدام Resource مستقل للمنتج أيضًا

إذا كنا نريد إرجاع أكثر من معلومة عن المنتج، فمن الأفضل عدم وضع كل بيانات Product يدويًا داخل:

UserCartItemResource

يمكن إنشاء:

ProductResource

مثل:

php artisan make:resource ProductResource

ثم:

class ProductResource extends JsonResource
{
    public function toArray(
        Request $request
    ): array {

        return [
            'id' => $this->id,
            'name' => $this->name,
        ];
    }
}

ويصبح UserCartItemResource:

class UserCartItemResource extends JsonResource
{
    public function toArray(
        Request $request
    ): array {

        return [
            'id' => $this->id,

            'price' => $this->price,

            'qty' => $this->qty,

            'product' =>
                new ProductResource(
                    $this->whenLoaded('product')
                ),
        ];
    }
}

وبذلك يصبح لدينا Nested Resources منظمة:

UserOrderResource
       |
       v
UserCartItemResource
       |
       v
ProductResource

الحل الثاني: استخدام map()

يمكن أيضًا التعامل مع Collection مباشرة باستخدام:

map()

مثلًا:

public function toArray(
    Request $request
): array {

    return [
        'id' => $this->id,

        'payment_method' =>
            $this->payment_method,

        'received_type' =>
            $this->received_type,

        'received_location' =>
            $this->received_location,

        'status' =>
            $this->status,

        'created_at' =>
            $this->created_at,

        'items' =>
            $this->userCartItems
                ->map(function ($item) {

                    return [
                        'product_name' =>
                            $item->product?->name,

                        'price' =>
                            $item->price,

                        'qty' =>
                            $item->qty,
                    ];

                })
                ->values()
                ->all(),
    ];
}

هنا:

map()

تمر على كل UserCartItem بشكل منفصل.

في كل دورة تكون:

$item

عبارة عن Model واحد، ولذلك يمكن استخدام:

$item->product
$item->price
$item->qty

أي الطريقتين أفضل؟

كلتا الطريقتين تعملان، لكن في معظم APIs المتوسطة والكبيرة يفضل استخدام:

Nested API Resources

أي:

UserCartItemResource::collection(...)

بدل وضع كل التحويلات داخل Parent Resource.

السبب أن Resources المنفصلة توفر:

  • كودًا أكثر تنظيمًا.
  • إعادة استخدام Resource في أكثر من Endpoint.
  • سهولة تعديل شكل Response.
  • فصل مسؤوليات كل Resource.
  • سهولة التعامل مع Nested Relationships.
  • دعم أفضل لـwhenLoaded.

أما map() فتكون مناسبة إذا كانت العملية صغيرة جدًا أو Transformation خاصة بهذا Endpoint فقط.

تجنب مشكلة N+1

من الأخطاء المهمة أن نكتب داخل Resource:

$this->product->name

بدون التأكد من تحميل Product مسبقًا.

إذا كانت لدينا 100 Cart Items، فقد يؤدي Lazy Loading إلى Queries إضافية لكل عنصر.

لذلك نستخدم:

with('userCartItems.product')

قبل إنشاء Resources.

مثل:

$orders = UserCart::query()
    ->with([
        'userCartItems.product',
    ])
    ->where(
        'user_id',
        $userId
    )
    ->get();

return UserOrderResource::collection(
    $orders
);

Eager Loading من الأدوات الأساسية لتجنب مشكلة N+1 عند التعامل مع Eloquent Relationships.

اختيار Columns مع العلاقات

يمكن تقليل البيانات التي يتم تحميلها من قاعدة البيانات عندما لا نحتاج إلى كل Columns.

مثلًا:

$orders = UserCart::query()
    ->select([
        'id',
        'user_id',
        'payment_method',
        'received_type',
        'received_location',
        'status',
        'created_at',
    ])
    ->with([
        'userCartItems' =>
            function ($query) {

                $query->select([
                    'id',
                    'user_cart_id',
                    'product_id',
                    'price',
                    'qty',
                ])
                ->with([
                    'product:id,name',
                ]);
            },
    ])
    ->where(
        'user_id',
        $userId
    )
    ->get();

هناك نقطة مهمة عند تحديد Columns داخل Relationship:

يجب الاحتفاظ بالمفاتيح التي يحتاجها Eloquent لبناء العلاقات.

في هذا المثال نحتاج داخل UserCartItem إلى:

user_cart_id
product_id

لأنهما يستخدمان لربط Models ببعضها.

إذا حذفت Foreign Keys المطلوبة من select() فقد لا يتمكن Eloquent من ربط النتائج بالشكل المتوقع.

التعامل مع Product غير موجود

قد تكون قاعدة البيانات مصممة بحيث يمكن أن يكون Product محذوفًا أو العلاقة غير موجودة.

بدل:

$item->product->name

يمكن استخدام Null-safe Operator:

$item->product?->name

وبذلك إذا لم يوجد Product تصبح القيمة:

null

بدل ظهور Exception بسبب محاولة قراءة Property من null.

لكن هذا لا يغني عن تصميم Database Constraints وعلاقات الحذف بصورة صحيحة.

تحسين أسماء الحقول في API

في الكود القديم كانت هناك أسماء مثل:

payament_method
recivedType
reciveLocation

ويفضل تصحيحها وجعل API Contract متناسقًا.

مثل:

payment_method
received_type
received_location

واستخدام Naming Convention واحدة في جميع Responses.

مثل Snake Case:

{
    "payment_method": 1,
    "received_type": 1,
    "received_location": 1
}

أو Camel Case إذا كان هذا هو Standard الخاص بالمشروع.

المهم هو الاتساق وعدم تغيير الأسماء عشوائيًا بين Endpoints.

مثال متكامل بالطريقة الموصى بها

علاقة UserCart

class UserCart extends Model
{
    public function userCartItems()
    {
        return $this->hasMany(
            UserCartItem::class
        );
    }
}

علاقة UserCartItem

class UserCartItem extends Model
{
    public function product()
    {
        return $this->belongsTo(
            Product::class
        );
    }
}

ProductResource

<?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,
            'name' => $this->name,
        ];
    }
}

UserCartItemResource

<?php

namespace App\Http\Resources;

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

class UserCartItemResource extends JsonResource
{
    public function toArray(
        Request $request
    ): array {

        return [
            'id' => $this->id,

            'price' => $this->price,

            'qty' => $this->qty,

            'product' =>
                new ProductResource(
                    $this->whenLoaded(
                        'product'
                    )
                ),
        ];
    }
}

UserOrderResource

<?php

namespace App\Http\Resources;

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

class UserOrderResource extends JsonResource
{
    public function toArray(
        Request $request
    ): array {

        return [
            'id' => $this->id,

            'payment_method' =>
                $this->payment_method,

            'received_type' =>
                $this->received_type,

            'received_location' =>
                $this->received_location,

            'status' =>
                $this->status,

            'created_at' =>
                $this->created_at,

            'items' =>
                UserCartItemResource::collection(
                    $this->whenLoaded(
                        'userCartItems'
                    )
                ),
        ];
    }
}

Controller

public function myOrders(
    int $userId
) {
    $orders = UserCart::query()
        ->with([
            'userCartItems.product',
        ])
        ->where(
            'user_id',
            $userId
        )
        ->latest()
        ->get();

    return UserOrderResource::collection(
        $orders
    );
}

بهذه الطريقة يقوم Controller بتحميل العلاقات المطلوبة، بينما تتولى Resources تنظيم JSON.

شكل JSON النهائي

يمكن أن تكون النتيجة:

{
    "data": [
        {
            "id": 44,
            "payment_method": 1,
            "received_type": 1,
            "received_location": 1,
            "status": 0,
            "created_at": "2026-08-30T10:30:00.000000Z",

            "items": [
                {
                    "id": 3,
                    "price": 2.5,
                    "qty": 2,

                    "product": {
                        "id": 2,
                        "name": "Product A"
                    }
                },
                {
                    "id": 4,
                    "price": 10,
                    "qty": 2,

                    "product": {
                        "id": 1,
                        "name": "Product B"
                    }
                }
            ]
        }
    ]
}

وهكذا أصبحت العلاقة المتعددة عبارة عن Array منظمة من Resources.

أفضل الممارسات عند استخدام العلاقات داخل API Resources

استخدم Resource لكل Entity مهمة

بدل وضع جميع البيانات داخل Resource واحد ضخم، استخدم:

OrderResource
OrderItemResource
ProductResource

استخدم whenLoaded مع العلاقات

مثل:

ProductResource::collection(
    $this->whenLoaded('products')
)

هذه الطريقة تجعل العلاقة تظهر عندما تكون محملة فقط. Laravel صمم `whenLoaded()` لهذا السيناريو تحديدًا.

حمّل العلاقات من Query

استخدم:

with()

بدل الاعتماد على Lazy Loading داخل Resource.

استخدم Nested Eager Loading

مثل:

with('items.product')

عندما يحتاج Child Resource إلى علاقة إضافية.

لا تتعامل مع hasMany كأنها Model

الخطأ:

$this->items->product

والصحيح إما:

ItemResource::collection(
    $this->whenLoaded('items')
)

أو المرور على Collection باستخدام:

map()

استخدم Resources بدل إرجاع Models كاملة

هذا يمنحك تحكمًا أفضل في API Contract ويمنع إرسال Columns لا يحتاجها Client.

انتبه إلى N+1 Queries

إذا كنت تستخدم Relationship داخل Loop أو Resource Collection، تأكد من تحميلها مسبقًا.

حافظ على Response Structure ثابتًا

Frontend يجب أن يعرف أن:

items

دائمًا Array، وأن:

product

Object أو null حسب API Contract.

ملخص المقال

المفهومالاستخدام
hasManyعلاقة تعيد Collection من Models.
belongsToعلاقة تعيد Model واحدًا عادةً.
Resource::collection()تحويل مجموعة Models إلى مجموعة Resources.
whenLoaded()إضافة Relationship إلى Resource عندما تكون محملة.
with()تحميل Eloquent Relationships مسبقًا.
Nested Eager Loadingتحميل علاقة داخل علاقة مثل items.product.
map()تحويل كل عنصر في Collection يدويًا.
N+1مشكلة Queries إضافية تنتج غالبًا عن Lazy Loading المتكرر.
Nested Resourcesاستخدام Resource داخل Resource لتنظيم العلاقات.

الخلاصة

عند التعامل مع علاقات Eloquent داخل Laravel API Resources، يجب أولًا معرفة نوع العلاقة التي نتعامل معها.

إذا كانت العلاقة مثل:

belongsTo
hasOne

فنحن نتعامل عادةً مع Model واحد.

أما إذا كانت:

hasMany
belongsToMany

فنحن نتعامل مع Collection تحتوي على عدة Models.

ولهذا ظهر الخطأ:

Property [product] does not exist on this collection instance.

لأن الكود حاول الوصول إلى:

$this->userCartItems->product

بينما userCartItems Collection وليست UserCartItem واحدة.

الحل الأفضل في معظم الحالات هو إنشاء Resource خاص بكل عنصر:

UserCartItemResource

ثم تمرير العلاقة باستخدام:

UserCartItemResource::collection(
    $this->whenLoaded('userCartItems')
)

وإذا كان كل Item يحتوي على Product، يمكن استخدام Resource إضافي:

ProductResource

وبهذه الطريقة يصبح لدينا API Response منظم وسهل الصيانة وإعادة الاستخدام.

كما يجب تحميل العلاقات مسبقًا باستخدام:

with('userCartItems.product')

حتى نتجنب مشكلة N+1 ونفصل مسؤولية تحميل البيانات عن مسؤولية تحويلها إلى JSON.