Magento2.1 – Custom Category Image Attribute Not Saved

categorymagento-2.1

I have created a custom category thumbnail attribute according to the post (Magento 2 create category attribute thumbnail and upload image using File Uploader Component).

The image gets saved in the "tmp" directory but is not saved when I save the category.

what is the issue for that?

Best Answer

This is quite a big answer.

In Magento 2.1.0 you need to apply a patch to create a additional file upload attribute for category.

As they are using hardcoded values for image attribute on many places. for more details have a look at link1 and link2

Steps to apply a patch.

1) Edit composer.json file located at root directory as follows.

{
    "name": "magento/project-community-edition",
    "description": "eCommerce Platform for Growth (Community Edition)",
    "type": "project",
    "version": "2.1.0",
    "license": [
        "OSL-3.0",
        "AFL-3.0"
    ],
    "repositories": [
        {
            "type": "composer",
            "url": "https://repo.magento.com/"
        }
    ],
    "require": {
        "magento/product-community-edition": "2.1.0",
        "cweagans/composer-patches": "~1.4.0",
        "composer/composer": "@alpha"        
    },
    "require-dev": {
        "phpunit/phpunit": "4.1.0",
        "squizlabs/php_codesniffer": "1.5.3",
        "phpmd/phpmd": "2.3.*",
        "pdepend/pdepend": "2.2.2",
        "sjparkinson/static-review": "~4.1",
        "fabpot/php-cs-fixer": "~1.2",
        "lusitanian/oauth": "~0.3 <=0.7.0"
    },
    "config": {
        "use-include-path": true
    },
    "autoload": {
        "psr-4": {
            "Magento\\Framework\\": "lib/internal/Magento/Framework/",
            "Magento\\Setup\\": "setup/src/Magento/Setup/",
            "Magento\\": "app/code/Magento/"
        },
        "psr-0": {
            "": "app/code/"
        },
        "files": [
            "app/etc/NonComposerComponentRegistration.php"
        ]
    },
    "autoload-dev": {
        "psr-4": {
            "Magento\\Sniffs\\": "dev/tests/static/framework/Magento/Sniffs/",
            "Magento\\Tools\\": "dev/tools/Magento/Tools/",
            "Magento\\Tools\\Sanity\\": "dev/build/publication/sanity/Magento/Tools/Sanity/",
            "Magento\\TestFramework\\Inspection\\": "dev/tests/static/framework/Magento/TestFramework/Inspection/",
            "Magento\\TestFramework\\Utility\\": "dev/tests/static/framework/Magento/TestFramework/Utility/"
        }
    },
    "minimum-stability": "alpha",
    "prefer-stable": true,
    "extra": {
        "patches": {
            "magento/module-catalog": {
                "Fix: https://github.com/magento/magento2/issues/5438": "patches/Patch-Magento_Catalog-M2.1.0-image-attribute-backend-model-hardcoded-attribute-code-removal.patch"
            },
            "magento/module-ui": {
                "Fix: https://github.com/magento/magento2/issues/5438": "patches/Patch-Magento_Ui-M2.1.0-allow-backend-to-know-the-origin-input-of-the-upload-request.patch"
            }
        },
        "magento-force": "override"
    }
}

As mentioned on link2 above newly added code is in extra and require.

2) Create a patches folder on root directory.

3) Create file Patch-Magento_Catalog-M2.1.0-image-attribute-backend-model-hardcoded-attribute-code-removal.patch under patches folder with following contains.

--- Controller/Adminhtml/Category/Save.php.org  2016-08-03 21:35:33.772109252 +0200
+++ Controller/Adminhtml/Category/Save.php  2016-08-03 21:36:33.982229407 +0200
@@ -6,6 +6,7 @@
 namespace Magento\Catalog\Controller\Adminhtml\Category;

 use Magento\Store\Model\StoreManagerInterface;
+use \Magento\Catalog\Api\Data\CategoryAttributeInterface;

 /**
  * Class Save
@@ -49,6 +50,11 @@
     private $storeManager;

     /**
+     * @var \Magento\Eav\Model\Config
+     */
+    private $eavConfig;
+
+    /**
      * Constructor
      *
      * @param \Magento\Backend\App\Action\Context $context
@@ -62,37 +68,15 @@
         \Magento\Framework\Controller\Result\RawFactory $resultRawFactory,
         \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory,
         \Magento\Framework\View\LayoutFactory $layoutFactory,
-        StoreManagerInterface $storeManager
+        StoreManagerInterface $storeManager,
+        \Magento\Eav\Model\Config $eavConfig
     ) {
         parent::__construct($context);
         $this->resultRawFactory = $resultRawFactory;
         $this->resultJsonFactory = $resultJsonFactory;
         $this->layoutFactory = $layoutFactory;
         $this->storeManager = $storeManager;
-    }
-
-    /**
-     * Filter category data
-     *
-     * @param array $rawData
-     * @return array
-     */
-    protected function _filterCategoryPostData(array $rawData)
-    {
-        $data = $rawData;
-        // @todo It is a workaround to prevent saving this data in category model and it has to be refactored in future
-        if (isset($data['image']) && is_array($data['image'])) {
-            if (!empty($data['image']['delete'])) {
-                $data['image'] = null;
-            } else {
-                if (isset($data['image'][0]['name']) && isset($data['image'][0]['tmp_name'])) {
-                    $data['image'] = $data['image'][0]['name'];
-                } else {
-                    unset($data['image']);
-                }
-            }
-        }
-        return $data;
+        $this->eavConfig = $eavConfig;
     }

     /**
@@ -126,7 +110,7 @@
         $this->storeManager->setCurrentStore($store->getCode());
         $parentId = isset($categoryPostData['parent']) ? $categoryPostData['parent'] : null;
         if ($categoryPostData) {
-            $category->addData($this->_filterCategoryPostData($categoryPostData));
+            $category->addData($categoryPostData);
             if ($isNewCategory) {
                 $parentCategory = $this->getParentCategory($parentId, $storeId);
                 $category->setPath($parentCategory->getPath());
@@ -247,22 +231,29 @@
         );
     }

-    /**
-     * Image data preprocessing
-     *
-     * @param array $data
-     *
-     * @return array
-     */
     public function imagePreprocessing($data)
     {
-        if (empty($data['image'])) {
-            unset($data['image']);
-            $data['image']['delete'] = true;
+        $entityType = $this->eavConfig->getEntityType(CategoryAttributeInterface::ENTITY_TYPE_CODE);
+
+        foreach ($entityType->getAttributeCollection() as $attributeModel) {
+            $attributeCode = $attributeModel->getAttributeCode();
+            $backendModel = $attributeModel->getBackend();
+
+            if (isset($data[$attributeCode])) {
+                continue;
+            }
+
+            if (!$backendModel instanceof \Magento\Catalog\Model\Category\Attribute\Backend\Image) {
+                continue;
+            }
+
+            $data[$attributeCode] = false;
         }
+
         return $data;
     }

+
     /**
      * Converting inputs from string to boolean
      *
--- Controller/Adminhtml/Category/Image/Upload.php.org  2016-08-01 20:36:22.014237780 +0200
+++ Controller/Adminhtml/Category/Image/Upload.php  2016-08-01 20:36:25.292475257 +0200
@@ -50,8 +50,10 @@
      */
     public function execute()
     {
+        $imageId = $this->_request->getParam('param_name', 'image');
+
         try {
-            $result = $this->imageUploader->saveFileToTmpDir('image');
+            $result = $this->imageUploader->saveFileToTmpDir($imageId);

             $result['cookie'] = [
                 'name' => $this->_getSession()->getName(),
--- Model/Category/Attribute/Backend/Image.php.org  2016-08-03 21:36:05.695112561 +0200
+++ Model/Category/Attribute/Backend/Image.php  2016-08-03 21:36:46.829828298 +0200
@@ -13,6 +13,8 @@

 class Image extends \Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend
 {
+    const ADDITIONAL_DATA_SUFFIX = '_additional_data';
+
     /**
      * @var \Magento\MediaStorage\Model\File\UploaderFactory
      *
@@ -21,8 +23,6 @@
     protected $_uploaderFactory;

     /**
-     * Filesystem facade
-     *
      * @var \Magento\Framework\Filesystem
      *
      * @deprecated
@@ -30,8 +30,6 @@
     protected $_filesystem;

     /**
-     * File Uploader factory
-     *
      * @var \Magento\MediaStorage\Model\File\UploaderFactory
      *
      * @deprecated
@@ -46,15 +44,11 @@
     protected $_logger;

     /**
-     * Image uploader
-     *
      * @var \Magento\Catalog\Model\ImageUploader
      */
     private $imageUploader;

     /**
-     * Image constructor.
-     *
      * @param \Psr\Log\LoggerInterface $logger
      * @param \Magento\Framework\Filesystem $filesystem
      * @param \Magento\MediaStorage\Model\File\UploaderFactory $fileUploaderFactory
@@ -70,8 +64,50 @@
     }

     /**
-     * Get image uploader
+     * @param $value
+     * @return string|bool
+     */
+    protected function getUploadedImageName($value)
+    {
+        if (!is_array($value)) {
+            return false;
+        }
+
+        if (!count($value)) {
+            return false;
+        }
+
+        $imageData = reset($value);
+
+        if (!isset($imageData['name'])) {
+            return false;
+        }
+
+        return $imageData['name'];
+    }
+
+    /**
+     * Avoiding saving potential upload data to DB
      *
+     * @param \Magento\Framework\DataObject $object
+     * @return $this
+     */
+    public function beforeSave($object)
+    {
+        $attributeName = $this->getAttribute()->getName();
+        $value = $object->getData($attributeName);
+
+        if ($value === false || (is_array($value) && isset($value['delete']) && $value['delete'] === true)) {
+            $object->setData($attributeName, '');
+        } else if ($imageName = $this->getUploadedImageName($value)) {
+            $object->setData($attributeName . self::ADDITIONAL_DATA_SUFFIX, $value);
+            $object->setData($attributeName, $imageName);
+        }
+
+        return parent::beforeSave($object);
+    }
+
+    /**
      * @return \Magento\Catalog\Model\ImageUploader
      *
      * @deprecated
@@ -80,9 +116,10 @@
     {
         if ($this->imageUploader === null) {
             $this->imageUploader = \Magento\Framework\App\ObjectManager::getInstance()->get(
-                'Magento\Catalog\CategoryImageUpload'
+                \Magento\Catalog\CategoryImageUpload::class
             );
         }
+
         return $this->imageUploader;
     }

@@ -94,15 +131,18 @@
      */
     public function afterSave($object)
     {
-        $image = $object->getData($this->getAttribute()->getName(), null);
+        $value = $object->getData($this->getAttribute()->getName() . self::ADDITIONAL_DATA_SUFFIX);

-        if ($image !== null) {
-            try {
-                $this->getImageUploader()->moveFileFromTmp($image);
-            } catch (\Exception $e) {
-                $this->_logger->critical($e);
-            }
+        if (!$imageName = $this->getUploadedImageName($value)) {
+            return $this;
         }
+
+        try {
+            $this->getImageUploader()->moveFileFromTmp($imageName);
+        } catch (\Exception $e) {
+            $this->_logger->critical($e);
+        }
+
         return $this;
     }
 }
--- Model/Category/DataProvider.php.org 2016-08-01 21:35:43.567609510 +0200
+++ Model/Category/DataProvider.php 2016-08-01 21:43:07.800993338 +0200
@@ -203,14 +203,24 @@
         $category = $this->getCurrentCategory();
         if ($category) {
             $categoryData = $category->getData();
+
             $categoryData = $this->addUseDefaultSettings($category, $categoryData);
             $categoryData = $this->addUseConfigSettings($categoryData);
             $categoryData = $this->filterFields($categoryData);
-            if (isset($categoryData['image'])) {
-                unset($categoryData['image']);
-                $categoryData['image'][0]['name'] = $category->getData('image');
-                $categoryData['image'][0]['url'] = $category->getImageUrl();
+
+            foreach ($category->getAttributes() as $attributeCode => $attribute) {
+                $backendModel = $attribute->getBackend();
+
+                if ($backendModel instanceof \Magento\Catalog\Model\Category\Attribute\Backend\Image) {
+                    if (isset($categoryData[$attributeCode])) {
+                        unset($categoryData[$attributeCode]);
+
+                        $categoryData[$attributeCode][0]['name'] = $category->getData($attributeCode);
+                        $categoryData[$attributeCode][0]['url'] = $category->getImageUrl($attributeCode);
+                    }
+                }
             }
+
             $this->loadedData[$category->getId()] = $categoryData;
         }
         return $this->loadedData;
--- Model/Category.php.org  2016-08-01 21:41:47.535876208 +0200
+++ Model/Category.php  2016-08-01 21:42:57.916422400 +0200
@@ -652,14 +652,15 @@
     }

     /**
-     * Retrieve image URL
+     * @param $attributeCode
      *
-     * @return string
+     * @return bool|string
+     * @throws \Magento\Framework\Exception\LocalizedException
      */
-    public function getImageUrl()
+    public function getImageUrl($attributeCode = 'image')
     {
         $url = false;
-        $image = $this->getImage();
+        $image = $this->getData($attributeCode);
         if ($image) {
             if (is_string($image)) {
                 $url = $this->_storeManager->getStore()->getBaseUrl(

4) Create file Patch-Magento_Ui-M2.1.0-allow-backend-to-know-the-origin-input-of-the-upload-request.patch under patches folder with following contains.

--- view/base/web/js/form/element/file-uploader.js.org  2016-08-01 20:33:22.866555339 +0200
+++ view/base/web/js/form/element/file-uploader.js  2016-08-01 20:33:42.384061881 +0200
@@ -302,7 +302,13 @@
                 allowed  = this.isFileAllowed(file);

             if (allowed.passed) {
-                $(e.target).fileupload('process', data).done(function () {
+                var $target = $(e.target);
+
+                $target.on('fileuploadsend', function(event, postData) {
+                    postData.data.set('param_name', this.paramName);
+                }.bind(data));
+
+                $target.fileupload('process', data).done(function () {
                     data.submit();
                 });
             } else {

5) Run following Commands

  1. composer validate composer.json (To Vallidate composer file).
  2. composer update
  3. php bin/magento setup:upgrade

6) Now its time to create your module for new file upload option.

Create a file

app\code\Vendor\Module\registration.php

\Magento\Framework\Component\ComponentRegistrar::register(
        \Magento\Framework\Component\ComponentRegistrar::MODULE,
        "Vendor_Module",
        __DIR__
    );

Create a file

app\code\Vendor\Module\etc\module.xml

<?xml version="1.0" encoding="UTF-8" ?>
    <config>
        <module name="Vendor_Module" setup_version="1.0" />
    </config>

Create a file

app\code\Vendor\Module\Setup\InstallData.php

<?php
    namespace Vendor\Module\Setup;
    use Magento\Framework\Module\Setup\Migration;
    use Magento\Framework\Setup\InstallDataInterface;
    use Magento\Framework\Setup\ModuleContextInterface;
    use Magento\Framework\Setup\ModuleDataSetupInterface;
    use Magento\Catalog\Setup\CategorySetupFactory;
    class InstallData implements InstallDataInterface
    {
        public function __construct(CategorySetupFactory $categorySetupFactory)
        {
            $this->categorySetupFactory = $categorySetupFactory;
        }
        public function install(ModuleDataSetupInterface $setup, ModuleContextInterface $context)
        {
            $installer = $setup;
            $installer->startSetup();    
            $categorySetup = $this->categorySetupFactory->create(['setup' => $setup]);
            $entityTypeId = $categorySetup->getEntityTypeId(\Magento\Catalog\Model\Category::ENTITY);
            $attributeSetId = $categorySetup->getDefaultAttributeSetId($entityTypeId);
            $categorySetup->removeAttribute(
                \Magento\Catalog\Model\Category::ENTITY, 'featured_image' );
            $categorySetup->addAttribute(
                \Magento\Catalog\Model\Category::ENTITY, 'featured_image', [
                    'type' => 'varchar',
                    'label' => 'Featured Image',
                    'input' => 'image',
                    'backend' => 'Magento\Catalog\Model\Category\Attribute\Backend\Image',
                    'required' => false,
                    'sort_order' => 5,
                    'global' => \Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface::SCOPE_STORE,
                    'group' => 'General Information',
                ]
            );
            $installer->endSetup();
        }
    }

Create a file

app\code\Vendor\Module\view\adminhtml\ui_component\category_form.xml

<?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">
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="label" xsi:type="string" translate="true">Content</item>
                    <item name="collapsible" xsi:type="boolean">true</item>
                    <item name="sortOrder" xsi:type="number">10</item>
                </item>
            </argument>
            <field name="featured_image">
                <argument name="data" xsi:type="array">
                    <item name="config" xsi:type="array">
                        <item name="dataType" xsi:type="string">string</item>
                        <item name="source" xsi:type="string">category</item>
                        <item name="label" xsi:type="string" translate="true">Featured Image</item>
                        <item name="visible" xsi:type="boolean">true</item>
                        <item name="formElement" xsi:type="string">fileUploader</item>
                        <item name="elementTmpl" xsi:type="string">ui/form/element/uploader/uploader</item>
                        <item name="previewTmpl" xsi:type="string">Magento_Catalog/image-preview</item>
                        <item name="required" xsi:type="boolean">false</item>
                        <item name="sortOrder" xsi:type="number">40</item>
                        <item name="uploaderConfig" xsi:type="array">
                            <item name="url" xsi:type="url" path="catalog/category_image/upload"/>
                        </item>
                    </item>
                </argument>
            </field>
        </fieldset>
    </form>

Hope this helps you.