Product Attributesالمرجع الكامل.
كل حاجة عن خصائص المنتجات — الأنواع والخصائص، الإنشاء من الأدمن ومن الكود، الخيارات والـ source models، القراءة والفلترة، العرض في الواجهة، وأثرها على الأداء.
الـ Product Attributes في Magento 2 متخزّنة بنظام EAV: تعريفها في eav_attribute و catalog_eav_attribute، وقيمها في جداول catalog_product_entity_varchar و _int و _decimal حسب النوع. الخصائص اللي الكود بيعتمد عليها تتعمل بـ Data Patch وEavSetup::addAttribute، والخصائص اللي المحتوى بيديرها من الأدمن. النوع والـ scope قرارات صعب التراجع عنها، وإعدادات الفلترة والبحث ليها تكلفة أداء.
يعني إيه Attribute
الأساس اللي كل حاجة بعده بتبني عليه.
الـ attribute هي خاصية على المنتج — الاسم، السعر، اللون، المقاس، أي حاجة. Magento بيخزّنها بنظام EAV، يعني القيم في جداول منفصلة مش أعمدة في جدول المنتج.
لو كانت أعمدة
كل خاصية جديدة = تعديل على بنية الجدول. ومنتج ملوش لون = عمود فاضي.
مع EAV
خاصية جديدة = صف في جدول التعريف. والقيم بتتخزّن للمنتجات اللي ليها قيمة بس.
مرونة عالية، قراءة أبطأ. جلب منتج كامل بيحتاج ربط جداول متعددة. عشان كده Magento عنده indexers بتحوّل البيانات لجداول مسطّحة جاهزة للقراءة السريعة.
- System attributes — جاية مع Magento (name, price, sku). مش هتقدر تحذفها.
- User-defined — اللي بتعملها إنت. قابلة للتعديل والحذف.
الجداول — فين البيانات
مهم جداً وقت التشخيص.
-- كل إعدادات خاصية معيّنة SELECT a.attribute_id, a.attribute_code, a.backend_type, a.frontend_input, c.is_global, c.is_filterable, c.is_searchable, c.used_in_product_listing FROM eav_attribute a JOIN catalog_eav_attribute c ON c.attribute_id = a.attribute_id WHERE a.attribute_code = 'color';
الأنواع (input types)
أنهي نوع تختار لأي بيانات.
الـ Scope
أنهي مستوى تختلف عنده القيمة.
جداول القيم فيها عمود store_id. القيمة الافتراضية بتتخزّن بـ store_id = 0، والقيم المخصّصة لكل store view بـ id بتاعها. لو مفيش قيمة مخصّصة، Magento بياخد الافتراضية.
use Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface; ScopedAttributeInterface::SCOPE_GLOBAL ScopedAttributeInterface::SCOPE_WEBSITE ScopedAttributeInterface::SCOPE_STORE
Attribute Sets و Groups
إزاي الخصائص بتتنظّم.
مجموعة الخصائص المتاحة لنوع منتج معيّن. مثال: مجموعة "ملابس" فيها اللون والمقاس والخامة، ومجموعة "إلكترونيات" فيها الضمان والقوة.
تبويب داخل صفحة تعديل المنتج. الخصائص بتتوزّع على تبويبات زي "General" و"SEO" و"Design" عشان الصفحة متبقاش فوضى.
المنتج بيشوف الخصائص اللي في مجموعته بس. لو عملت خاصية جديدة وماضفتهاش لمجموعة، مش هتظهر في صفحة التعديل — وده سبب شائع لـ "عملت الخاصية ومش لاقيها".
الإنشاء من الأدمن
السريع — وامتى يكون غلط.
- Stores → Attributes → Product ثم Add New Attribute.
- حدد Default Label وCatalog Input Type.
- لو select أو multiselect، ضيف الخيارات.
- من Advanced: الكود، الـ scope، هل مطلوب، القيمة الافتراضية.
- من Storefront Properties: الفلترة، البحث، العرض في القوائم.
- احفظ، وبعدين ضيفها لـ Attribute Set — من Attribute Set.
من الأدمن
سريع، بس موجود في قاعدة بيانات واحدة بس. مش هينتقل لـ staging أو production تلقائياً.
بالكود
بيتطبّق على كل بيئة مع النشر، ومسجّل في الـ git، وقابل للمراجعة.
الخصائص اللي الكود بيعتمد عليها → بالكود. لو موديولك بيقرا warranty_months، لازم تكون متعرّفة بالكود، وإلا هيقع على أي بيئة مفيهاش. الخصائص اللي فريق المحتوى بيديرها → من الأدمن عادي.
الإنشاء بالكود — Data Patch
الطريقة الصح للخصائص اللي الكود بيعتمد عليها.
namespace Vendor\Module\Setup\Patch\Data; use Magento\Catalog\Model\Product; use Magento\Eav\Setup\EavSetup; use Magento\Eav\Setup\EavSetupFactory; use Magento\Framework\Setup\ModuleDataSetupInterface; use Magento\Framework\Setup\Patch\DataPatchInterface; use Magento\Framework\Setup\Patch\PatchRevertableInterface; use Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface; class AddWarrantyAttribute implements DataPatchInterface, PatchRevertableInterface { public function __construct( private readonly ModuleDataSetupInterface $moduleDataSetup, private readonly EavSetupFactory $eavSetupFactory ) {} public function apply(): void { $this->moduleDataSetup->getConnection()->startSetup(); /** @var EavSetup $eavSetup */ $eavSetup = $this->eavSetupFactory->create([ 'setup' => $this->moduleDataSetup ]); $eavSetup->addAttribute( Product::ENTITY, 'warranty_months', [ 'type' => 'int', 'label' => 'Warranty (months)', 'input' => 'text', 'required' => false, 'user_defined' => true, 'global' => ScopedAttributeInterface::SCOPE_GLOBAL, 'visible' => true, 'searchable' => false, 'filterable' => false, 'comparable' => false, 'group' => 'General', 'sort_order' => 100, ] ); $this->moduleDataSetup->getConnection()->endSetup(); } public function revert(): void { $eavSetup = $this->eavSetupFactory->create([ 'setup' => $this->moduleDataSetup ]); $eavSetup->removeAttribute(Product::ENTITY, 'warranty_months'); } public static function getDependencies(): array { return []; } public function getAliases(): array { return []; } }
bin/magento setup:upgrade bin/magento cache:clean
public function apply(): void { $eavSetup = $this->eavSetupFactory->create([ 'setup' => $this->moduleDataSetup ]); // غيّر خاصية واحدة $eavSetup->updateAttribute( Product::ENTITY, 'warranty_months', 'is_filterable', 1 ); // أو كذا خاصية مرة واحدة $eavSetup->updateAttribute( Product::ENTITY, 'warranty_months', [ 'is_filterable' => 1, 'is_searchable' => 1, ] ); }
$eavSetup->addAttributeToSet( Product::ENTITY, 'Default', // اسم الـ attribute set 'General', // اسم الـ group (التبويب) 'warranty_months' );
كل خصائص الإعداد
المصفوفة اللي بتتبعت لـ addAttribute — مرجع كامل.
الخيارات و Source Models
للقوائم المنسدلة — ثابتة أو ديناميكية.
$eavSetup->addAttribute( Product::ENTITY, 'material', [ 'type' => 'int', 'label' => 'Material', 'input' => 'select', 'source' => Table::class, // الافتراضي للخيارات 'global' => ScopedAttributeInterface::SCOPE_GLOBAL, 'option' => [ 'values' => ['Cotton', 'Leather', 'Synthetic'], ], 'group' => 'General', ] );
لما الخيارات محسوبة مش ثابتة — جاية من نظام خارجي، أو من جدول تاني، أو من إعدادات. الـ source model بيولّدها في كل مرة.
namespace Vendor\Module\Model\Attribute\Source; use Magento\Eav\Model\Entity\Attribute\Source\AbstractSource; class Supplier extends AbstractSource { public function __construct( private readonly SupplierRepository $repository ) {} public function getAllOptions(): array { if ($this->_options === null) { $this->_options = [ ['value' => '', 'label' => __('-- Select --')], ]; foreach ($this->repository->getActive() as $supplier) { $this->_options[] = [ 'value' => $supplier->getCode(), 'label' => $supplier->getName(), ]; } } return $this->_options; } }
$eavSetup->addAttribute( Product::ENTITY, 'features', [ 'type' => 'varchar', // مش int! 'label' => 'Features', 'input' => 'multiselect', 'source' => Table::class, // إلزامي: بيحوّل المصفوفة لنص مفصول بفواصل 'backend' => ArrayBackend::class, 'global' => ScopedAttributeInterface::SCOPE_GLOBAL, 'option' => [ 'values' => ['Waterproof', 'Wireless', 'Foldable'], ], ] );
القراءة والكتابة
إزاي توصل للقيم في الكود.
// قيمة بسيطة $months = (int) $product->getData('warranty_months'); // select — الـ id مقابل النص $colorId = $product->getData('color'); // 42 $colorText = $product->getAttributeText('color'); // 'Red' // multiselect — نص مفصول، حوّله لمصفوفة $raw = $product->getData('features'); // '12,15,18' $ids = $raw ? explode(',', $raw) : []; // آمن مع القيم الفاضية $attr = $product->getCustomAttribute('warranty_months'); $value = $attr ? $attr->getValue() : null;
// منتج واحد $product = $this->productRepository->get('SKU-123'); $product->setData('warranty_months', 24); $this->productRepository->save($product); // multiselect — مصفوفة، والـ backend بيحوّلها $product->setData('features', [12, 15]);
use Magento\Catalog\Model\Product\Action; // بيحدّث الخاصية بس، من غير حفظ المنتج كامل $this->productAction->updateAttributes( [1, 2, 3], // product ids ['warranty_months' => 24], // القيم 0 // store id );
الفلترة في الـ Collections
البحث بالخصائص.
$collection = $this->collectionFactory->create(); $collection // لازم تطلب الخاصية عشان تقدر تقراها ->addAttributeToSelect(['name', 'warranty_months']) ->addAttributeToFilter('warranty_months', ['gteq' => 12]) ->addAttributeToFilter('status', 1) ->setPageSize(20); foreach ($collection as $product) { $months = $product->getData('warranty_months'); }
$criteria = $this->searchCriteriaBuilder ->addFilter('warranty_months', 12, 'gteq') ->addFilter('status', 1) ->create(); $result = $this->productRepository->getList($criteria); foreach ($result->getItems() as $product) { // ... }
الأداء
الجزء اللي بيفرّق بين كتالوج سريع وبطيء.
كل خاصية بتضيف صفوف وربط جداول. على كتالوج كبير، إعداد واحد غلط ممكن يضيف ثواني على وقت تحميل صفحة القسم — والأثر مش بيبان في التطوير لأن البيانات قليلة.
-- الخصائص المحمّلة في القوائم SELECT a.attribute_code, c.used_in_product_listing, c.is_filterable, c.is_searchable FROM eav_attribute a JOIN catalog_eav_attribute c ON c.attribute_id = a.attribute_id WHERE c.used_in_product_listing = 1 OR c.is_filterable = 1 ORDER BY a.attribute_code; -- خصائص user-defined مالهاش أي قيم (مرشّحة للحذف) SELECT a.attribute_code FROM eav_attribute a LEFT JOIN catalog_product_entity_varchar v ON v.attribute_id = a.attribute_id WHERE a.entity_type_id = 4 AND a.is_user_defined = 1 GROUP BY a.attribute_code HAVING COUNT(v.value_id) = 0;
الفخاخ الشائعة
اقرا دي حتى لو مش هتقرا حاجة تانية.
- نسيان addAttributeToSelect — القيمة موجودة في الأدمن وnull في الكود. أشهر فخ على الإطلاق.
- نسيان إضافة الخاصية لـ attribute set — مش هتظهر في صفحة المنتج خالص.
- تعديل data patch بعد ما اتنفّذ — مش هيتطبّق. اعمل patch جديد.
- multiselect من غير ArrayBackend — الحفظ بيفشل أو القيم بتتخزّن غلط.
- فلترة multiselect بـ eq — لازم finset.
- required = true على متجر فيه منتجات — كل المنتجات القديمة مش هتتحفظ.
- اختيار text بدل select لقيم محدودة — مفيش فلاتر، والتحويل بعدين مؤلم.
- الـ scope الغلط — خاصية global محتاجة ترجمة. صعب التراجع.
- source model بينده قاعدة بيانات من غير تخزين — استعلام مع كل منتج في كل صفحة.
- addAttributeToSelect('*') — بيحمّل كل حاجة ويقتل الذاكرة.
أسئلة الإنترفيو
الأسئلة المتكررة.
Product Attributes ✓
دلوقتي عندك الصورة كاملة: البنية والجداول، الأنواع والـ scope، الإنشاء من الأدمن وبالكود، الخيارات والـ source models، القراءة والفلترة، وأثر كل إعداد على الأداء.
الخلاصة: الخصائص اللي الكود بيعتمد عليها تتعمل بـ data patch، والنوع والـ scope قرارات صعب التراجع عنها، وكل إعداد فلترة أو بحث له تكلفة حقيقية.
خلّصت الدرس؟علّمه عشان تتابع تقدّمك في الكورس.
أسئلة شائعة
إزاي أضيف Product Attribute بالكود في Magento 2؟
بـ Data Patch في Setup/Patch/Data/ بيطبّق DataPatchInterface، وجوّاه EavSetup::addAttribute(Product::ENTITY, 'warranty_months', [...]) بالنوع والـ input والـ label والـ scope والـ group. بعدها setup:upgrade. الـ patch بيتنفّذ مرة واحدة وبيتسجّل في جدول patch_list، فلو عدّلته بعد التنفيذ مش هيتطبّق تاني ولازم patch جديد.
ليه قيمة الخاصية null في الكود وهي موجودة في الأدمن؟
غالبًا المنتج اتحمّل من collection من غير addAttributeToSelect(). الـ collections بتحمّل الخصائص المطلوبة صراحةً بس للأداء، وgetData() بيرجّع null من غير أي خطأ. الأسباب التانية: الخاصية مش مضافة للـ attribute set، أو القيمة متحددة على store view تاني، أو الكاش والفهرسة مااتعملوش بعد التغيير.
عملت Attribute ومش لاقيها في صفحة المنتج، ليه؟
لأن المنتج بيشوف الخصائص اللي في الـ attribute set بتاعه بس. لازم تضيفها لمجموعة من Stores › Attributes › Attribute Set، أو بالكود بـ addAttributeToSet(Product::ENTITY, 'Default', 'General', 'code'). لو مرّرت مفتاح group في addAttribute، الـ EavSetup بيضيفها لكل الـ attribute sets الموجودة تلقائيًا.
إيه الفرق بين Source Model و Backend Model في Magento؟
الـ source model بيحدد الخيارات المتاحة لخاصية select أو multiselect، وبيورّث من AbstractSource ويرجّعها من getAllOptions(). الـ backend model بيعالج القيمة نفسها قبل الحفظ وبعد التحميل، زي ArrayBackend اللي بيحوّل مصفوفة الـ multiselect لنص مفصول بفواصل. الـ multiselect لازم يكون نوعه varchar ومعاه ArrayBackend وإلا الحفظ بيبوظ.
إزاي أفلتر Collection بخاصية multiselect؟
بمعامل finset مش eq، لأن القيمة متخزّنة كنص فيه أكتر من id مفصولين بفاصلة: addAttributeToFilter('features', ['finset' => 12]). باقي المعاملات زي eq و gteq و like و in و null بتشتغل مع الخصائص العادية. ومتستخدمش addAttributeToSelect('*') على كتالوج كبير لأنه بيحمّل كل خاصية لكل منتج.
إزاي أعرف الخصائص اللي بتبطّئ الكتالوج في Magento 2؟
اعمل JOIN بين eav_attribute و catalog_eav_attribute وشوف اللي عليها used_in_product_listing أو is_filterable أو is_searchable. الأولى بتتحمّل مع كل منتج في كل صفحة قسم وهي أكتر واحدة بتتفعّل بالغلط، والفلترة والبحث بيكبّروا الفهارس. عطّل اللي مش مستخدم فعلًا وبعدها أعد الفهرسة وامسح الكاش.
إزاي أحدّث خاصية لآلاف المنتجات بسرعة؟
بـ Magento\Catalog\Model\Product\Action::updateAttributes($productIds, ['warranty_months' => 24], $storeId) بدل حفظ كل منتج على حدة. بيحدّث الخاصية مباشرة من غير تحميل المنتج كامل ولا تشغيل كل الـ observers والفهرسة لكل حفظة، والفرق على كتالوج كبير ممكن يكون ساعات.