What I would like to do is add a field to an admin page where a file can be downloaded and uploaded. A client would like to upload a csv file that is used by a module developt by an agency that was developing the site before us.

What I have is that I have been able to add a page to the backend, a download and an upload field.

enter image description here

<!-- app/code/Vendor/ModuleName/etc/adminhtml/system.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd">
        <section id="modulename" sortOrder="500" showInDefault="1" showInStore="1" showInWebsite="1">
            <label>Module Name</label>

            <group id="general" translate="label" showInDefault="1" showInWebsite="1" showInStore="1">

                <field id="download" translate="label" type="Vendor\ModuleName\Block\Adminhtml\Form\Field\Download" sortOrder="5" showInDefault="1">
                <field id="upload_plates_file" translate="label" type="Vendor\ModuleName\Block\Adminhtml\Form\Field\Upload" sortOrder="6" showInDefault="1">

And the two files; Upload.php and Download.php.

// app/code/Vendor/ModuleName/Block/Adminhtml/Form/Field/Upload.php
class Upload extends \Magento\Framework\Data\Form\Element\AbstractElement

    protected function _construct(): void

    public function getElementHtml(): string
        return parent::getElementHtml();
// app/code/Vendor/ModuleName/Block/Adminhtml/Form/Field/Download.php 
class Download extends \Magento\Framework\Data\Form\Element\AbstractElement

    public function getElementHtml(): string
        $buttonBlock = $this->getForm()->getParent()->getLayout()->createBlock(Button::class);
        $params = ['website' => $buttonBlock->getRequest()->getParam('website')];
        $url = $this->backendUrl->getUrl("*/*/exportPlatesCsv", $params);
        $data = [
            'label' => __('Export CSV'),
            'onclick' => "setLocation('" .
                $url .
                "somewhere/somefile.csv' )",
            'class' => '',
        return $buttonBlock->setData($data)->toHtml();

Now I understand that I need controllers for the actual upload and download action, and I have a app/code/Vendor/ModuleName/Controller/Adminhtml/System/Config/Download.php and a app/code/Elgentos/DownUploadPlatesCSV/Model/Config/Backend/Upload.php. And I have some scafolding inside there, but it does nothing and I wouldn't know where to begin to fix it.

I know this perhaps isn't a lot to go by and debugging something on a system you don't have access to is next to impossible but a nudge in the right direction or an article that covers this use case, which I have not been able to find, would be wonderful! (BTW using magento 2.4.5-p1)

1 Answer 1


With a little help form a colleague we figured it out. It was not as easy as I had expected it would be, but that's magento for you. Documentation is also lacking but that's magento for you...

What I had was good but I indeed needed a controller and a model. First the model:

namespace Vendor\ModuleName\Model\Config\Backend;

use Laminas\Stdlib\ParametersInterface;
use Magento\Framework\App\Cache\TypeListInterface;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\App\Config\Value;
use Magento\Framework\App\Filesystem\DirectoryList;
use Magento\Framework\App\Request\Http\Proxy;
use Magento\Framework\Data\Collection\AbstractDb;
use Magento\Framework\Exception\FileSystemException;
use Magento\Framework\Exception\ValidatorException;
use Magento\Framework\Filesystem;
use Magento\Framework\Filesystem\Directory\WriteFactory;
use Magento\Framework\Model\Context;
use Magento\Framework\Model\ResourceModel\AbstractResource;
use Magento\Framework\Registry;

class PlatesCsv extends Value
    public const DIRECTORY_NAME = 'directoryname';
    public const FILE_NAME = 'filename.csv';

    public function __construct(
        private readonly Proxy $request,
        private readonly WriteFactory $writeFactory,
        private readonly Filesystem $filesystem,
        Context $context,
        Registry $registry,
        ScopeConfigInterface $config,
        TypeListInterface $cacheTypeList,
        AbstractResource $resource = null,
        AbstractDb $resourceCollection = null,
        array $data = []
    ) {
        parent::__construct($context, $registry, $config, $cacheTypeList,
            $resource, $resourceCollection, $data);

     * @return $this
     * @throws ValidatorException
     * @throws FileSystemException
    public function afterSave(): static
        if(!$this->request->getFiles('groups')) {
            return parent::afterSave();

        /** @var ParametersInterface|mixed $uploadedPlatesFile */
        $uploadedPlatesFile = $this->request->getFiles('groups')['general']['fields']['upload_plates_file']['value'];

        $rootDir = $this->filesystem->getDirectoryRead(DirectoryList::ROOT);
        $uploadDir = $this->writeFactory->create($rootDir->getAbsolutePath(self::DIRECTORY_NAME));
        $realpath = $uploadDir->getDriver()->getRealPathSafety($uploadDir->getAbsolutePath(self::FILE_NAME));
        return $this;

And the controller, the execute() function was a lot of trail and error:

// app/code/Vendor/ModuleName/Controller/Adminhtml/System/Config/ControllerName.php

namespace Vendor\ModuleName\Controller\Adminhtml\System\Config;

use Vendor\ModuleName\Model\Config\Backend\ModelName;
use Magento\Backend\App\Action\Context;
use Magento\Config\Controller\Adminhtml\System\AbstractConfig;
use Magento\Config\Controller\Adminhtml\System\ConfigSectionChecker;
use Magento\Config\Model\Config\Structure;
use Magento\Framework\App\Filesystem\DirectoryList;
use Magento\Framework\App\Response\Http\FileFactory;
use Magento\Framework\App\ResponseInterface;
use Psr\Log\LoggerInterface;

class ControllerName extends AbstractConfig
    public function __construct(
        Context $context,
        Structure $configStructure,
        ConfigSectionChecker $sectionChecker,
        private readonly FileFactory $fileFactory,
        private readonly LoggerInterface $logger,
    ) {
        parent::__construct($context, $configStructure, $sectionChecker);

    public function execute(): ?ResponseInterface
        try {
            return $this->fileFactory->create(
                    'type'  => 'filename',
                    'value' => ModelName::DIRECTORY_NAME . '/' . ModelName::FILE_NAME,
        } catch (\Exception $e) {
            return null;

The file will now be saved at the following location, relative to the Magento root dir: magento_root/directory_name/filename.csv.

  • 1
    that sound great. Please mark your answer as “accepted” answer, that indicates your issue was resolved by the solution in your answer.
    – Tu Van
    Commented Jan 9, 2023 at 8:05

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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