diff --git a/src/data/navigation/sections/tutorials.js b/src/data/navigation/sections/tutorials.js index a826f36ff..ead44dac0 100644 --- a/src/data/navigation/sections/tutorials.js +++ b/src/data/navigation/sections/tutorials.js @@ -143,6 +143,11 @@ module.exports = [ title: "Modify media library folder permissions", path: "/tutorials/backend/modify-image-library-permissions/", }, + { + title: "Create a custom product attribute input type", + path: "/tutorials/backend/create-custom-attribute-input-type/", + }, ], }, - ]; \ No newline at end of file + ]; + diff --git a/src/pages/_images/tutorials/custom-attribute-input-type.png b/src/pages/_images/tutorials/custom-attribute-input-type.png new file mode 100644 index 000000000..51da25aa6 Binary files /dev/null and b/src/pages/_images/tutorials/custom-attribute-input-type.png differ diff --git a/src/pages/_images/tutorials/product-tags-attribute-final-result.png b/src/pages/_images/tutorials/product-tags-attribute-final-result.png new file mode 100644 index 000000000..456490679 Binary files /dev/null and b/src/pages/_images/tutorials/product-tags-attribute-final-result.png differ diff --git a/src/pages/tutorials/backend/create-custom-attribute-input-type.md b/src/pages/tutorials/backend/create-custom-attribute-input-type.md new file mode 100644 index 000000000..e5b5e4c30 --- /dev/null +++ b/src/pages/tutorials/backend/create-custom-attribute-input-type.md @@ -0,0 +1,464 @@ +--- +title: Create a Custom Product Attribute Input Type | Commerce PHP Extensions +description: Follow this tutorial to create a custom product attribute input type with an Adobe Commerce or Magento Open Source extension. +contributor_name: Goivvy LLC +contributor_link: https://www.goivvy.com/ +--- + +# Create a custom product attribute input type + +This tutorial shows you how to add a custom product attribute input type. + +We will create an extension that adds a `Dynamic Array` attribute input type and a product attribute `Product Tags` of that type. + +The final result will look like this: + +![Product Tags Attribute](../../_images/tutorials/product-tags-attribute-final-result.png) + +![Custom Attribute Input Type](../../_images/tutorials/custom-attribute-input-type.png) + +The final extension is available on [Github](https://github.com/goivvy/new-attribute-type). + +## Add a new product attribute input type + +To define a new input type, we need to modify arguments of `Magento\Eav\Model\Adminhtml\System\Config\Source\Inputtype` and setup a new UI modifier. + +***`app/code/Goivvy/Attribute/etc/adminhtml/di.xml`***: + +```xml + + + + + + + dynamicarray + Dynamic Array + + + + + + + + + Goivvy\Attribute\Ui\DataProvider\Product\Form\Modifier\Dynamic + 50 + + + + + +``` + +***`app/code/Goivvy/Attribute/Ui/DataProvider/Product/Form/Modifier/Dynamic.php`***: + +```php +locator = $locator; + $this->eavAttributeFactory = $eavAttributeFactory; + } + + public function modifyData(array $data) + { + return $data; + } + + public function modifyMeta(array $meta) + { + foreach ($meta as $groupCode => $groupConfig) { + $meta[$groupCode] = $this->modifyMetaConfig($groupConfig); + } + + return $meta; + } + + protected function modifyMetaConfig(array $metaConfig) + { + if (isset($metaConfig['children'])) { + foreach ($metaConfig['children'] as $attributeCode => $attributeConfig) { + if ($this->startsWith($attributeCode, self::CONTAINER_PREFIX)) { + $metaConfig['children'][$attributeCode] = $this->modifyMetaConfig($attributeConfig); + } elseif (!empty($attributeConfig['arguments']['data']['config']['formElement']) && + $attributeConfig['arguments']['data']['config']['formElement'] === static::FORM_ELEMENT_WEEE + ) { + $metaConfig['children'][$attributeCode] = + $this->modifyAttributeConfig($attributeCode, $attributeConfig); + } + } + } + + return $metaConfig; + } + + protected function modifyAttributeConfig($attributeCode, array $attributeConfig) + { + $product = $this->locator->getProduct(); + $eavAttribute = $this->eavAttributeFactory->create()->loadByCode(Product::ENTITY, $attributeCode); + + return array_replace_recursive($attributeConfig, [ + 'arguments' => [ + 'data' => [ + 'config' => [ + 'componentType' => 'dynamicRows', + 'formElement' => 'component', + 'renderDefaultRecord' => false, + 'itemTemplate' => 'record', + 'dataScope' => '', + 'dndConfig' => [ + 'enabled' => false, + ], + 'required' => (bool)$attributeConfig['arguments']['data']['config']['required'], + ], + ], + ], + 'children' => [ + 'record' => [ + 'arguments' => [ + 'data' => [ + 'config' => [ + 'componentType' => Container::NAME, + 'isTemplate' => true, + 'is_collection' => true, + 'component' => 'Magento_Ui/js/dynamic-rows/record', + 'dataScope' => '', + ], + ], + ], + 'children' => [ + 'value' => [ + 'arguments' => [ + 'data' => [ + 'config' => [ + 'componentType' => Field::NAME, + 'formElement' => Input::NAME, + 'dataType' => Text::NAME, + 'label' => __('Value'), + 'enableLabel' => true, + 'dataScope' => 'value', + 'validation' => [ + 'required-entry' => true + ], + 'showLabel' => false, + ], + ], + ], + ], + 'actionDelete' => [ + 'arguments' => [ + 'data' => [ + 'config' => [ + 'componentType' => 'actionDelete', + 'dataType' => Text::NAME, + 'label' => __('Action'), + ], + ], + ], + ], + ], + ], + ], + ]); + } +} +``` + +Then we set a custom backend model for every attribute of type `dynamicarray`. + +***`app/code/Goivvy/Attribute/etc/events.xml`***: + +```xml + + + + + + +``` + +***`app/code/Goivvy/Attribute/Observer/UpdateElement.php`***: + +```php +productType = $productType; + $this->productTypeConfig = $productTypeConfig; + } + + public function execute(\Magento\Framework\Event\Observer $observer) + { + $backendModel = \Goivvy\Attribute\Model\Attribute\Backend\Dynamic::class; + $object = $observer->getEvent()->getAttribute(); + if ($object->getFrontendInput() == 'dynamicarray') { + $object->setBackendModel($backendModel); + if (!$object->getApplyTo()) { + $applyTo = []; + foreach ($this->productType->getOptions() as $option) { + if ($this->productTypeConfig->isProductSet($option['value'])) { + continue; + } + $applyTo[] = $option['value']; + } + $object->setApplyTo($applyTo); + } + } + + return $this; + } +} +``` + +***`app/code/Goivvy/Attribute/Model/Attribute/Backend/Dynamic.php`***: + +```php +_modelDynamic = $modelDynamic; + } + + public function validate($object) + { + return $this; + } + + public function afterLoad($object) + { + $data = $this->_modelDynamic->loadProductData($object, $this->getAttribute()); + + $object->setData($this->getAttribute()->getName(), $data); + return $this; + } + + public function afterSave($object) + { + $orig = $object->getOrigData($this->getAttribute()->getName()); + $current = $object->getData($this->getAttribute()->getName()); + if ($orig == $current) { + return $this; + } + + $this->_modelDynamic->deleteProductData($object, $this->getAttribute()); + $values = $object->getData($this->getAttribute()->getName()); + + if (!is_array($values)) { + return $this; + } + + foreach ($values as $value) { + if (empty($value['value']) || !empty($value['delete'])) { + continue; + } + + $data = []; + $data['value'] = $value['value']; + $data['attribute_id'] = $this->getAttribute()->getId(); + + $this->_modelDynamic->insertProductData($object, $data); + } + + return $this; + } + + public function afterDelete($object) + { + $this->_modelDynamic->deleteProductData($object, $this->getAttribute()); + return $this; + } + + public function getTable() + { + return $this->_modelDynamic->getTable('goivvy_dynamic'); + } + + public function getEntityIdField() + { + return $this->_modelDynamic->getIdFieldName(); + } +} +``` + +Then we setup a DB table to hold all values of product attributes of type `dynamicarray`. + +***`app/code/Goivvy/Attribute/etc/db_schema.xml`***: + +```xml + + + + + + + + + + + + + + + + + + +
+
+``` + +And add a resource model for attributes of type `dynamicarray`. + +***`app/code/Goivvy/Attribute/Model/ResourceModel/Attribute/Dynamic.php`***: + +```php +_init('goivvy_dynamic', 'id'); + } + + public function loadProductData($product, $attribute) + { + $select = $this->getConnection()->select()->from( + $this->getMainTable(), + ['value'] + )->where( + 'product_id = ?', + (int)$product->getId() + )->where( + 'attribute_id = ?', + (int)$attribute->getId() + ); + return $this->getConnection()->fetchAll($select); + } + + public function deleteProductData($product, $attribute) + { + $where = ['product_id = ?' => (int)$product->getId(), 'attribute_id = ?' => (int)$attribute->getId()]; + + $connection = $this->getConnection(); + $connection->delete($this->getMainTable(), $where); + return $this; + } + + public function insertProductData($product, $data) + { + $data['product_id'] = (int)$product->getId(); + $this->getConnection()->insert($this->getMainTable(), $data); + return $this; + } +} +``` + +## Create a custom product attribute of type dynamicarray + +***`app/code/Goivvy/Attribute/Setup/InstallData.php`***: + +```php +eavSetupFactory = $eavSetupFactory; + } + + public function install(ModuleDataSetupInterface $setup, ModuleContextInterface $context) + { + $eavSetup = $this->eavSetupFactory->create(['setup' => $setup]); + $eavSetup->addAttribute( + \Magento\Catalog\Model\Product::ENTITY + , 'product_tags' + , [ + 'type' => 'static' + , 'backend' => 'Goivvy\Attribute\Model\Attribute\Backend\Dynamic' + , 'frontend' => '' + , 'label' => __('Product Tags') + , 'input' => 'dynamicarray' + , 'visible' => true + , 'visible_on_front' => true + , 'global' => \Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface::SCOPE_GLOBAL + ] + ); + } +} +``` + +## Display Product Tags attribute on frontend + +```php +
+ +
+``` + +You can find a working copy of the extension on a [GitHub page](https://github.com/goivvy/new-attribute-type).