Magento 2. Добавление картинок в динамический массив

от автора

Приветствую вас, хабравчане! Немного набравшись смелости решил написать свою первую статью, точнее поделиться небольшим опытом, в интересной, как мне показалось теме, а именно как в динамический массив в конфиге, добавить загрузчик файлов.

Итак начнем.

Для начала создадим модуль, и базовую структуру модуля

Mr/ImageDynamicConfig/registration.php
<?php  \Magento\Framework\Component\ComponentRegistrar::register(     \Magento\Framework\Component\ComponentRegistrar::MODULE,     'Mr_ImageDynamicConfig',     __DIR__ );
Mr/ImageDynamicConfig/etc/module.xml
<?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="Mr_ImageDynamicConfig" setup_version="1.0.0"/> </config> 

Далее начнем описывать все необходимые элементы, шаг за шагом:

И первым на очереди, создадим сам конфиг:

Mr/ImageDynamicConfig/etc/adminhtml/system.xml
<?xml version="1.0"?>  <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"         xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd">     <system>         <tab id="mr" translate="label" sortOrder="400">             <label>Mr</label>         </tab>         <section id="swatch" translate="label" type="text" sortOrder="300" showInDefault="1" showInWebsite="1" showInStore="1">             <class>separator-top</class>             <label>Image Array Swatch</label>             <tab>mr</tab>             <resource>Mr_ImageDynamicConfig::config</resource>             <group id="image_serializer" translate="label" type="text" sortOrder="140" showInDefault="1" showInWebsite="1" showInStore="1">                 <field id="image" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="0">                     <label>Image</label>                     <frontend_model>Mr\ImageDynamicConfig\Block\Adminhtml\System\Config\ImageFields</frontend_model>                     <backend_model>Mr\ImageDynamicConfig\Model\Config\Backend\Serialized\ArraySerialized</backend_model>                     <upload_dir>var/uploads/swatch/image_serializer</upload_dir>                 </field>             </group>         </section>     </system> </config> 

Для динамического массива строка <frontend_model>Mr\ImageDynamicConfig\Block\Adminhtml\System\Config\ImageFields</frontend_model> совсем не нова, и класс ImageFields рендерит все основные колонки и показывает как они должны выглядеть

Mr/ImageDynamicConfig/Block/Adminhtml/System/Config/ImageFields.php
<?php declare(strict_types=1);  namespace Mr\ImageDynamicConfig\Block\Adminhtml\System\Config;  use Magento\Config\Block\System\Config\Form\Field\FieldArray\AbstractFieldArray;  class ImageFields extends AbstractFieldArray {     const IMAGE_FIELD = 'image';     const NAME_FIELD = 'name';     private $imageRenderer;      protected function _prepareToRender()     {         $this->addColumn(             self::IMAGE_FIELD,             [                 'label' => __('Image'),                 'renderer' => $this->getImageRenderer()             ]         );          $this->addColumn(             self::NAME_FIELD,             [                 'label' => __('Name'),             ]         );          $this->_addAfter       = false;         $this->_addButtonLabel = __('Add');     }      private function getImageRenderer()     {         if (!$this->imageRenderer) {             $this->imageRenderer = $this->getLayout()->createBlock(                 \Mr\ImageDynamicConfig\Block\Adminhtml\Form\Field\ImageColumn::class,                 '',                 ['data' => ['is_render_to_js_template' => true]]             );         }         return $this->imageRenderer;     } }

тут в методе _prepareToRender объявляем колонки, которые будут в динамическом массиве, и если в колонке есть поле отличное от текстового инпута, описываем для этого поля рендерер (метод getImageRenderer). На строке 38 рендерим блок \Mr\ImageDynamicConfig\Block\Adminhtml\Form\Field\ImageColumn, который и будет отдавать нам вместо инпута html — код с выбором файлов и отображением файла

Mr/ImageDynamicConfig/Block/Adminhtml/Form/Field/ImageColumn.php
<?php declare(strict_types=1);  namespace Mr\ImageDynamicConfig\Block\Adminhtml\Form\Field;  use Mr\ImageDynamicConfig\Block\Adminhtml\ImageButton;  class ImageColumn extends \Magento\Framework\View\Element\AbstractBlock {     public function setInputName(string $value)     {         return $this->setName($value);     }      public function setInputId(string $value)     {         return $this->setId($value);     }      protected function _toHtml(): string     {         $imageButton = $this->getLayout()             ->createBlock(ImageButton::class)             ->setData('id', $this->getId())             ->setData('name', $this->getName());         return $imageButton->toHtml();     } } 

В перегруженном методе _toHtml рендерим блок Mr\ImageDynamicConfig\Block\Adminhtml\ImageButton, который будет отдавать нам темплейт с html — кодом

Mr/ImageDynamicConfig/Block/Adminhtml//ImageButton.php
<?php declare(strict_types=1);  namespace Mr\ImageDynamicConfig\Block\Adminhtml;  class ImageButton extends \Magento\Backend\Block\Template {     protected $_template = 'Mr_ImageDynamicConfig::config/array_serialize/swatch_image.phtml';      private $assetRepository;      public function __construct(         \Magento\Backend\Block\Template\Context $context,         \Magento\Framework\View\Asset\Repository $assetRepository,         array $data = []     ) {         $this->assetRepository = $assetRepository;         parent::__construct($context, $data);     }      public function getAssertRepository(): \Magento\Framework\View\Asset\Repository     {         return $this->assetRepository;     } }

Публичный метод getAssertRepository нам нужен, чтобы вывести полный url на css файл в темплейте.

Mr/ImageDynamicConfig/view/adminhtml/templates/config/array_serialize/swatch_image.phtml
<?php /*** @var \Mr\ImageDynamicConfig\Block\Adminhtml\ImageButton $block */ $css = $block->getAssertRepository()->createAsset("Mr_ImageDynamicConfig::css/image_button.css"); ?> <link rel="stylesheet" type="text/css" media="all" href="<?php /* @escapeNotVerified */echo $css->getUrl() ?>"/>  <div class="upload-file" data-id="<?=$block->getId()?>">     <div class="upload-file__block upload-file__block_first">         <img class="upload-file__block__img" id="swatch_image_image_<?= $block->getId() ?>" src="">     </div>     <div class="upload-file__block">         <input class="upload-file__input" hidden type="file" name="<?= $block->getName() ?>" id="swatch_image_input_<?= $block->getId() ?>" value=""/>         <label class="upload-file__label" for="swatch_image_input_<?= $block->getId() ?>">             <?= __("File") ?>         </label>     </div>     <input class="upload-file__input" type="hidden" id="<?=$block->getId()?>"> </div>   <script type="text/javascript">     require(["jquery"], function (jq) {         jq(function () {             const id = "<?=$block->getId()?>"             const imageId = "swatch_image_image_<?=$block->getId()?>"             const data = jq("#" + id).val();             if (data) {                 jq("#" + imageId).attr("src", data)                 jq("#" + imageId).attr("value", data)             }         });     }); </script>

В этом темплейте отображается инпут для загрузки, и вывода загруженной картинки. С одной стороны очень странное решение сделать скрытый инпут:

<input class="upload-file__input" type="hidden" id="<?=$block->getId()?>">

а после из него вставлять в img тег значение:

jq(function () {             const id = "<?=$block->getId()?>"             const imageId = "swatch_image_image_<?=$block->getId()?>"             const data = jq("#" + id).val();             if (data) {                 jq("#" + imageId).attr("src", data)                 jq("#" + imageId).attr("value", data)             }         });

Но, когда Magento рендерит форму в конфиге, чтобы вставить туда значение, она пытается найти input с id и записать в value это значение. По-этому я сделал скрытый инпут и через jquery прокинул в source img путь на картинку

Таким образом, мы разобрали frontend_model и как вывести image input в динамический массив.

Теперь рассмотрим этап — загрузки картинок.

Для этого используется backend_model, и в обычных случаях, когда нужно просто добавить динамический массив в конфиг, то прокидываем в backend_model Magento\Config\Model\Config\Backend\Serialized\ArraySerialized и на этом все наши проблемы решены, но ArraySerialized не работает с загрузкой и сохранением картинок, и по этому на его основе делаем свой array serializer

Mr/ImageDynamicConfig/Model/Config/Backend/Serialized/ArraySerialized
<?php declare(strict_types=1);  namespace Mr\ImageDynamicConfig\Model\Config\Backend\Serialized;  use Magento\Framework\Serialize\Serializer\Json; use Mr\ImageDynamicConfig\Block\Adminhtml\System\Config\ImageFields;  class ArraySerialized extends \Magento\Config\Model\Config\Backend\Serialized\ArraySerialized {     private $imageUploaderFactory;     private $imageConfig;      public function __construct(         \Magento\Framework\Model\Context $context,         \Magento\Framework\Registry $registry,         \Magento\Framework\App\Config\ScopeConfigInterface $config,         \Magento\Framework\App\Cache\TypeListInterface $cacheTypeList,         \Mr\ImageDynamicConfig\Model\Config\ImageConfig $imageConfig,         \Mr\ImageDynamicConfig\Model\ImageUploaderFactory $imageUploaderFactory,         \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null,         \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null,         array $data = [],         Json $serializer = null     ) {         $this->imageUploaderFactory = $imageUploaderFactory;         $this->imageConfig = $imageConfig;         parent::__construct(             $context,             $registry,             $config,             $cacheTypeList,             $resource,             $resourceCollection,             $data,             $serializer         );     }      public function beforeSave(): ArraySerialized     {         $value = $this->getValue();         $value = $this->mapRows($value);         $this->setValue($value);         return parent::beforeSave();     }      private function mapRows(array $rows): array     {         $iconUploader = $this->imageUploaderFactory->create([             'path' => $this->getPath(),             'uploadDir' => $this->getUploadDir(),         ]);         $uploadedFiles = $iconUploader->upload();         $swatches = $this->imageConfig->getSwatches();         foreach ($rows as $id => $data) {             if (isset($uploadedFiles[$id])) {                 $rows[$id][ImageFields::IMAGE_FIELD] = $uploadedFiles[$id];                 continue;             }             if (!isset($swatches[$id])) {                 unset($swatches[$id]);             } else {                 $rows[$id] = $this->matchRow($data, $swatches[$id]);             }         }         return $rows;     }      private function matchRow(array $row, array $configTabIcon): array     {         foreach ($row as $fieldName => $value) {             if (is_array($value) && $fieldName == ImageFields::IMAGE_FIELD) {                 $row[ImageFields::IMAGE_FIELD] = $configTabIcon[ImageFields::IMAGE_FIELD];             }         }         return $row;     }      private function getUploadDir(): string     {         $fieldConfig = $this->getFieldConfig();          if (!array_key_exists('upload_dir', $fieldConfig)) {             throw new \Magento\Framework\Exception\LocalizedException(                 __('The base directory to upload file is not specified.')             );         }          if (is_array($fieldConfig['upload_dir'])) {             $uploadDir = $fieldConfig['upload_dir']['value'];             if (array_key_exists('scope_info', $fieldConfig['upload_dir'])                 && $fieldConfig['upload_dir']['scope_info']             ) {                 $uploadDir = $this->_appendScopeInfo($uploadDir);             }              if (array_key_exists('config', $fieldConfig['upload_dir'])) {                 $uploadDir = $this->getUploadDirPath($uploadDir);             }         } else {             $uploadDir = (string)$fieldConfig['upload_dir'];         }          return $uploadDir;     } }

Тут немного заострим внимание на методе mapRows, на строках 50-54 загружаем картинку, на строках 56-66 модифицируем данные из конфига, добавляем/заменяем картинку в массив конфига и остальные поля тоже добавляем/обновляем

класс ImageUploader:

Mr/ImageDynamicConfig/Model/ImageUploader.php
<?php declare(strict_types=1);  namespace Mr\ImageDynamicConfig\Model;  use Magento\MediaStorage\Model\File\Uploader;  class ImageUploader {     private $arrayFileModifier;     private $uploaderFactory;     private $uploadDir;     private $allowExtensions;      public function __construct(         \Mr\ImageDynamicConfig\Model\ArrayFileModifier $arrayFileModifier,         \Magento\MediaStorage\Model\File\UploaderFactory $uploaderFactory,         string $uploadDir,         array $allowExtensions     ) {         $this->arrayFileModifier = $arrayFileModifier;         $this->uploaderFactory = $uploaderFactory;         $this->uploadDir = $uploadDir;         $this->allowExtensions = $allowExtensions;     }      public function upload(): array     {         $result = [];         $files = $this->arrayFileModifier->modify();         if (!$files) {             return $result;         }          foreach ($files as $id => $file) {             try {                 $uploader = $this->uploaderFactory->create(['fileId' => $id]);                 $uploader->setAllowedExtensions($this->allowExtensions);                 $uploader->setAllowRenameFiles(true);                 $uploader->addValidateCallback('size', $this, 'validateMaxSize');                 $newFileName = $this->getNewFileName($uploader);                 $uploader->save($this->uploadDir, $newFileName);                 $result[$id] = $this->getFullFilPath($newFileName);             } catch (\Exception $e) {                 throw new \Magento\Framework\Exception\LocalizedException(__('%1', $e->getMessage()));             }         }         return $result;     }      private function getNewFileName(Uploader $uploader): string     {         return sprintf(             '%s.%s',             uniqid(),             $uploader->getFileExtension()         );     }      private function getFullFilPath(string $filename): string     {         return sprintf(             '/%s/%s',             $this->uploadDir,             $filename         );     } }

В этом классе есть строчка $files = $this->arrayFileModifier->modify(); Этот modifier нам нужен чтобы привести массив, который к нам пришел, из формы такого вида:

в понятный для аплоудера:

чтобы передать id $uploader = $this->uploaderFactory->create([‘fileId’ => $id]);

и аплоудер знал с чем ему работать.

И последний пазлик — класс для работы с конфигом

Mr/ImageDynamicConfig/Model/Config/ImageConfig
<?php declare(strict_types=1);  namespace Mr\ImageDynamicConfig\Model\Config;  use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\Serialize\SerializerInterface;  class ImageConfig {     const XML_PATH_IMAGE_SERIALIZER = 'swatch/image_serializer/';      private $scopeConfig;     private $serializer;      public function __construct(         SerializerInterface $serializer,         ScopeConfigInterface $scopeConfig     ) {         $this->scopeConfig = $scopeConfig;         $this->serializer = $serializer;     }          public function getSwatches(): array     {         $data = $this->scopeConfig->getValue(self::XML_PATH_IMAGE_SERIALIZER . 'image');         if (!$data) {             return [];         }         return $this->serializer->unserialize($data);     } }

И сам результат:

Эпилог

Репозиторий модуля на гитхабе

Надеюсь данная статья покажется кому-нибудь интересной и/или полезной. Если есть замечания/предложения/вопросы добро пожаловать в комментарии.

Благодарю за внимание.


ссылка на оригинал статьи https://habr.com/ru/post/658167/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *