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:
+
+
+
+
+
+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
+
+
+getProductTags() as $k => $v):?>
+
+
+
+
+```
+
+You can find a working copy of the extension on a [GitHub page](https://github.com/goivvy/new-attribute-type).