How to add a multiselect EAV attribute on Magento 2 Cateogory

This will give you an overview about how to add an EAV multiselect attribute on category entity on backend.
Context:
I am creating this tutorial while I develop a feature for one the Magento 2 projects I am currently working on, called Product Tags. This feature allows users to filter products by tags on frontend at category level.
For this we need an attribute which will store all the product tags for the entire category. In this tutorial I am describing how to add this EAV attribute to the category.
Steps:
We will go trough the following steps via a new module called here Bb_Tags:
- Creating and registering the new module ( we won’t cover this ).
- Creating the EAV attribute by Setup\InstallData script.
- Add the form field via adminhtml/view/category_form.xml file.
- Creating the EAV attribute BackendModel.
- Testing the new Module by having the values stored for one category.
2. Creating the EAV attribute by Setup\InstallData script
In order to accomplish this step, we only need to create a new install script on the new module.
This will be executed only once when the cli command setup:upgrade will be executed first time after the module:enable Bb_Tags .
Notes:
a) Here we need to have an
\Magento\Eav\Setup\EavSetup object injected on the __construct() method. Optionally we included additional objects and factories in order to add some mock options, you can skip using these.
b) We need to use this object to create a catalog_category EAV attribute.
c) Source Model for the EAV attribute is: Magento\Eav\Model\Entity\Attribute\Source\Table
d) Custom Backend Model is required here in order to have the values imploded by a comma at save.
Here is the
Bb\Tags\Setup\InstallData.php file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 |
<?php /** * @author Serbu Florin-Adrian <florin.serbu@gmail.com> * @copyright Copyright (c) 2016 Serbu Florin-Adrian (https://serbu.me) * @package Bb_Tags */ namespace Bb\Tags\Setup; use Magento\Framework\Setup\InstallDataInterface; use Magento\Framework\Setup\ModuleContextInterface; use Magento\Framework\Setup\ModuleDataSetupInterface; use Magento\Eav\Setup\EavSetup; use Magento\Eav\Setup\EavSetupFactory; /** * @codeCoverageIgnore */ class InstallData implements InstallDataInterface { protected $eavSetupFactory; protected $_logger; protected $_attributeRepository; protected $_attributeOptionManagement; protected $_option; protected $_attributeOptionLabel; public function __construct( EavSetupFactory $eavSetupFactory, // the following parameters are optional \Psr\Log\LoggerInterface $logger, \Magento\Eav\Model\AttributeRepository $attributeRepository, \Magento\Eav\Api\AttributeOptionManagementInterface $attributeOptionManagement, \Magento\Eav\Model\Entity\Attribute\OptionLabelFactory $attributeOptionLabel, \Magento\Eav\Model\Entity\Attribute\OptionFactory $option ){ $this->_eavSetupFactory = $eavSetupFactory; /* These are optional and used only to populate the attribute with some mock options */ $this->_logger = $logger; $this->_attributeRepository = $attributeRepository; $this->_attributeOptionManagement = $attributeOptionManagement; $this->_option = $option; $this->_attributeOptionLabel = $attributeOptionLabel; } /** * {@inheritdoc} */ public function install(ModuleDataSetupInterface $setup, ModuleContextInterface $context) { /** @var EavSetup $eavSetup */ $eavSetup = $this->_eavSetupFactory->create(['setup' => $setup]); $entityTypeId = $eavSetup->getEntityTypeId(\Magento\Catalog\Model\Category::ENTITY); $attributeSetId = $eavSetup->getDefaultAttributeSetId($entityTypeId); $eavSetup->removeAttribute(\Magento\Catalog\Model\Category::ENTITY, 'tags'); $eavSetup->addAttribute( \Magento\Catalog\Model\Category::ENTITY, 'tags', [ 'type' => 'int', 'label' => 'Tags', 'input' => 'multiselect', 'source' => 'Magento\Eav\Model\Entity\Attribute\Source\Table', 'backend' => 'Bb\Tags\Model\Category\Attribute\Backend\Tags', 'required' => false, 'sort_order' => 70, 'global' => \Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface::SCOPE_STORE, 'group' => 'Content', ] ); /* * The following lines are optional * and used only to populate the attribute with some mock options * used script from here: http://magento.stackexchange.com/questions/103934/magento2-programmatically-add-product-attribute-options */ $mockOptions = [ 'Tag1', 'Tag2' ]; $attributeId = $this->_attributeRepository->get( \Magento\Catalog\Model\Category::ENTITY, 'tags' )->getAttributeId(); foreach($mockOptions as $label){ /** @var \Magento\Eav\Model\Entity\Attribute\Option $option */ $option = $this->_option->create(); $labelObj = $this->_attributeOptionLabel->create(); $labelObj->setStoreId(0); $labelObj->setLabel($label); $option->setLabel($labelObj); $option->setStoreLabels([$labelObj]); $option->setSortOrder(0); $option->setIsDefault(false); $option->setAttributeId($attributeId)->getResource()->save($option); $this->_attributeOptionManagement->add(\Magento\Catalog\Model\Category::ENTITY, $attributeId, $option); } } } |
3. Add the form field via adminhtml/view/category_form.xml file
As we can see in the Magento_Catalog module, the category form fields are specified in the vendor/magento/module-catalog/view/adminhtml/ui_component/category_form.xml file.
Similarly we will have our own Bb/Tags/view/adminhtml/ui_component/category_form.xml file that is handling the displaying of our multiselect filed:
Notes:
a) This field is displayed in the content accordion tab that already exists.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<?xml version="1.0" encoding="UTF-8"?> <form xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd"> <fieldset name="content"> <field name="tags"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="sortOrder" xsi:type="number">70</item> <item name="dataType" xsi:type="string">string</item> <item name="formElement" xsi:type="string">multiselect</item> <item name="label" xsi:type="string" translate="true">Category Tags</item> </item> </argument> </field> </fieldset> </form> |
After module:enable and setup:upgrade cli commands, you should be able to see this:
If you hit save, you’ll see that no actual values are stored for ‘Category Tags’ field. And that’s because we need to modify the options format.
[Offtopic] I didn’t figured out why the EAV model cannot accept array for multiselect values when saving the options for a certain entity.
If you are willing to dig more, check vendor/magento/module-eav/Model/ResourceModel/UpdateHandler.php at line 129. If you won’t implement a custom Backend Model that simply implodes the values into a comma sep. string, it won’t pass these lines.
4. Creating the EAV attribute BackendModel
On this step we will create the custom backendModel we used at previous step on attribute creation. By having this we will accomplish the actual storing tags attribute options for category procedure.
So let’s go and create the Bb/Tags/Model/Category/Attribute/Backend/Tags.php file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
<?php /** * @author Serbu Florin-Adrian <florin.serbu@gmail.com> * @copyright Copyright (c) 2016 Serbu Florin-Adrian (https://serbu.me) * @package Bb_Tags */ namespace Bb\Tags\Model\Category\Attribute\Backend; class Tags extends \Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend { public function beforeSave($object) { $attributeCode = $this->getAttribute()->getName(); if ($attributeCode == 'tags') { $data = $object->getData($attributeCode); if (!is_array($data)) { $data = []; } $object->setData($attributeCode, implode(',', $data) ?: null); } if (!$object->hasData($attributeCode)) { $object->setData($attributeCode, null); } return $this; } public function afterLoad($object) { $attributeCode = $this->getAttribute()->getName(); if ($attributeCode == 'tags') { $data = $object->getData($attributeCode); if ($data) { if (!is_array($data)) { $object->setData($attributeCode, explode(',', $data)); } else { $object->setData($attributeCode, $data); } } } return $this; } } |
You’ll be able to save now, and see that the selected values persists.
Let me know if you succeed!