المرحلة 03 · الكتالوج والتجارةدرس 18 من 3419 دقيقة قراءةآخر تحديث:
Lesson 18 / 34
Quote · Cart · Addressesالمرجع الكامل.
كل حاجة قبل الأوردر بتعيش في الـ Quote. هنفكّك الكيانات وعلاقتها ببعض، وجداولها في قاعدة البيانات، والميثودز المهمة في كل واحد — كل ميثود بتعمل إيه وامتى تستخدمها — وبعدين التدفّقات كاملة.
● مرجعجداول ميثودزتدفّقات كاملةفخاخ شائعة
الإجابة المختصرة
كل حاجة قبل الأوردر في Magento 2 بتعيش في الـ Quote: الأصناف في quote_item، والعناوين في quote_address، وطريقة الدفع في quote_payment. أي تعديل لازم يتبعه collectTotals وحفظ بـ CartRepositoryInterface. للعرض استخدم getAllVisibleItems وللمخزون getAllItems، وسلال الضيوف بتتعامل بالمعرّف المقنّع من quote_id_mask، ولحظة placeOrder الشجرة كلها بتتنسخ لـ Order.
// إزاي تستخدمه
ده مرجع مش قصة — مش مطلوب تحفظه. اقرا القسم 01 و02 الأول عشان تفهم الخريطة، وبعدها ارجع لجداول الميثودز وقت الحاجة. الفخاخ في القسم 13 اقراها دلوقتي حتى لو مش هتقرا الباقي، لأنها بتوفّر ساعات debugging.
◆ Part 1 — الخريطة
01
الكيانات وعلاقتها
شوف الشكل الكبير الأول — كل الباقي بيتفرّع منه.
الجذر
Quote
│
أبناء مباشرين
Quote Item
Quote Address
Quote Payment
│
تفاصيل
Item Option
Address Item
Shipping Rate
الفكرة الجوهرية
الـ Quote هو السلة. كل حاجة تانية بتعيش جوّاه: المنتجات (items)، العناوين (addresses)، وطريقة الدفع (payment). لحظة ما العميل يأكّد، الشجرة دي كلها بتتنسخ لشجرة موازية اسمها Order.
Quote Address — عنوان. نوعه إما billing أو shipping.
Quote Payment — طريقة الدفع المختارة وبياناتها.
Item Option — خيارات المنتج (المقاس، اللون، الـ buy request).
Address Item — ربط الصنف بالعنوان (بيستخدم في الشحن لأكتر من عنوان).
Shipping Rate — أسعار الشحن المتاحة للعنوان ده.
تشبيه
الـ Quote زي طلب في مطعم قبل ما يتأكّد. الأصناف (items) ممكن تزوّد وتشيل، والترابيزة (address) ممكن تتغيّر، وطريقة الدفع لسه مفتوحة. لما تقول "أكّد"، الطلب بيتحوّل لفاتورة (Order) وبقى ثابت.
!
فرق بيلخبط كتير: الـ Quote Address مش هو الـ Customer Address. الأول نسخة داخل السلة، والتاني اللي محفوظ في دفتر عناوين العميل. لما العميل يختار عنوان محفوظ، بيتنسخ للـ quote address — فلو عدّل عنوانه في حسابه بعدين، السلة مابتتغيّرش. وده مقصود.
• • •
02
الجداول في قاعدة البيانات
مفيد جداً وقت الـ debugging — تعرف تدوّر فين.
table
quote
السلة نفسها — العميل، المتجر، العملة، الإجماليات، وحالة النشاط.
دوّر هنالما تعرف الـ quote id وعايز تشوف حالتها الكلية.
quote_item
أسطر المنتجات. كل صف = منتج بكمية وسعر.
دوّر هنالما منتج مش ظاهر في السلة أو سعره غلط.
quote_item_option
خيارات كل صنف، ومنها الـ info_buyRequest اللي بيحفظ طلب الإضافة الأصلي.
دوّر هنالما خيارات المنتج (مقاس/لون) مش متسجّلة صح.
quote_address
العناوين. كل quote ليها صفّين عادة: billing و shipping.
دوّر هنالما الشحن أو الضريبة بتتحسب غلط — الاتنين بيعتمدوا على العنوان.
quote_address_item
ربط الأصناف بالعناوين. بيمتلي في الشحن لأكتر من عنوان.
دوّر هنافي حالات الـ multi-shipping بس.
quote_payment
طريقة الدفع المختارة وبياناتها الإضافية.
دوّر هنالما طريقة دفع مش بتتحفظ أو مش ظاهرة.
quote_shipping_rate
أسعار الشحن المحسوبة للعنوان — بتتولّد وقت الحساب.
دوّر هنالما طريقة شحن مش ظاهرة للعميل.
quote_id_mask
بيربط الـ quote id الحقيقي بمعرّف مقنّع للضيوف.
دوّر هنالما تشتغل على سلة ضيف من API أو تطبيق موبايل.
i
الـ quote_id_mask موجود عشان الأمان. لو الـ API كان بيستخدم الـ id الرقمي المتسلسل، أي حد يقدر يجرّب أرقام ويوصل لسلال غيره. المعرّف المقنّع عشوائي فمينفعش يتخمّن.
• • •
◆ Part 2 — الميثودز
03
Quote — الميثودز
Magento\Quote\Model\Quote
method
getAllItems()
كل الأصناف بما فيها الأبناء — لمنتج قابل للتكوين بترجّع الأب والابن.
امتىلما تحتاج المنتج الفعلي اللي هيتشحن أو يتخصم من المخزون.
getAllVisibleItems()
الأصناف اللي العميل شايفها بس — الآباء من غير الأبناء.
امتىلما تعرض السلة أو تعدّ الأصناف للعميل. ده اللي هتحتاجه في 90% من الحالات.
addProduct($product, $request = null)
بيضيف منتج للسلة. الـ $request بيحمل الكمية والخيارات.
امتىالإضافة برمجياً. بيرجّع الصنف أو نص الخطأ — لازم تشيك على النتيجة.
getItemById($itemId)
بيجيب صنف بالـ id بتاعه من السلة.
امتىتعديل كمية أو حذف صنف محدد.
getItemByProduct($product)
بيدوّر على صنف بالمنتج نفسه.
امتىتشيك إذا كان المنتج موجود في السلة قبل ما تضيفه.
removeItem($itemId)
بيشيل صنف من السلة.
امتىالحذف برمجياً. لازم collectTotals() بعده.
getItemsCount()
عدد الأسطر في السلة (مش مجموع الكميات).
امتىعرض "عندك 3 منتجات".
getItemsQty()
مجموع الكميات كلها.
امتىعرض "عندك 7 قطع" أو حساب على أساس الكمية.
hasItems()
هل السلة فيها حاجة؟
امتىقبل أي عملية checkout.
removeAllItems()
بيفضّي السلة بالكامل.
امتىإعادة تعيين السلة، أو بعد تحويلها لأوردر.
!
الفرق بين getAllItems وgetAllVisibleItems هو أكتر مصدر لأخطاء العد والأسعار. لو حسبت إجمالي على getAllItems مع منتجات قابلة للتكوين، هتحسب السعر مرتين — مرة للأب ومرة للابن.
method
getBillingAddress()
عنوان الفواتير. بيرجّع كائن فاضي لو لسه ماتحددش.
امتىقراءة أو تعديل بيانات الفوترة.
getShippingAddress()
عنوان الشحن — وهو اللي الشحن والضريبة بيتحسبوا عليه.
امتىأي حاجة ليها علاقة بالشحن أو الضريبة.
getAllAddresses()
كل العناوين المرتبطة بالـ quote.
امتىحالات الشحن لأكتر من عنوان.
isVirtual()
هل السلة كلها منتجات مالهاش شحن (رقمية/خدمات)؟
امتىقبل ما تطلب عنوان شحن — السلة الافتراضية مالهاش عنوان شحن أصلاً.
setCustomer($customer)
بيربط السلة بعميل.
امتىتحويل سلة ضيف لسلة عميل بعد تسجيل الدخول.
getCustomerId()
id العميل، أو null لو ضيف.
امتىتفرّق بين سلة ضيف وسلة عميل مسجّل.
getCustomerEmail()
إيميل العميل — مطلوب حتى للضيوف.
امتىالتحقق قبل إنشاء الأوردر.
merge($quote)
بيدمج سلة تانية في الحالية.
امتىالعميل عنده سلة قديمة محفوظة وسجّل دخول بسلة ضيف جديدة.
method
collectTotals()
بيشغّل سلسلة الـ collectors ويعيد حساب كل الإجماليات.
امتىبعد أي تغيير في الأصناف أو العناوين أو الكوبونات. من غيرها الأرقام بتفضل قديمة.
getGrandTotal()
الإجمالي النهائي بعملة العرض.
امتىالعرض للعميل.
getBaseGrandTotal()
الإجمالي النهائي بالعملة الأساسية للمتجر.
امتىالتقارير والمحاسبة والمقارنات. استخدم الـ base دايماً في المنطق.
getSubtotal()
مجموع الأصناف قبل الشحن والضريبة والخصم.
امتىعرض تفصيل الفاتورة.
getTotals()
مصفوفة بكل الإجماليات المحسوبة ومسمّياتها.
امتىعرض جدول الإجماليات بالكامل.
setIsActive($flag)
بيفعّل أو يعطّل السلة.
امتىبيتعمل false تلقائياً بعد تحويلها لأوردر.
reserveOrderId()
بيحجز رقم أوردر للسلة دي.
امتىقبل إنشاء الأوردر — وبيستخدم كتير مع بوابات الدفع اللي محتاجة مرجع مسبق.
save()
بيحفظ السلة مباشرة.
تجنّبهاستخدم CartRepositoryInterface::save() بدلاً منه — الطريقة المدعومة رسمياً.
!
قاعدة مهمة: استخدم الـ base في أي منطق حسابي، والعادي في العرض بس. لو المتجر بيبيع بأكتر من عملة، مقارنة إجماليين بعملات مختلفة هتديك نتيجة غلط من غير أي خطأ ظاهر.
• • •
04
Quote Item — الميثودز
Magento\Quote\Model\Quote\Item
method
getProduct()
كائن المنتج المرتبط بالصنف.
امتىتحتاج بيانات المنتج نفسه (خصائص، صور، نوع).
getQty() / setQty($qty)
الكمية.
امتىتعديل الكمية — وcollectTotals() بعدها.
getPrice() / getBasePrice()
سعر الوحدة بعملة العرض / بالعملة الأساسية.
امتىالعرض / المنطق الحسابي.
setCustomPrice($price)
بيفرض سعر مخصّص على الصنف ويتجاهل سعر الكتالوج.
امتىتسعير خاص أو عروض مخصّصة. لازم تحدد setOriginalCustomPrice() كمان وإلا السعر بيترجع مع أول إعادة حساب.
getRowTotal() / getBaseRowTotal()
إجمالي السطر (السعر × الكمية).
امتىعرض تفصيل السلة.
getParentItem()
الصنف الأب لو ده ابن (في المنتجات المركّبة).
امتىتفرّق بين الأب والابن وأنت بتلف على الأصناف.
getChildren()
الأبناء لو ده أب.
امتىتحتاج المكوّنات الفعلية لمنتج مركّب.
getOptionByCode($code)
بيجيب خيار محدد بالكود بتاعه.
امتىقراءة info_buyRequest عشان تعرف العميل طلب إيه بالظبط.
addOption($option)
بيضيف خيار للصنف.
امتىتخزين بيانات مخصّصة مع الصنف.
setNoDiscount($flag)
بيستثني الصنف من الخصومات.
امتىمنتجات مستثناة من العروض.
الـ buy request
أهم خيار هو info_buyRequest — بيحفظ طلب الإضافة الأصلي كامل (الكمية، الخيارات المختارة، أي بيانات مخصّصة). ده اللي Magento بيستخدمه لما يعيد بناء الصنف، وهو أول مكان تبص فيه لما خيارات المنتج تطلع غلط.
امتىتعرف إذا كان العنوان من الدفتر ولا مكتوب يدوي.
importCustomerAddressData($address)
بينسخ بيانات عنوان العميل جوّه عنوان الـ quote.
امتىالعميل يختار عنوان محفوظ.
getStreetFull() / setStreetFull()
الشارع كنص واحد (Magento بيخزّنه كأسطر متعددة).
امتىالعرض أو التعامل مع أنظمة خارجية بتستقبل سطر واحد.
validate()
بيتحقق من اكتمال العنوان. بيرجّع true أو مصفوفة أخطاء.
امتىقبل الشحن أو إنشاء الأوردر.
getShippingMethod()
طريقة الشحن المختارة بصيغة carrier_method.
امتىقراءة اختيار العميل.
setShippingMethod($code)
بيحدد طريقة الشحن.
امتىتحديد الشحن برمجياً — وcollectTotals() بعدها.
getShippingAmount() / getBaseShippingAmount()
تكلفة الشحن المحسوبة.
امتىالعرض / المنطق.
collectShippingRates()
بيعيد حساب أسعار الشحن المتاحة للعنوان.
امتىبعد تغيير العنوان أو محتويات السلة.
getGroupedAllShippingRates()
الأسعار المتاحة مجمّعة حسب شركة الشحن.
امتىعرض اختيارات الشحن للعميل.
getAllVisibleItems()
الأصناف المرتبطة بالعنوان ده.
امتىحساب شحن مبني على محتوى كل عنوان.
!
الـ country_id وpostcode وregion هم اللي الشحن والضريبة بيتحسبوا عليهم. لو طريقة شحن مش ظاهرة، ابدأ بالتأكد من اكتمال العنوان قبل ما تفتح كود الـ carrier — دي أشهر حالة بيضيع فيها وقت.
• • •
06
Payment و Shipping Rates
الكيانان المكمّلان للصورة.
method
$quote->getPayment()
كائن الدفع المرتبط بالسلة.
امتىنقطة البداية لأي حاجة ليها علاقة بالدفع.
getMethod() / setMethod($code)
كود طريقة الدفع.
امتىقراءة أو تحديد طريقة الدفع.
getAdditionalInformation($key = null)
بيانات إضافية للبوابة (توكن، مرجع، تفاصيل).
امتىتمرير بيانات من الواجهة لبوابة الدفع.
setAdditionalInformation($key, $value)
بيخزّن بيانات إضافية مع الدفع.
امتىحفظ توكن الدفع قبل إنشاء الأوردر.
$quote->setPaymentMethod($code)
اختصار لتحديد طريقة الدفع على مستوى السلة.
امتىالتحديد البرمجي السريع.
!
متخزّنش بيانات كروت أبداً في additional_information ولا في أي مكان تاني. خزّن التوكن اللي البوابة بتديهولك بس — ده شرط أساسي في الـ PCI compliance، وتخزين رقم كارت واحد بيحوّل المشروع كله لمسؤولية قانونية.
إزاي بتتولّد
لما العنوان يتحدد، Magento بينده كل شركات الشحن المفعّلة. كل واحدة بترجّع الأسعار المتاحة (أو ترفض)، والنتيجة بتتخزّن في quote_shipping_rate.
العنوان اتحدد
→
نداء الـ carriers
→
أسعار متاحة
→
العميل يختار
method
getCode()
كود السعر بصيغة carrier_method.
امتىده اللي بيتحفظ كطريقة شحن مختارة.
getCarrierTitle() / getMethodTitle()
الأسماء المعروضة للعميل.
امتىعرض اختيارات الشحن.
getPrice()
سعر الشحن لهذا الخيار.
امتىالعرض والمقارنة.
getErrorMessage()
سبب رفض شركة الشحن، لو رفضت.
امتىأول حاجة تبص عليها لما طريقة شحن مش ظاهرة.
• • •
07
Service Contracts — الطريقة الحديثة
دي اللي المفروض تستخدمها في الكود الجديد، مش الموديلات مباشرة.
getList() · save() · deleteById() — إدارة أصناف السلة.
امتىإضافة وتعديل وحذف الأصناف من API.
ShippingAddressManagementInterface
assign() · get() — عنوان الشحن.
امتىتحديد عنوان الشحن من الواجهة أو الـ API.
BillingAddressManagementInterface
assign() · get() — عنوان الفواتير.
امتىتحديد عنوان الفوترة.
PaymentMethodManagementInterface
set() · get() · getList() — طرق الدفع.
امتىعرض وتحديد طريقة الدفع المتاحة.
ShipmentEstimationInterface
estimateByExtendedAddress() — تقدير الشحن قبل تحديد العنوان كامل.
امتىعرض تقدير شحن في السلة قبل الـ checkout.
CartTotalRepositoryInterface
get($cartId) — الإجماليات المفصّلة.
امتىعرض الإجماليات في تطبيق موبايل أو واجهة headless.
GuestCart* (نسخ الضيوف)
نفس الواجهات بس بتاخد المعرّف المقنّع بدل الـ id الرقمي.
امتىأي عملية ضيف من API — وده اللي تطبيق الموبايل بيستخدمه.
i
لو بتبني API لتطبيق موبايل، الـ GuestCart هي اللي هتشتغل بيها مع غير المسجّلين. الفرق الوحيد إنها بتاخد المعرّف المقنّع من quote_id_mask بدل الـ id الحقيقي.
• • •
08
Session و Cart و الضيوف
إزاي توصل للسلة الحالية في سياق الواجهة.
Magento\Checkout\Model\Session — بوابتك للسلة الحالية في الواجهة.
method
getQuote()
سلة الزائر الحالي. بينشئ واحدة لو مفيش.
امتىالطريقة الأساسية للوصول للسلة في blocks و controllers.
getQuoteId() / setQuoteId($id)
id السلة في الجلسة.
امتىتبديل السلة أو التحقق منها.
clearQuote()
بيفصل السلة عن الجلسة.
امتىبعد إنشاء الأوردر.
getLastRealOrder()
آخر أوردر اتعمل في الجلسة.
امتىصفحة "تم الطلب بنجاح".
replaceQuote($quote)
بيستبدل سلة الجلسة بسلة تانية.
امتىدمج سلال أو استعادة سلة محفوظة.
!
الـ Checkout Session موجود في سياق الواجهة بس. في CLI أو cron أو consumer مفيش جلسة — لازم تحمّل السلة بالـ repository بالـ id. ده سبب متكرر لأخطاء "call on null" في كود الـ background.
عميل مسجّل
الـ API بياخد الـ id الرقمي مباشرة، والتوكن بيحدد مين هو.
ضيف
الـ API بياخد معرّف مقنّع عشوائي من quote_id_mask.
التحويل بين الاتنينphp
// من المقنّع للحقيقيuseMagento\Quote\Model\MaskedQuoteIdToQuoteIdInterface;
$quoteId = $this->maskedToQuoteId->execute($maskedId);
// من الحقيقي للمقنّعuseMagento\Quote\Model\QuoteIdToMaskedQuoteIdInterface;
$masked = $this->quoteIdToMasked->execute($quoteId);
!
متعرّضش الـ id الرقمي للضيوف أبداً. لو عرضته في response أو URL، أي حد يقدر يجرّب أرقام متتالية ويوصل لسلال غيره — وده تسريب بيانات فعلي.
الـ addProduct() بيرجّع نص الخطأ لو فشل، مش استثناء. لو مشيّكتش بـ is_string()، الكود هيكمّل كأن كل حاجة تمام والصنف مش هيتضاف — وده بيطلع كـ "الإضافة مابتشتغلش أحياناً" من غير أي خطأ في الـ log.
• • •
10
تدفّق: العناوين والشحن
أكتر جزء بتحصل فيه مشاكل.
العنوان
→
carriers
→
أسعار
→
الاختيار
→
totals
العميل يدخل العنوان أو يختار واحد محفوظ.
لو من الدفتر، بيتنسخ بـ importCustomerAddressData() — نسخة مش ربط.
بيتنده collectShippingRates() فالنظام بينده كل شركات الشحن.
كل شركة بترجّع أسعارها أو ترفض بسبب (وزن، دولة، منطقة غير مغطاة).
الأسعار بتتخزّن في quote_shipping_rate وبتتعرض.
العميل يختار، فبيتنده setShippingMethod().
collectTotals() بيضيف تكلفة الشحن للإجمالي.
لما طريقة شحن مش ظاهرة
ابدأ بالترتيب ده — من الأرخص للأغلى: العنوان كامل؟ (دولة، منطقة، رمز بريدي) ← الشركة مفعّلة للدولة دي؟ ← الوزن في المدى المسموح؟ ← الـ getErrorMessage() بيقول إيه؟
i
الـ getErrorMessage() على الـ rate هو أسرع طريق للإجابة، ومعظم الناس بتفتح كود الـ carrier قبل ما تقراه.
• • •
11
تدفّق: حساب الـ Totals
إزاي الأرقام بتتحسب، وليه الترتيب مهم.
الفكرة
الإجماليات مش بتتحسب في مكان واحد. فيه سلسلة collectors، كل واحد مسؤول عن جزء، وبيشتغلوا بترتيب محدد بـ sort_order في sales.xml.
subtotal
→
shipping
→
discount
→
tax
→
grand total
ليه الترتيب مهم
الضريبة بتتحسب على المبلغ بعد الخصم. لو اتحسبت قبله، العميل هيدفع ضريبة على فلوس ماخدهاش — وده خطأ محاسبي بيظهر في التدقيق مش في الـ logs.
إضافة رسم مخصّص
etc/sales.xmlxml
<!-- collector جديد بين الخصم والضريبة -->
<section name="quote">
<group name="totals">
<item name="custom_fee"
instance="Vendor\Module\Model\Total\CustomFee"
sort_order="350"/>
</group>
</section>
!
الـ collectTotals() بيتنده كتير جداً أثناء الـ checkout. لو حطيت منطق تقيل في collector مخصّص (نداء API خارجي مثلاً)، هتبطّئ الـ checkout كله. خلّي الـ collectors خفيفة، وأي حاجة تقيلة اعملها مرة وخزّنها.
• • •
12
تدفّق: Quote إلى Order
اللحظة الحاسمة — من مؤقت لدائم.
Quote
→
تحقّق
→
نسخ
→
دفع
→
Order
placeOrder($cartId) بيتنده.
تحقّق نهائي — السلة فيها أصناف؟ العناوين كاملة؟ طريقة الدفع محددة؟
أحداث بتتطلق زي sales_order_place_after للـ observers.
كلاسات التحويل
Magento بيستخدم كلاسات مخصّصة للنسخ — ToOrder، ToOrderAddress، ToOrderItem، ToOrderPayment. لو عايز تنقل بيانات مخصّصة من السلة للأوردر، دي نقطة التدخّل الصح (بـ plugin عليها).
ليه الـ Order flat مش EAV
الأوردرات بنيتها ثابتة وبتتكتب كتير وبتتقرا في التقارير. الـ flat أسرع بكتير للحالة دي. الـ EAV مفيد للمنتجات لأن خصائصها بتختلف — الأوردر مش محتاج المرونة دي.
!
بعد التحويل، الأوردر مستقل تماماً عن السلة. لو عدّلت السلة بعدين (نظرياً)، الأوردر مابيتغيّرش — وده مقصود، لأن الأوردر وثيقة قانونية لازم تفضل زي ما هي لحظة الشراء.
• • •
◆ Part 4 — الخلاصة
13
الفخاخ الشائعة
اقرا دي حتى لو مش هتقرا حاجة تانية.
نسيان collectTotals() — عدّلت الأصناف والإجماليات فضلت قديمة. أشهر فخ على الإطلاق.
getAllItems بدل getAllVisibleItems — حساب مزدوج مع المنتجات المركّبة.
عدم التشييك على نتيجة addProduct() — بترجّع نص خطأ مش استثناء، فالفشل بيعدي بصمت.
استخدام getGrandTotal بدل getBaseGrandTotal في المنطق — بيكسر المتاجر متعددة العملات.
setCustomPrice من غير setOriginalCustomPrice — السعر بيترجع لسعر الكتالوج مع أول إعادة حساب.
الاعتماد على Checkout Session في cron أو consumer — مفيش جلسة هناك.
تعديل عنوان العميل وتوقّع تغيّر السلة — الـ quote address نسخة مش ربط.
عرض الـ quote id الرقمي للضيوف — تسريب بيانات، استخدم المقنّع.
منطق تقيل في total collector — بيتنده عشرات المرات فبيبطّئ الـ checkout كله.
i
لو واجهت سلوك غريب في السلة، ابدأ بسؤالين: هل اتنده collectTotals() بعد التعديل؟ وهل أنا بستخدم النسخة الصح من ميثود الأصناف؟ دول بيحلّوا أغلب الحالات.
• • •
14
أسئلة الإنترفيو
الأسئلة المتكررة في المجال ده.
Q1إيه الفرق بين Quote و Order؟
الإجابة: الـ Quote سلة مؤقتة بتتغيّر بحرية أثناء الـ checkout. الـ Order وثيقة دائمة بتتعمل لحظة التأكيد بنسخ بيانات الـ Quote. الـ Order flat للسرعة، والـ Quote مصمّم للتعديل المتكرر. وبعد التحويل الاتنين مستقلين تماماً.
Q2إيه الفرق بين getAllItems و getAllVisibleItems؟
الإجابة: الأولى بترجّع كل الأصناف بما فيها الأبناء (في المنتجات المركّبة)، والتانية بترجّع اللي العميل شايفه بس. للعرض والعد استخدم getAllVisibleItems، وللمخزون والشحن استخدم getAllItems. الخلط بينهم بيسبب حساب مزدوج.
Q3الفرق بين Quote Address و Customer Address؟
الإجابة: الـ Customer Address محفوظ في دفتر عناوين العميل. الـ Quote Address نسخة منه داخل السلة. لما العميل يختار عنوان محفوظ، بيتنسخ — فتعديل الدفتر بعدين مابيأثرش على السلة. ده مقصود عشان الأوردر يفضل معبّر عن لحظة الشراء.
Q4إزاي بتتحسب الـ totals؟
الإجابة: بسلسلة total collectors، كل واحد مسؤول عن جزء (subtotal، shipping، discount، tax، grand total) وبيشتغلوا بترتيب sort_order المعرّف في sales.xml. الترتيب مهم — الضريبة لازم بعد الخصم. وتقدر تضيف collector مخصّص بنفس الطريقة.
Q5إزاي تتعامل مع سلة ضيف من API؟
الإجابة: بالـ معرّف المقنّع من جدول quote_id_mask، مش بالـ id الرقمي. فيه نسخ GuestCart من كل الواجهات بتاخد المقنّع. السبب أمني: الـ id الرقمي متسلسل فيتخمّن بسهولة.
Q6سلة العميل بتضيع بعد تسجيل الدخول — ليه؟
الإجابة: غالباً سلة الضيف ماترابطتش بالعميل. لازم assignCustomer() بعد الدخول، أو merge() لو العميل عنده سلة محفوظة من قبل. في تطبيقات الموبايل دي من أشهر المشاكل، لأن الخطوة دي بتتنسي في تدفّق الـ API.
Quote · Cart · Addresses ✓
دلوقتي عندك الخريطة كاملة: الكيانات وعلاقتها، الجداول، الميثودز المهمة في كل طبقة، والتدفّقات الأربعة الأساسية.
الخلاصة اللي تربط كله: كل حاجة قبل الأوردر بتعيش في الـ Quote، وأي تعديل لازم يتبعه collectTotals()، والوصول الصح بيكون عن طريق الـ service contracts مش الموديلات مباشرة.
الـ Quote هو السلة: كيان مؤقت بيتغيّر بحرية أثناء الـ checkout وبيحمل الأصناف والعناوين وطريقة الدفع والإجماليات. الـ Order وثيقة دائمة بتتعمل لحظة التأكيد بنسخ شجرة الـ Quote كلها بكلاسات ToOrder و ToOrderItem و ToOrderAddress و ToOrderPayment. بعد التحويل الاتنين مستقلين تمامًا، والـ Order جداوله flat مش EAV عشان القراءة والتقارير أسرع.
إيه الفرق بين getAllItems و getAllVisibleItems؟
getAllItems() بترجّع كل الأصناف بما فيها الأبناء، فمنتج configurable بيرجّع الأب والابن. getAllVisibleItems() بترجّع اللي العميل شايفه بس. للعرض والعدّ استخدم الـ visible، وللمخزون والشحن استخدم getAllItems مع تخطّي الأصناف اللي ليها أبناء. الخلط بينهم أشهر سبب لحساب السعر أو الكمية مرتين.
ليه الإجماليات مش بتتحدّث بعد ما عدّلت السلة برمجيًا؟
لأن الإجماليات بتتحسب بسلسلة total collectors مابتشتغلش لوحدها. بعد أي تغيير في الأصناف أو العناوين أو الكوبونات أو طريقة الشحن لازم تنادي $quote->collectTotals() وبعدين تحفظ بـ CartRepositoryInterface::save() مش $quote->save(). ده أشهر فخ في التعامل مع الـ Quote على الإطلاق.
إزاي أضيف منتج للسلة برمجيًا في Magento 2؟
هات الـ quote من Checkout Session في الواجهة أو من الـ repository في الخلفية، وحمّل المنتج، وابعت DataObject فيه qty والـ options لـ $quote->addProduct($product, $request). الميثود بترجّع الصنف لو نجحت أو نص الخطأ لو فشلت من غير exception، فلازم تشيّك بـ is_string(). بعدها collectTotals واحفظ بالـ repository.
إزاي أتعامل مع سلة ضيف من الـ API في Magento 2؟
بالمعرّف المقنّع من جدول quote_id_mask مش الـ id الرقمي، وفيه نسخ GuestCart من كل الـ interfaces بتاخده. للتحويل في الكود استخدم MaskedQuoteIdToQuoteIdInterface والعكس. السبب أمني: الـ id الرقمي متسلسل، فلو ظهر في response أو URL أي حد يجرّب أرقام ويوصل لسلال غيره. وبعد تسجيل الدخول اربط السلة بـ assignCustomer وإلا هتضيع.
طريقة الشحن مش ظاهرة في الـ checkout، ليه؟
الشحن والضريبة بيتحسبوا على country_id و region و postcode بتوع الـ shipping address، فاتأكد الأول إن العنوان كامل. بعدها الـ carrier مفعّل للدولة دي؟ والوزن في المدى المسموح؟ وبعدين اقرا getErrorMessage() على الـ rate لأنه بيقول سبب الرفض مباشرة. معظم الناس بتفتح كود الـ carrier قبل ما تقراه.
إزاي أضيف رسم مخصّص على إجماليات السلة في Magento 2؟
بـ total collector مخصّص: كلاس بيورّث من AbstractTotal وبتسجّله في etc/sales.xml جوّه section quote و group totals بـ sort_order بيحدد مكانه بين الـ collectors الأساسية، والـ subtotal 100 والـ shipping 350 والـ discount 400 والـ tax 450. خلّي المنطق خفيف لأن collectTotals بيتنده عشرات المرات في الـ checkout، وأي نداء خارجي فيه بيبطّئ كل حاجة.