Magento 2 – Show Value of Product Attribute of Type File in Backend

magento2product-attribute

As Administrator I want to be able to upload files for each products in order to show link on frontend.

I created upgrade script to create attribute with this properties:

'type' => 'varchar',
'input' => 'file',
'label' => 'Datasheet',
'required' => false,
'visible_on_front' => true,
'backend' => 'My\Module\Model\Product\Attribute\Backend\Datasheet'

Here is my backend model:

namespace My\Module\Model\Product\Attribute\Backend;

use Magento\Framework\App\Filesystem\DirectoryList;

class Datasheet extends \Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend
{
    const MEDIA_SUBFOLDER = 'datasheet';

    protected $_uploaderFactory;
    protected $_filesystem;
    protected $_fileUploaderFactory;
    protected $_logger;

    /**
     * Construct
     *
     * @param \Psr\Log\LoggerInterface $logger
     * @param \Magento\Framework\Filesystem $filesystem
     * @param \Magento\Framework\File\UploaderFactory $uploaderFactory
     */
    public function __construct(
        \Psr\Log\LoggerInterface $logger,
        \Magento\Framework\Filesystem $filesystem,
        \Magento\Framework\File\UploaderFactory $uploaderFactory
    ) {
        $this->_filesystem = $filesystem;
        $this->_uploaderFactory = $uploaderFactory;
        $this->_logger = $logger;
    }

    public function afterSave($object)
    {
        $attributeName = $this->getAttribute()->getName();
        $fileName = $this->uploadFileAndGetName($attributeName, $this->_filesystem->getDirectoryWrite(DirectoryList::MEDIA)->getAbsolutePath(self::MEDIA_SUBFOLDER));

        if ($fileName) {
            $object->setData($attributeName, $fileName);
            $this->getAttribute()->getEntity()->saveAttribute($object, $attributeName);
        }

        return $this;
    }

    public function uploadFileAndGetName($input, $destinationFolder)
    {
        try {
            $uploader = $this->_uploaderFactory->create(array('fileId' => 'product['.$input.']'));
            $uploader->setAllowedExtensions(['pdf']);
            $uploader->setAllowRenameFiles(true);
            $uploader->setFilesDispersion(true);
            $uploader->setAllowCreateFolders(true);
            $uploader->save($destinationFolder);

            return $uploader->getUploadedFileName();
        } catch (\Exception $e) {
            if ($e->getCode() != \Magento\Framework\File\Uploader::TMP_NAME_EMPTY) {
                throw new \FrameworkException($e->getMessage());
            }
        }

        return '';
    }
}

Attribute is shown in backend and saves value in database.

PROBLEM

Attribute value is not shown in backend.

References:

Best Answer

Investigation

file input is not implemented in Magento 2.1 yet.

in vendor/magento/module-ui/view/base/web/templates/form/element/media.html there is only uploader and no view and delete button:

<!--
/**
 * Copyright © 2016 Magento. All rights reserved.
 * See COPYING.txt for license details.
 */
-->
<input class="admin__control-file" type="file" data-bind="
    hasFocus: focused,
    attr: {
        name: inputName,
        placeholder: placeholder,
        'aria-describedby': noticeId,
        id: uid,
        disabled: disabled,
        form: formId
    }"
/>

It will be implemented in next Magento2 release hopefully.

So I thought to implement a custom renderer, but

Solution

Magento2 way is to use UI components and PHP Modifiers

Create 3 files:

etc/adminhtml/di.xml - dependency injection configuration for adminhtml

<?xml version="1.0"?>
<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="your_module_datasheet" xsi:type="array">
                    <item name="class" xsi:type="string">Vendor\Module\Ui\DataProvider\Product\Form\Modifier\Datasheet</item>
                    <item name="sortOrder" xsi:type="number">150</item>
                </item>
            </argument>
        </arguments>
    </virtualType>
</config>

your_module_datasheet can be any name.

With sortOrder from 10 to 50 it didn't work for me. $meta variable in Modifier was empty.

Ui/DataProvider/Product/Form/Modifier/Datasheet.php - php modifier

<?php   
namespace Vendor\Module\Ui\DataProvider\Product\Form\Modifier;

use Magento\Catalog\Model\Locator\LocatorInterface;
use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\AbstractModifier;
use Magento\Framework\Stdlib\ArrayManager;
use Magento\Framework\UrlInterface;

/**
 * Data provider for "Datasheet" field of product page
 */
class Datasheet extends AbstractModifier
{
    /**
     * @param LocatorInterface            $locator
     * @param UrlInterface                $urlBuilder
     * @param ArrayManager                $arrayManager
     */
    public function __construct(
        LocatorInterface $locator,
        UrlInterface $urlBuilder,
        ArrayManager $arrayManager
    ) {
        $this->locator = $locator;
        $this->urlBuilder = $urlBuilder;
        $this->arrayManager = $arrayManager;
    }

    public function modifyMeta(array $meta)
    {
        $fieldCode = 'datasheet';
        $elementPath = $this->arrayManager->findPath($fieldCode, $meta, null, 'children');
        $containerPath = $this->arrayManager->findPath(static::CONTAINER_PREFIX . $fieldCode, $meta, null, 'children');

        if (!$elementPath) {
            return $meta;
        }

        $meta = $this->arrayManager->merge(
            $containerPath,
            $meta,
            [
                'children'  => [
                    $fieldCode => [
                        'arguments' => [
                            'data' => [
                                'config' => [
                                    'elementTmpl'   => 'Vendor_Module/grid/filters/elements/datasheet',
                                ],
                            ],
                        ],
                    ]
                ]
            ]
        );

        return $meta;
    }

    /**
     * {@inheritdoc}
     */
    public function modifyData(array $data)
    {
        return $data;
    }
}

view/adminhtml/web/template/grid/filters/elements/datasheet.html - knockout js template

<!-- ko if: $parent.source.data.product[code] -->
<div>
    <!-- todo: dynamically get path to file from config or controller -->
    <a attr="href: '/pub/media/datasheet'+$parent.source.data.product[code]" text="$parent.source.data.product[code]"></a>
    <label attr="for: uid+'_delete'">
        <!-- todo: generate name -->
        <input type="checkbox" attr="name: 'product['+code + '_delete]', id: uid+'_delete', form: formId">
        <span data-bind="i18n:'Delete'"></span>
    </label>
</div>
<!-- /ko -->
<input class="admin__control-file" type="file" data-bind="
    hasFocus: focused,
    attr: {
        name: inputName,
        placeholder: placeholder,
        'aria-describedby': noticeId,
        id: uid,
        disabled: disabled,
        form: formId
    }"
/>

It is still looks dirty to me and a lot of things to improve.

Alternatives

I found some interesting files, which can bring you other ideas how to solve it:

vendor/magento/module-catalog/view/adminhtml/ui_component/design_config_form.xml

<item name="formElement" xsi:type="string">fileUploader</item>

vendor/magento/module-ui/view/base/ui_component/etc/definition.xml

<fileUploader class="Magento\Ui\Component\Form\Element\DataType\Media">
Related Topic