Add product Image upload field and preview image in Magento 2 Product UI Component Form

By vijayramiUpdated 11 June 2023
Magento UIMagento2Magento2 Product
Add product Image upload field and preview image in Magento 2 Product UI Component Form

Media Image

For that solution, we will provide a custom image upload button which is useful to upload your custom image and it will not come inside the media gallery images for that product.



Let's start by creating a custom module.

Find the Complete module on Github at Magelearn_ProductImage

Create folder in app/code/Magelearn/ProductImage

Create a module registration file

Add registration.php file in it:
<?php
/**
 * Copyright ©  All rights reserved.
 * See COPYING.txt for license details.
 */
use Magento\Framework\Component\ComponentRegistrar;
 
ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magelearn_ProductImage', __DIR__);
Add composer.json file in it:
{
    "name": "magelearn/module-ProductImage",
    "description": "Magento2 image attachment in product add/edit admin form and display image on product page",
    "type": "magento2-module",
    "license": "OSL-3.0",
    "authors": [
        {
            "email": "vijaymrami@gmail.com",
            "name": "vijay rami"
        }
    ],
    "minimum-stability": "dev",
    "require": {},
    "autoload": {
        "files": [
            "registration.php"
        ],
        "psr-4": {
            "Magelearn\\ProductImage\\": ""
        }
    }
}
Add etc/module.xml file in it:
<?xml version="1.0" ?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
    <module name="Magelearn_ProductImage" setup_version="1.0.0">
    </module>
</config>

Add etc/acl.xml file in it:

<?xml version="1.0" encoding="UTF-8"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:Acl/etc/acl.xsd">
    <acl>
        <resources>
            <resource id="Magento_Backend::admin">
                <resource id="Magento_Backend::content">
                    <resource id="Magelearn_ProductImage::upload"
                              title="Default Product Image Settings"
                              sortOrder="20"/>
                </resource>
            </resource>
        </resources>
    </acl>
</config>
Create InstallData script to add product custom image attribute
Next, we will create a script to add `product_label_image`  attribute. So create an InstallData.php in app/code/Magelearn/ProductImage/Setup directory.
<?php
namespace Magelearn\ProductImage\Setup;

use Magelearn\ProductImage\Model\Product\Attribute\Backend\ImageUploader as ImageUploaderBackend;
use Magento\Framework\Setup\ModuleContextInterface;
use Magento\Framework\Setup\ModuleDataSetupInterface;
use Magento\Framework\Setup\InstallDataInterface;
use Magento\Eav\Setup\EavSetupFactory;

/**
 * Class InstallData
 * @package Magelearn\Productattachement\Setup
 *
 * @codeCoverageIgnore
 */
class InstallData implements InstallDataInterface
{
    /**
     * @var EavSetupFactory
     */
    protected $_eavSetupFactory;
    
    public function __construct(
        EavSetupFactory $eavSetupFactory
        ) {
            $this->_eavSetupFactory = $eavSetupFactory;
    }
    
    /**
     * {@inheritdoc}
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
     * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
     * @SuppressWarnings(PHPMD.NPathComplexity)
     */
    public function install(ModuleDataSetupInterface $setup, ModuleContextInterface $context)
    {
        $eavSetup = $this->_eavSetupFactory->create(["setup"=>$setup]);
        $eavSetup->addAttribute(
            \Magento\Catalog\Model\Product::ENTITY,
            'product_label_image',
            [
                'group' => 'Content',
                'type' => 'varchar',
                'label' => 'Product Label Image',
                'input' => 'text',
                'backend' => ImageUploaderBackend::class,
                'frontend' => '',
                'class' => '',
                'source' => '',
                'global' => \Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface::SCOPE_GLOBAL,
                'visible' => true,
                'required' => false,
                'user_defined' => true,
                'default' => '',
                'searchable' => false,
                'filterable' => false,
                'comparable' => false,
                'visible_on_front' => true,
                'unique' => false,
                'apply_to' => 'simple,configurable', // applicable for simple and configurable product
                'used_in_product_listing' => true
            ]
            );
    }
}
Now, as per highlighted code above, we will add our backed class.

Add file at Magelearn/ProductImage/Model/Product/Attribute/Backend/ImageUploader.php file.
<?php

declare(strict_types = 1);

namespace Magelearn\ProductImage\Model\Product\Attribute\Backend;

use Magelearn\ProductImage\Model\ImageIO;
use Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend;

class ImageUploader extends AbstractBackend
{
    /**
     * @var ImageIO
     */
    private $imageIO;

    /**
     * ImageUploader constructor.
     *
     * @param ImageIO $imageIO
     */
    public function __construct(
        ImageIO $imageIO
    ) {
        $this->imageIO = $imageIO;
    }

    /**
     * @inheritdoc
     */
    public function beforeSave($object)
    {
        $attribute = $this->getAttribute();
        $attrCode  = $attribute->getAttributeCode();
        $value     = $object->getData($attrCode);

        if ($value && is_array($value) && isset($value[0])) {
            if (!isset($value[0]['id'])) {
                $object->setData($attrCode, null);
            } elseif (isset($value[0]['url']) && $this->isWysiwygPath($value[0]['url'])) {
                $object->setData($attrCode, $value[0]['url']);
            } elseif (isset($value[0]['file'])) {
                $object->setData($attrCode, $value[0]['file']);
            }
        }

        return parent::beforeSave($object);
    }

    /**
     * @inheritdoc
     */
    public function afterSave($object)
    {
        $value = $object->getData($this->getAttribute()->getName());
        
        if ($value && is_array($value) && isset($value[0]) && isset($value[0]['url'])) {
            $imagepath = (string)$value['0']['url'];
            if($this->isWysiwygPath($imagepath)) {
                $this->imageIO->moveFileFromTmp($imagepath);
            }
        } elseif ($value && !$this->isWysiwygPath($value)) {
            $this->imageIO->moveFileFromTmp($value);
        }

        return $this;
    }

    /**
     * @inheritdoc
     */
    public function afterLoad($object)
    {
        $attrName = $this->getAttribute()->getName();

        if ($value = $object->getData($attrName)) {
            $isWysiwygPath = $this->isWysiwygPath($value);

            $object->setData(
                $attrName,
                [
                    [
                        'name' => $isWysiwygPath ? $this->getFileNameFromPath($value) : $value,
                        'url'  => $this->imageIO->getImageUrl($value, $isWysiwygPath),
                        'type' => 'image',
                    ]
                ]
            );
        }

        return parent::afterLoad($object);
    }

    /**
     * @param string $path
     *
     * @return bool
     */
    public function isWysiwygPath(string $path): bool
    {
        return (bool) strpos($path, 'wysiwyg');
    }

    /**
     * @param string $path
     *
     * @return string
     */
    private function getFileNameFromPath(string $path): string
    {
        $parts = explode('/', $path);

        return end($parts);
    }
}
Now we will add code to add this image upload button on the Product Add/edit form.

Add file at Magelearn/ProductImage/view/adminhtml/ui_component/product_form.xml file.
<?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="product_label_image">
		    <argument name="data" xsi:type="array">
		        <item name="config" xsi:type="array">
		            <item name="dataType" xsi:type="string">string</item>
		            <item name="label" xsi:type="string" translate="true">Product Label Image</item>
		            <item name="visible" xsi:type="boolean">true</item>
		            <item name="allowedExtensions" xsi:type="string">jpg jpeg gif png svg</item>
		            <item name="maxFileSize" xsi:type="number">2097152</item>
		            <item name="formElement" xsi:type="string">imageUploader</item>
		            <item name="previewTmpl" xsi:type="string">Magento_Catalog/image-preview</item>
		            <item name="elementTmpl" xsi:type="string">ui/form/element/uploader/uploader</item>
		            <item name="dataScope" xsi:type="string">product_label_image</item>
		            <item name="sortOrder" xsi:type="number">40</item>
		            <item name="uploaderConfig" xsi:type="array">
		                <item name="url" xsi:type="url" path="productimage/image/upload">
		                    <param name="target_element_id">product_label_image</param>
		                    <param name="type">image</param>
		                </item>
		            </item>
		            <item name="validation" xsi:type="array">
		                <item name="required-entry" xsi:type="boolean">false</item>
		            </item>
		        </item>
		    </argument>
		</field>
    </fieldset>
</form>

To make this uploaderConfig URL working properly, we will add etc/adminhtml/routes.xml file.

<?xml version="1.0" encoding="UTF-8"?>

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd">
    <router id="admin">
        <route id="productimage" frontName="productimage">
            <module name="Magelearn_ProductImage" />
        </route>
    </router>
</config>

Now note that we have added this `product_label_image` field as an input type 'text'.

So we need to modify this field to display it properly on product edit form.

For that add file at Magelearn/ProductImage/etc/adminhtml/di.xml

<?xml version="1.0" encoding="UTF-8"?>

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <virtualType name="Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\Pool">
        <arguments>
            <argument name="modifiers" xsi:type="array">
                <item name="add_product_image" xsi:type="array">
                    <item name="class" xsi:type="string">Magelearn\ProductImage\Ui\DataProvider\Product\Form\Modifier\Image</item>
                    <item name="sortOrder" xsi:type="number">200</item>
                </item>
            </argument>
        </arguments>
    </virtualType>
</config>

Now as per highlighted code above,
Add Magelearn/ProductImage/Ui/DataProvider/Product/Form/Modifier/Image.php file.

<?php

declare(strict_types = 1);

namespace Magelearn\ProductImage\Ui\DataProvider\Product\Form\Modifier;

use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\AbstractModifier;

class Image extends AbstractModifier
{
    /**
     * @param array $data
     *
     * @return array
     */
    public function modifyData(array $data): array
    {
        return $data;
    }

    /**
     * @param array $meta
     *
     * @return array
     */
    public function modifyMeta(array $meta): array
    {
        if (isset($meta['content']['children']['container_product_label_image'])) {
            $meta['content']['children']['container_product_label_image']['children'] = array_replace_recursive(
                $meta['content']['children']['container_product_label_image']['children'],
                [
                    'product_label_image' => [
                        'arguments' => [
                            'data' => [
                                'config' => [
                                    'visible' => 0,
                                ],
                            ]
                        ]
                    ]
                ]
            );
        }

        return $meta;
    }
}

Now we will give our imageUploader configuration for our custom backend model.

For that Add a file at etc/di.xml

<?xml version="1.0" encoding="UTF-8"?>

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="Magelearn\ProductImage\Model\Product\Attribute\Backend\ImageUploader">
        <arguments>
            <argument name="imageIO" xsi:type="object">Magelearn\ProductImage\Model\Virtual\ImageIO</argument>
        </arguments>
    </type>
    <type name="Magelearn\ProductImage\Controller\Adminhtml\Image\Upload">
        <arguments>
            <argument name="imageIO" xsi:type="object">Magelearn\ProductImage\Model\Virtual\ImageIO</argument>
        </arguments>
    </type>
    <virtualType name="Magelearn\ProductImage\Model\Virtual\ImageIO" type="Magelearn\ProductImage\Model\ImageIO">
        <arguments>
            <argument name="baseTmpPath" xsi:type="string">catalog/tmp/product/labels</argument>
            <argument name="basePath" xsi:type="string">catalog/product/labels</argument>
            <argument name="allowedExtensions" xsi:type="array">
                <item name="jpg" xsi:type="string">jpg</item>
                <item name="jpeg" xsi:type="string">jpeg</item>
                <item name="png" xsi:type="string">png</item>
                <item name="svg" xsi:type="string">svg</item>
                <item name="gif" xsi:type="string">gif</item>
            </argument>
        </arguments>
    </virtualType>
</config>

Now as per highlighted above code, we will add our controller and imageUploader file.

Add file at Controller/Adminhtml/Image/Upload.php

<?php

declare(strict_types = 1);

namespace Magelearn\ProductImage\Controller\Adminhtml\Image;

use Exception;
use Magelearn\ProductImage\Model\ImageIO;
use Magento\Backend\App\Action;
use Magento\Backend\App\Action\Context;
use Magento\Framework\App\Action\HttpPostActionInterface;
use Magento\Framework\Controller\ResultFactory;

class Upload extends Action
{
    /**
     * @var ImageIO
     */
    private $imageIO;

    /**
     * Upload constructor.
     *
     * @param Context $context
     * @param ImageIO $imageIO
     */
    public function __construct(
        Context $context,
        ImageIO $imageIO
    ) {
        $this->imageIO = $imageIO;
        parent::__construct($context);
    }
    
    /**
     * @return mixed
     */
    public function _isAllowed() {
        return $this->_authorization->isAllowed('Magelearn_ProductImage::upload');
    }
    
    /**
     * @inheritdoc
     */
    public function execute()
    {
        $imageId = $this->_request->getParam('param_name', 'product_label_image');
        
        try {
            $result = $this->imageIO->saveFileToTmpDir($imageId);
            $result['cookie'] = [
                'name' => $this->_getSession()->getName(),
                'value' => $this->_getSession()->getSessionId(),
                'lifetime' => $this->_getSession()->getCookieLifetime(),
                'path' => $this->_getSession()->getCookiePath(),
                'domain' => $this->_getSession()->getCookieDomain(),
            ];
        } catch (Exception $e) {
            $result = [
                'error'     => $e->getMessage(),
                'errorcode' => $e->getCode()
            ];
        }

        return $this->resultFactory->create(ResultFactory::TYPE_JSON)->setData($result);
    }
}

Add file at Model/ImageIO.php

<?php

namespace Magelearn\ProductImage\Model;

use Exception;
use Magento\Framework\App\Filesystem\DirectoryList;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Framework\UrlInterface;

class ImageIO
{

    const BASE_TMP_PATH = "catalog/tmp/product/labels";
    const BASE_PATH = "catalog/product/labels";
    const ALLOWED_EXTENSIONS = ['jpg', 'jpeg', 'gif', 'png', 'svg'];

    /**
     * @var string
     */
    public $baseTmpPath;
    /**
     * @var string
     */
    public $basePath;
    /**
     * @var string[]
     */
    public $allowedExtensions;
    /**
     * @var \Magento\MediaStorage\Helper\File\Storage\Database
     */
    private $coreFileStorageDatabase;
    /**
     * @var \Magento\Framework\Filesystem\Directory\WriteInterface
     */
    private $mediaDirectory;
    /**
     * @var \Magento\MediaStorage\Model\File\UploaderFactory
     */
    private $uploaderFactory;
    /**
     * @var \Magento\Store\Model\StoreManagerInterface
     */
    private $storeManager;
    /**
     * @var \Psr\Log\LoggerInterface
     */
    private $logger;

    /**
     * ImageUploader constructor.
     * @param \Magento\MediaStorage\Helper\File\Storage\Database $coreFileStorageDatabase
     * @param \Magento\Framework\Filesystem $filesystem
     * @param \Magento\MediaStorage\Model\File\UploaderFactory $uploaderFactory
     * @param \Magento\Store\Model\StoreManagerInterface $storeManager
     * @param \Psr\Log\LoggerInterface $logger
     * @throws \Magento\Framework\Exception\FileSystemException
     */
    public function __construct(
        \Magento\MediaStorage\Helper\File\Storage\Database $coreFileStorageDatabase,
        \Magento\Framework\Filesystem $filesystem,
        \Magento\MediaStorage\Model\File\UploaderFactory $uploaderFactory,
        \Magento\Store\Model\StoreManagerInterface $storeManager,
        \Psr\Log\LoggerInterface $logger
    ) {
        $this->coreFileStorageDatabase = $coreFileStorageDatabase;
        $this->mediaDirectory = $filesystem->getDirectoryWrite(DirectoryList::MEDIA);
        $this->uploaderFactory = $uploaderFactory;
        $this->storeManager = $storeManager;
        $this->logger = $logger;
        $this->baseTmpPath = self::BASE_TMP_PATH;
        $this->basePath = self::BASE_PATH;
        $this->allowedExtensions = self::ALLOWED_EXTENSIONS;
    }

    /**
     * @param $imageName
     * @return mixed
     * @throws LocalizedException
     */
    public function moveFileFromTmp($imageName)
    {
        $baseTmpPath = $this->getBaseTmpPath();
        $basePath = $this->getBasePath();
        $baseImagePath = $this->getFilePath($basePath, $imageName);
        $baseTmpImagePath = $this->getFilePath($baseTmpPath, $imageName);
        try {
            $this->coreFileStorageDatabase->copyFile(
                $baseTmpImagePath,
                $baseImagePath
            );
            $this->mediaDirectory->renameFile(
                $baseTmpImagePath,
                $baseImagePath
            );
        } catch (Exception $e) {
            throw new LocalizedException(
                __('Something went wrong while saving the file(s).')
            );
        }
        return $imageName;
    }

    /**
     * @return string
     */
    public function getBaseTmpPath()
    {
        return $this->baseTmpPath;
    }

    /**
     * @param $baseTmpPath
     */
    public function setBaseTmpPath($baseTmpPath)
    {
        $this->baseTmpPath = $baseTmpPath;
    }

    /**
     * @return string
     */
    public function getBasePath()
    {
        return $this->basePath;
    }

    /**
     * @param $basePath
     */
    public function setBasePath($basePath)
    {
        $this->basePath = $basePath;
    }

    /**
     * @param $path
     * @param $imageName
     * @return string
     */
    public function getFilePath($path, $imageName)
    {
        return rtrim($path, '/') . '/' . ltrim($imageName, '/');
    }

    /**
     * @param $fileId
     * @return mixed
     * @throws LocalizedException
     * @throws NoSuchEntityException
     */
    public function saveFileToTmpDir($fileId)
    {
        $baseTmpPath = $this->getBaseTmpPath();
        $uploader = $this->uploaderFactory->create(['fileId' => $fileId]);
        $uploader->setAllowedExtensions($this->getAllowedExtensions());
        $uploader->setAllowRenameFiles(true);
        $result = $uploader->save($this->mediaDirectory->getAbsolutePath($baseTmpPath));
        if (!$result) {
            throw new LocalizedException(
                __('File can not be saved to the destination folder.')
            );
        }

        $result['tmp_name'] = str_replace('\\', '/', $result['tmp_name']);
        $result['path'] = str_replace('\\', '/', $result['path']);
        $result['url'] = $this->storeManager
                ->getStore()
                ->getBaseUrl(
                    UrlInterface::URL_TYPE_MEDIA
                ) . $this->getFilePath($baseTmpPath, $result['file']);
        $result['name'] = $result['file'];
        if (isset($result['file'])) {
            try {
                $relativePath = rtrim($baseTmpPath, '/') . '/' . ltrim($result['file'], '/');
                $this->coreFileStorageDatabase->saveFile($relativePath);
            } catch (Exception $e) {
                $this->logger->critical($e);
                throw new LocalizedException(
                    __('Something went wrong while saving the file(s).')
                );
            }
        }
        return $result;
    }

    /**
     * @return string[]
     */
    public function getAllowedExtensions()
    {
        return $this->allowedExtensions;
    }

    /**
     * @param $allowedExtensions
     */
    public function setAllowedExtensions($allowedExtensions)
    {
        $this->allowedExtensions = $allowedExtensions;
    }

    public function saveMediaImage($imageName, $imagePath)
    {
        $baseTmpPath = $this->getBaseTmpPath();
        $basePath = $this->getBasePath();
        $baseImagePath = $this->getFilePath($basePath, $imageName);
        $mediaPath = substr($imagePath, 0, strpos($imagePath, "media"));
        $baseTmpImagePath = str_replace($mediaPath . "media/", "", $imagePath);
        if ($baseImagePath == $baseTmpImagePath) {
            return $imageName;
        }
        try {
            $this->mediaDirectory->copyFile(
                $baseTmpImagePath,
                $baseImagePath
            );
        } catch (Exception $e) {
            throw new LocalizedException(
                __('Something went wrong while saving the file(s).')
            );
        }
        return $imageName;
    }
    
    /**
     * @param string $value
     * @param bool   $fromWysiwyg
     *
     * @return string
     */
    public function getImageUrl(string $value, bool $fromWysiwyg = false): string
    {
        try {
            $basUrl = $this->storeManager->getStore()->getBaseUrl(UrlInterface::URL_TYPE_MEDIA);
            
            if (strpos($value, $basUrl) !== false) {
                return $value;
            }
            
            return $fromWysiwyg ? $basUrl . str_replace('/media/', '', $value) :
            $basUrl . $this->getFilePath($this->getBasePath(), $value);
        } catch (NoSuchEntityException $e) {
            return '';
        }
    }
}

After adding the above code, custom image upload button will be displayed at Product Add/Edit form.

Now we will check how to add this product custom image attribute`product_label_image` at frontend.

For that add layout file at view/frontend/layout/catalog_product_view.xml

<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceContainer name="product.info.main">
            <block class="Magento\Catalog\Block\Product\View" name="product.info.product_label_image" template="Magelearn_ProductImage::display-product-label-image.phtml" />
        </referenceContainer>
    </body>
</page>

Also at template file at view/frontend/templates/display-product-label-image.phtml

<?php
$product = $block->getProduct();
$product_label_image = $product->getProductLabelImage();
if(is_array($product_label_image) && isset($product_label_image['0']['type'])) {
    if($product_label_image['0']['type'] == 'image') {
        $product_label_image_name = $product_label_image['0']['name'];
        $product_label_image_url = $product_label_image['0']['url'];
    }
}
?>
<?php if ($product_label_image_url && $product_label_image_name): ?>
    <div class="product_label_image">
        <label class="label"><span><strong><?= __('Product Label Image: ')?></strong>
        <img src="<?= $block->escapeHtml($product_label_image_url) ?>" alt="<?= $block->escapeHtml($product_label_image_name) ?>">
        </span></label>
    </div>
<?php endif; ?>
    Add product Image upload field and preview image in Magento 2 Product UI Component Form | Vijay M. Rami