1

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:

  • Created issue in magento2 github: github.com/magento/magento2/issues/7023 – Aleksey Razbakov Oct 14 '16 at 17:09
  • Hmm if that's all you have then you're missing the part to actually make it show the value on the backend. Can you provide the full module? I've done something similar to category attributes, pretty sure it will not be that much different. – André Ferraz Oct 17 '16 at 14:25
  • That's all I have. I assume I need to add some layout xml and create some template for this field type – Aleksey Razbakov Oct 17 '16 at 17:02
  • I'll have a look at this when i get some free time today. – André Ferraz Oct 18 '16 at 12:03
  • 1
    @AndréFerraz can you please check my answer? – Aleksey Razbakov Oct 19 '16 at 14:13
10

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">
  • I did the second solution but always return this field is required when save despite uploading file. Also is it possible to have custom controller to save this file your own way. Thanks for sharing – Mohammed Gomma Jan 18 '17 at 8:23
  • What do you mean by second solution? You should use modifier class and you can have different modifiers for different field names. – Aleksey Razbakov Jan 18 '17 at 9:54
  • I am getting the Exception "$_FILES array is empty" - did you also encounter this? I am on 2.1.6 – simonthesorcerer Jun 1 '17 at 12:55
  • i am not able to make this work do it works ? – user1799722 Oct 14 '18 at 15:28
  • @AlekseyRazbakov can you tell will with above way can i create attribute type file or it adds new attribute as i tried but it did not worked – user1799722 Oct 14 '18 at 15:32
0

If you need to upload the files in product page tab, no need to create the product custom attribute. just install the following extension

Then php bin/magento setup:upgrade php bin/magento setup:static-content:deploy en_GB php bin/magento indexer:reindex php bin/magento cache:clean

working perfectly.

https://github.com/Sebwite/Magento2-Product-Downloads/tree/2.1-dev

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.