كيفية استخدام العلاقات في 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().
جدول المحتويات
- المثال الذي سنعمل عليه
- العلاقات بين Models
- شكل Response المطلوب
- سبب ظهور الخطأ
- الفرق بين hasOne وhasMany داخل Resource
- تحميل العلاقات باستخدام Eager Loading
- تحميل العلاقات المتداخلة
- الحل الأول: Resource مستقل للعلاقة
- إنشاء UserCartItemResource
- استخدام whenLoaded
- إنشاء Resource للمنتج
- الحل الثاني: استخدام map
- أي الطريقتين أفضل؟
- تجنب مشكلة N+1
- اختيار Columns مع العلاقات
- التعامل مع علاقة Product غير موجودة
- تحسين أسماء الحقول
- مثال متكامل
- شكل JSON النهائي
- أفضل الممارسات
- ملخص المقال
- الخلاصة
المثال الذي سنعمل عليه
لنفترض أن لدينا جدولًا يمثل سلة أو طلب المستخدم:
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، وأن:
productObject أو 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.
لو حابب اعمل pagination للداتا وتكون نفس شكل الرسبونس ازاي.؟
جزاكم الله خيراً أخي الكريم وبارك الله فيكم
شكراً لك, على هذا المحتوى الجيد , الى الامام يا اخي
جزاك الله خيراً
شكرا جزيلا على ال9 دروس العظيمة