<?php
namespace Magetu\BundleSelectionImage\Ui\DataProvider\Product\Form\Modifier;
use Magento\Bundle\Model\Product\Type;
use Magento\Bundle\Model\ResourceModel\Selection as SelectionResource;
use Magento\Bundle\Model\SelectionFactory;
use Magento\Bundle\Ui\DataProvider\Product\Form\Modifier\BundlePanel;
use Magento\Catalog\Model\Category\FileInfo;
use Magento\Catalog\Model\Locator\LocatorInterface;
use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\AbstractModifier;
use Magento\Framework\UrlInterface;
class SelectionImage extends AbstractModifier
{
public const FIELD_IMAGE = 'selection_image';
/**
* @var LocatorInterface
*/
private $locator;
/**
* @var UrlInterface
*/
private $urlBuilder;
/**
* @var FileInfo
*/
private $fileInfo;
/**
* @var SelectionFactory
*/
private $selectionFactory;
/**
* @var SelectionResource
*/
private $selectionResource;
public function __construct(
LocatorInterface $locator,
UrlInterface $urlBuilder,
FileInfo $fileInfo,
SelectionFactory $selectionFactory,
SelectionResource $selectionResource
) {
$this->locator = $locator;
$this->urlBuilder = $urlBuilder;
$this->fileInfo = $fileInfo;
$this->selectionFactory = $selectionFactory;
$this->selectionResource = $selectionResource;
}
/**
* {@inheritdoc}
*
* Converts selection image data to acceptable for rendering format
* Display selection image
*/
public function modifyData(array $data)
{
$product = $this->locator->getProduct();
$modelId = $product->getId();
$isBundleProduct = $product->getTypeId() === Type::TYPE_CODE;
if ($isBundleProduct && $modelId) {
$selectionModel = $this->selectionFactory->create();
foreach ($data[$modelId][BundlePanel::CODE_BUNDLE_OPTIONS][BundlePanel::CODE_BUNDLE_OPTIONS] as &$option) {
foreach ($option['bundle_selections'] as &$selection) {
$this->selectionResource->load($selectionModel, $selection['selection_id']);
$selectionImage = $selectionModel->getData('selection_image');
if ($selectionImage !== null && $this->fileInfo->isExist($selectionImage)) {
$stat = $this->fileInfo->getStat($selectionImage);
$mime = $this->fileInfo->getMimeType($selectionImage);
// phpcs:ignore Magento2.Functions.DiscouragedFunction
$imageUrl = $product->getStore()->getBaseUrl() . $selectionImage;
$imageRendering = [];
$imageRendering[0]['name'] = basename($selectionImage);
$imageRendering[0]['url'] = $imageUrl;
$imageRendering[0]['size'] = $stat['size'];
$imageRendering[0]['type'] = $mime;
$selectionModel->setData('selection_image', $imageRendering);
}
$selection['selection_image'] = $selectionModel->getData('selection_image');
}
}
}
return $data;
}
/**
* Add selection image field
*
* @param array $meta
* @return array
*/
public function modifyMeta(array $meta) {
if ($this->locator->getProduct()->getTypeId() === Type::TYPE_CODE) {
$groupCode = BundlePanel::CODE_BUNDLE_DATA;
$meta[$groupCode]['children']['bundle_options']['children']['record']['children']
['product_bundle_container']['children']['bundle_selections']
['children']['record']['children'][static::FIELD_IMAGE] = $this->getSelectionImageFieldConfig();
}
return $meta;
}
/**
* Get selection image field config
*
* @return array
*/
private function getSelectionImageFieldConfig()
{
return [
'arguments' => [
'data' => [
'config' => [
'componentType' => 'imageUploader',
'formElement' => 'imageUploader',
'template' => 'Magetu_BundleSelectionImage/form/element/uploader/image',
'fileInputName' => static::FIELD_IMAGE,
'uploaderConfig' => [
'url' => $this->urlBuilder->getUrl(
'bundleselectionimage/product_image/upload'
),
],
'dataScope' => static::FIELD_IMAGE,
'sortOrder' => 129,// Before the last field (action_delete)
],
],
],
];
}
}
<?php
namespace Magetu\BundleSelectionImage\Ui\DataProvider\Product\Form\Modifier;
use Magento\Bundle\Model\Product\Type;
use Magento\Bundle\Model\ResourceModel\Selection as SelectionResource;
use Magento\Bundle\Model\SelectionFactory;
use Magento\Bundle\Ui\DataProvider\Product\Form\Modifier\BundlePanel;
use Magento\Catalog\Model\Category\FileInfo;
use Magento\Catalog\Model\Locator\LocatorInterface;
use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\AbstractModifier;
use Magento\Framework\UrlInterface;
class SelectionImage extends AbstractModifier
{
public const FIELD_IMAGE = 'selection_image';
/**
* @var LocatorInterface
*/
private $locator;
/**
* @var UrlInterface
*/
private $urlBuilder;
/**
* @var FileInfo
*/
private $fileInfo;
/**
* @var SelectionFactory
*/
private $selectionFactory;
/**
* @var SelectionResource
*/
private $selectionResource;
public function __construct(
LocatorInterface $locator,
UrlInterface $urlBuilder,
FileInfo $fileInfo,
SelectionFactory $selectionFactory,
SelectionResource $selectionResource
) {
$this->locator = $locator;
$this->urlBuilder = $urlBuilder;
$this->fileInfo = $fileInfo;
$this->selectionFactory = $selectionFactory;
$this->selectionResource = $selectionResource;
}
/**
* {@inheritdoc}
*
* Converts selection image data to acceptable for rendering format
* Display selection image
*/
public function modifyData(array $data)
{
$product = $this->locator->getProduct();
$modelId = $product->getId();
$isBundleProduct = $product->getTypeId() === Type::TYPE_CODE;
if ($isBundleProduct && $modelId) {
$selectionModel = $this->selectionFactory->create();
foreach ($data[$modelId][BundlePanel::CODE_BUNDLE_OPTIONS][BundlePanel::CODE_BUNDLE_OPTIONS] as &$option) {
foreach ($option['bundle_selections'] as &$selection) {
$this->selectionResource->load($selectionModel, $selection['selection_id']);
$selectionImage = $selectionModel->getData('selection_image');
if ($selectionImage !== null && $this->fileInfo->isExist($selectionImage)) {
$stat = $this->fileInfo->getStat($selectionImage);
$mime = $this->fileInfo->getMimeType($selectionImage);
// phpcs:ignore Magento2.Functions.DiscouragedFunction
$imageUrl = $product->getStore()->getBaseUrl() . $selectionImage;
$imageRendering = [];
$imageRendering[0]['name'] = basename($selectionImage);
$imageRendering[0]['url'] = $imageUrl;
$imageRendering[0]['size'] = $stat['size'];
$imageRendering[0]['type'] = $mime;
$selectionModel->setData('selection_image', $imageRendering);
}
$selection['selection_image'] = $selectionModel->getData('selection_image');
}
}
}
return $data;
}
/**
* Add selection image field
*
* @param array $meta
* @return array
*/
public function modifyMeta(array $meta) {
$groupCode = BundlePanel::CODE_BUNDLE_DATA;
$meta[$groupCode]['children']['bundle_options']['children']['record']['children']
['product_bundle_container']['children']['bundle_selections']
['children']['record']['children'][static::FIELD_IMAGE] = $this->getSelectionImageFieldConfig();
return $meta;
}
/**
* Get selection image field config
*
* @return array
*/
private function getSelectionImageFieldConfig()
{
return [
'arguments' => [
'data' => [
'config' => [
'componentType' => 'imageUploader',
'formElement' => 'imageUploader',
'template' => 'Magetu_BundleSelectionImage/form/element/uploader/image',
'fileInputName' => static::FIELD_IMAGE,
'uploaderConfig' => [
'url' => $this->urlBuilder->getUrl(
'bundleselectionimage/product_image/upload'
),
],
'dataScope' => static::FIELD_IMAGE,
'sortOrder' => 129,// Before the last field (action_delete)
],
],
],
];
}
}
<?php
namespace Magetu\BundleSelectionImage\Ui\DataProvider\Product\Form\Modifier;
use Magento\Bundle\Model\Product\Type;
use Magento\Bundle\Model\ResourceModel\Selection as SelectionResource;
use Magento\Bundle\Model\SelectionFactory;
use Magento\Bundle\Ui\DataProvider\Product\Form\Modifier\BundlePanel;
use Magento\Catalog\Model\Category\FileInfo;
use Magento\Catalog\Model\Locator\LocatorInterface;
use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\AbstractModifier;
use Magento\Framework\UrlInterface;
class SelectionImage extends AbstractModifier
{
public const FIELD_IMAGE = 'selection_image';
/**
* @var LocatorInterface
*/
private $locator;
/**
* @var UrlInterface
*/
private $urlBuilder;
/**
* @var FileInfo
*/
private $fileInfo;
/**
* @var SelectionFactory
*/
private $selectionFactory;
/**
* @var SelectionResource
*/
private $selectionResource;
public function __construct(
LocatorInterface $locator,
UrlInterface $urlBuilder,
FileInfo $fileInfo,
SelectionFactory $selectionFactory,
SelectionResource $selectionResource
) {
$this->locator = $locator;
$this->urlBuilder = $urlBuilder;
$this->fileInfo = $fileInfo;
$this->selectionFactory = $selectionFactory;
$this->selectionResource = $selectionResource;
}
/**
* {@inheritdoc}
*
* Converts selection image data to acceptable for rendering format
* Display selection image
*/
public function modifyData(array $data)
{
$product = $this->locator->getProduct();
$modelId = $product->getId();
$isBundleProduct = $product->getTypeId() === Type::TYPE_CODE;
if ($isBundleProduct && $modelId) {
$selectionModel = $this->selectionFactory->create();
foreach ($data[$modelId][BundlePanel::CODE_BUNDLE_OPTIONS][BundlePanel::CODE_BUNDLE_OPTIONS] as &$option) {
foreach ($option['bundle_selections'] as &$selection) {
$this->selectionResource->load($selectionModel, $selection['selection_id']);
$selectionImage = $selectionModel->getData('selection_image');
if ($selectionImage !== null && $this->fileInfo->isExist($selectionImage)) {
$stat = $this->fileInfo->getStat($selectionImage);
$mime = $this->fileInfo->getMimeType($selectionImage);
// phpcs:ignore Magento2.Functions.DiscouragedFunction
$imageUrl = $product->getStore()->getBaseUrl() . $selectionImage;
$imageRendering = [];
$imageRendering[0]['name'] = basename($selectionImage);
$imageRendering[0]['url'] = $imageUrl;
$imageRendering[0]['size'] = $stat['size'];
$imageRendering[0]['type'] = $mime;
$selectionModel->setData('selection_image', $imageRendering);
}
$selection['selection_image'] = $selectionModel->getData('selection_image');
}
}
}
return $data;
}
/**
* Add selection image field
*
* @param array $meta
* @return array
*/
public function modifyMeta(array $meta) {
if ($this->locator->getProduct()->getTypeId() === Type::TYPE_CODE) {
$groupCode = BundlePanel::CODE_BUNDLE_DATA;
$meta[$groupCode]['children']['bundle_options']['children']['record']['children']
['product_bundle_container']['children']['bundle_selections']
['children']['record']['children'][static::FIELD_IMAGE] = $this->getSelectionImageFieldConfig();
}
return $meta;
}
/**
* Get selection image field config
*
* @return array
*/
private function getSelectionImageFieldConfig()
{
return [
'arguments' => [
'data' => [
'config' => [
'componentType' => 'imageUploader',
'formElement' => 'imageUploader',
'template' => 'Magetu_BundleSelectionImage/form/element/uploader/image',
'fileInputName' => static::FIELD_IMAGE,
'uploaderConfig' => [
'url' => $this->urlBuilder->getUrl(
'bundleselectionimage/product_image/upload'
),
],
'dataScope' => static::FIELD_IMAGE,
'sortOrder' => 129,// Before the last field (action_delete)
],
],
],
];
}
}
In this post, I'll give instructions on how to create and save a custom image in the bundle product custom selection image step-by-step.
- Create a modifier class which is defined in
di.xml
in the step 4 in order to modify bundle selection structure to add the selection image field:
Due to Magento 2.4.3 replacedreplacing the code of method to map product link to bundle selection model, the content of ourfor LinkManagement model class should be different:
<?php
namespace Magetu\BundleSelectionImage\Model;
use Magento\Bundle\Api\Data\LinkInterface;
use Magento\Bundle\Api\Data\LinkInterfaceFactory;
use Magento\Bundle\Model\ResourceModel\Bundle;
use Magento\Bundle\Model\ResourceModel\BundleFactory;
use Magento\Bundle\Model\ResourceModel\Option\CollectionFactory;
use Magento\Bundle\Model\Selection;
use Magento\Bundle\Model\SelectionFactory;
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Catalog\Model\ImageUploader;
use Magento\Catalog\Model\Product;
use Magento\Framework\Api\DataObjectHelper;
use Magento\Framework\App\Filesystem\DirectoryList;
use Magento\Framework\EntityManager\MetadataPool;
use Magento\Framework\Exception\CouldNotSaveException;
use Magento\Framework\Exception\FileSystemException;
use Magento\Framework\Exception\InputException;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Framework\Filesystem;
use Magento\Framework\Filesystem\Directory\WriteInterface;
use Magento\Framework\Message\Manager as MessageManager;
use Magento\Store\Model\StoreManagerInterface;
class LinkManagement extends \Magento\Bundle\Model\LinkManagement
{
/**
* @var StoreManagerInterface
*/
private $storeManager;
/**
* @var MetadataPool
*/
private $metadataPool;
/**
* @var ImageUploader
*/
private $imageUploader;
/**
* @var MessageManager
*/
private $messageManager;
public function __construct(
ProductRepositoryInterface $productRepository,
LinkInterfaceFactory $linkFactory,
SelectionFactory $bundleSelection,
BundleFactory $bundleFactory,
CollectionFactory $optionCollection,
StoreManagerInterface $storeManager,
DataObjectHelper $dataObjectHelper,
MetadataPool $metadataPool,
ImageUploader $imageUploader,
MessageManager $messageManager
) {
parent::__construct(
$productRepository,
$linkFactory,
$bundleSelection,
$bundleFactory,
$optionCollection,
$storeManager,
$dataObjectHelper,
$metadataPool
);
$this->storeManager = $storeManager;
$this->metadataPool = $metadataPool;
$this->imageUploader = $imageUploader;
$this->messageManager = $messageManager;
}
/**
* This method has same content as in the parent class
* Declare it here to allow override mapProductLinkToBundleSelectionModel which is private
*
* @inheritDoc
*
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
* @SuppressWarnings(PHPMD.NPathComplexity)
*/
public function saveChild(
$sku,
LinkInterface $linkedProduct
) {
$product = $this->productRepository->get($sku, true);
if ($product->getTypeId() != Product\Type::TYPE_BUNDLE) {
throw new InputException(
__('The product with the "%1" SKU isn\'t a bundle product.', [$product->getSku()])
);
}
/** @var Product $linkProductModel */
$linkProductModel = $this->productRepository->get($linkedProduct->getSku());
if ($linkProductModel->isComposite()) {
throw new InputException(__('The bundle product can\'t contain another composite product.'));
}
if (!$linkedProduct->getId()) {
throw new InputException(__('The product link needs an ID field entered. Enter and try again.'));
}
/** @var Selection $selectionModel */
$selectionModel = $this->bundleSelection->create();
$selectionModel->load($linkedProduct->getId());
if (!$selectionModel->getId()) {
throw new InputException(
__(
'The product link with the "%1" ID field wasn\'t found. Verify the ID and try again.',
[$linkedProduct->getId()]
)
);
}
$selectionModel = $this->mapProductLinkToBundleSelectionModel(
$selectionModel,
$linkedProduct,
$product,
(int)$linkProductModel->getId()
);
try {
$selectionModel->save();
} catch (\Exception $e) {
throw new CouldNotSaveException(__('Could not save child: "%1"', $e->getMessage()), $e);
}
return true;
}
/**
* This method has same content as in the parent class
* Declare it here to allow override mapProductLinkToBundleSelectionModel which is private
*
* @inheritDoc
*
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
*/
public function addChild(
ProductInterface $product,
$optionId,
LinkInterface $linkedProduct
) {
if ($product->getTypeId() != Product\Type::TYPE_BUNDLE) {
throw new InputException(
__('The product with the "%1" SKU isn\'t a bundle product.', $product->getSku())
);
}
$linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField();
$options = $this->optionCollection->create();
$options->setIdFilter($optionId);
$options->setProductLinkFilter($product->getData($linkField));
$existingOption = $options->getFirstItem();
if (!$existingOption->getId()) {
throw new InputException(
__(
'Product with specified sku: "%1" does not contain option: "%2"',
[$product->getSku(), $optionId]
)
);
}
/* @var $resource Bundle */
$resource = $this->bundleFactory->create();
$selections = $resource->getSelectionsData($product->getData($linkField));
/** @var Product $linkProductModel */
$linkProductModel = $this->productRepository->get($linkedProduct->getSku());
if ($linkProductModel->isComposite()) {
throw new InputException(__('The bundle product can\'t contain another composite product.'));
}
if ($selections) {
foreach ($selections as $selection) {
if ($selection['option_id'] == $optionId &&
$selection['product_id'] == $linkProductModel->getEntityId() &&
$selection['parent_product_id'] == $product->getData($linkField)) {
if (!$product->getCopyFromView()) {
throw new CouldNotSaveException(
__(
'Child with specified sku: "%1" already assigned to product: "%2"',
[$linkedProduct->getSku(), $product->getSku()]
)
);
}
return $this->bundleSelection->create()->load($linkProductModel->getEntityId());
}
}
}
$selectionModel = $this->bundleSelection->create();
$selectionModel = $this->mapProductLinkToBundleSelectionModel(
$selectionModel,
$linkedProduct,
$product,
(int)$linkProductModel->getEntityId()
);
$selectionModel->setOptionId($optionId);
try {
$selectionModel->save();
$resource->addProductRelation($product->getData($linkField), $linkProductModel->getEntityId());
} catch (\Exception $e) {
throw new CouldNotSaveException(__('Could not save child: "%1"', $e->getMessage()), $e);
}
return (int)$selectionModel->getId();
}
/**
* Fill selection model with product link data.
* Save selection image value
*
* @param Selection $selectionModel
* @param LinkInterface $productLink
* @param ProductInterface $parentProduct
* @param int $linkedProductId
* @return Selection
* @throws FileSystemException
* @throws NoSuchEntityException
*/
private function mapProductLinkToBundleSelectionModel(
Selection $selectionModel,
LinkInterface $productLink,
ProductInterface $parentProduct,
int $linkedProductId
): Selection {
$linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField();
$selectionModel->setProductId($linkedProductId);
$selectionModel->setParentProductId($parentProduct->getData($linkField));
if ($productLink->getSelectionId() !== null) {
$selectionModel->setSelectionId($productLink->getSelectionId());
}
if ($productLink->getOptionId() !== null) {
$selectionModel->setOptionId($productLink->getOptionId());
}
if ($productLink->getPosition() !== null) {
$selectionModel->setPosition($productLink->getPosition());
}
if ($productLink->getQty() !== null) {
$selectionModel->setSelectionQty($productLink->getQty());
}
if ($productLink->getPriceType() !== null) {
$selectionModel->setSelectionPriceType($productLink->getPriceType());
}
if ($productLink->getPrice() !== null) {
$selectionModel->setSelectionPriceValue($productLink->getPrice());
}
if ($productLink->getCanChangeQuantity() !== null) {
$selectionModel->setSelectionCanChangeQty($productLink->getCanChangeQuantity());
}
if ($productLink->getIsDefault() !== null) {
$selectionModel->setIsDefault($productLink->getIsDefault());
}
$selectionModel->setWebsiteId((int)$this->storeManager->getStore($parentProduct->getStoreId())->getWebsiteId());
// Start customize
$selectionImage = $thisnull;
if ($productLink->getSelectionImage($parentProduct,) !== null) {
$selectionImage = $this->getSelectionImage($productLink->getSelectionImage());
}
$selectionModel->setSelectionImage($selectionImage);
// End customize
return $selectionModel;
}
/**
* Move uploaded selection image file and get selection image path
* @param $parentProduct
* @param $selectionImageData
* @return string|null
* @throws FileSystemExceptionNoSuchEntityException
*/
private function getSelectionImage($parentProduct, $selectionImageData)
{
$selectionImage = null;
if ($selectionImageData$store !=== null$this->storeManager->getStore() {;
$baseMediaDir = $parentProduct->getStore()$store->getBaseMediaDir();
$selectionImageName$imageName = $selectionImageData[0]['name'];
if (isset($selectionImageData[0]['tmp_name'])) {
// Save new uploaded selection image
try {
$newImgRelativePath = $this->imageUploader->moveFileFromTmp(
$selectionImageName$imageName,
true
);
$selectionImage = $baseMediaDir . '/' . $newImgRelativePath;
} catch (\Exception $e) {
$message = __('Saving selection image: ') . $e->getMessage();
$this->messageManager->addErrorMessage($message);
}
} else {
// Save existing selection image
/**
* It was converted in Modifier for rendering in the admin interface:
* Magetu\BundleSelectionImage\Ui\DataProvider\Product\Form\Modifier\SelectionImage::modifyData
*/
$selectionImage = $baseMediaDir . '/' . $this->imageUploader->getBasePath() . '/' . $selectionImageName;
}
} else {
// Save existing selection image
/**
* It was converted in Modifier for rendering in the admin interface:
* Magetu\BundleSelectionImage\Ui\DataProvider\Product\Form\Modifier\SelectionImage::modifyData
*/
$selectionImage = $baseMediaDir . '/' . $this->imageUploader->getBasePath() . '/' . $imageName;
}
return $selectionImage;
}
}
- For Magento version >= 2.4.3: Due to this post reached the limit characters, please visit https://magetu.com/how-to-add-a-custom-image-field-in-product-bundle-selection-in-magento-2/ to see the file content.
In this post, I'll give instructions on how to create and save the product custom selection image step-by-step.
- Create modifier class which defined in
di.xml
in the step 4 to modify bundle selection structure to add the selection image field:
Due to Magento 2.4.3 replaced the code method to map product link to bundle selection model, the content of our LinkManagement model class should be different:
<?php
namespace Magetu\BundleSelectionImage\Model;
use Magento\Bundle\Api\Data\LinkInterface;
use Magento\Bundle\Api\Data\LinkInterfaceFactory;
use Magento\Bundle\Model\ResourceModel\Bundle;
use Magento\Bundle\Model\ResourceModel\BundleFactory;
use Magento\Bundle\Model\ResourceModel\Option\CollectionFactory;
use Magento\Bundle\Model\Selection;
use Magento\Bundle\Model\SelectionFactory;
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Catalog\Model\ImageUploader;
use Magento\Catalog\Model\Product;
use Magento\Framework\Api\DataObjectHelper;
use Magento\Framework\App\Filesystem\DirectoryList;
use Magento\Framework\EntityManager\MetadataPool;
use Magento\Framework\Exception\CouldNotSaveException;
use Magento\Framework\Exception\FileSystemException;
use Magento\Framework\Exception\InputException;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Framework\Filesystem;
use Magento\Framework\Filesystem\Directory\WriteInterface;
use Magento\Framework\Message\Manager as MessageManager;
use Magento\Store\Model\StoreManagerInterface;
class LinkManagement extends \Magento\Bundle\Model\LinkManagement
{
/**
* @var StoreManagerInterface
*/
private $storeManager;
/**
* @var MetadataPool
*/
private $metadataPool;
/**
* @var ImageUploader
*/
private $imageUploader;
/**
* @var MessageManager
*/
private $messageManager;
public function __construct(
ProductRepositoryInterface $productRepository,
LinkInterfaceFactory $linkFactory,
SelectionFactory $bundleSelection,
BundleFactory $bundleFactory,
CollectionFactory $optionCollection,
StoreManagerInterface $storeManager,
DataObjectHelper $dataObjectHelper,
MetadataPool $metadataPool,
ImageUploader $imageUploader,
MessageManager $messageManager
) {
parent::__construct(
$productRepository,
$linkFactory,
$bundleSelection,
$bundleFactory,
$optionCollection,
$storeManager,
$dataObjectHelper,
$metadataPool
);
$this->storeManager = $storeManager;
$this->metadataPool = $metadataPool;
$this->imageUploader = $imageUploader;
$this->messageManager = $messageManager;
}
/**
* This method has same content as in the parent class
* Declare it here to allow override mapProductLinkToBundleSelectionModel which is private
*
* @inheritDoc
*
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
* @SuppressWarnings(PHPMD.NPathComplexity)
*/
public function saveChild(
$sku,
LinkInterface $linkedProduct
) {
$product = $this->productRepository->get($sku, true);
if ($product->getTypeId() != Product\Type::TYPE_BUNDLE) {
throw new InputException(
__('The product with the "%1" SKU isn\'t a bundle product.', [$product->getSku()])
);
}
/** @var Product $linkProductModel */
$linkProductModel = $this->productRepository->get($linkedProduct->getSku());
if ($linkProductModel->isComposite()) {
throw new InputException(__('The bundle product can\'t contain another composite product.'));
}
if (!$linkedProduct->getId()) {
throw new InputException(__('The product link needs an ID field entered. Enter and try again.'));
}
/** @var Selection $selectionModel */
$selectionModel = $this->bundleSelection->create();
$selectionModel->load($linkedProduct->getId());
if (!$selectionModel->getId()) {
throw new InputException(
__(
'The product link with the "%1" ID field wasn\'t found. Verify the ID and try again.',
[$linkedProduct->getId()]
)
);
}
$selectionModel = $this->mapProductLinkToBundleSelectionModel(
$selectionModel,
$linkedProduct,
$product,
(int)$linkProductModel->getId()
);
try {
$selectionModel->save();
} catch (\Exception $e) {
throw new CouldNotSaveException(__('Could not save child: "%1"', $e->getMessage()), $e);
}
return true;
}
/**
* This method has same content as in the parent class
* Declare it here to allow override mapProductLinkToBundleSelectionModel which is private
*
* @inheritDoc
*
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
*/
public function addChild(
ProductInterface $product,
$optionId,
LinkInterface $linkedProduct
) {
if ($product->getTypeId() != Product\Type::TYPE_BUNDLE) {
throw new InputException(
__('The product with the "%1" SKU isn\'t a bundle product.', $product->getSku())
);
}
$linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField();
$options = $this->optionCollection->create();
$options->setIdFilter($optionId);
$options->setProductLinkFilter($product->getData($linkField));
$existingOption = $options->getFirstItem();
if (!$existingOption->getId()) {
throw new InputException(
__(
'Product with specified sku: "%1" does not contain option: "%2"',
[$product->getSku(), $optionId]
)
);
}
/* @var $resource Bundle */
$resource = $this->bundleFactory->create();
$selections = $resource->getSelectionsData($product->getData($linkField));
/** @var Product $linkProductModel */
$linkProductModel = $this->productRepository->get($linkedProduct->getSku());
if ($linkProductModel->isComposite()) {
throw new InputException(__('The bundle product can\'t contain another composite product.'));
}
if ($selections) {
foreach ($selections as $selection) {
if ($selection['option_id'] == $optionId &&
$selection['product_id'] == $linkProductModel->getEntityId() &&
$selection['parent_product_id'] == $product->getData($linkField)) {
if (!$product->getCopyFromView()) {
throw new CouldNotSaveException(
__(
'Child with specified sku: "%1" already assigned to product: "%2"',
[$linkedProduct->getSku(), $product->getSku()]
)
);
}
return $this->bundleSelection->create()->load($linkProductModel->getEntityId());
}
}
}
$selectionModel = $this->bundleSelection->create();
$selectionModel = $this->mapProductLinkToBundleSelectionModel(
$selectionModel,
$linkedProduct,
$product,
(int)$linkProductModel->getEntityId()
);
$selectionModel->setOptionId($optionId);
try {
$selectionModel->save();
$resource->addProductRelation($product->getData($linkField), $linkProductModel->getEntityId());
} catch (\Exception $e) {
throw new CouldNotSaveException(__('Could not save child: "%1"', $e->getMessage()), $e);
}
return (int)$selectionModel->getId();
}
/**
* Fill selection model with product link data.
* Save selection image value
*
* @param Selection $selectionModel
* @param LinkInterface $productLink
* @param ProductInterface $parentProduct
* @param int $linkedProductId
* @return Selection
* @throws FileSystemException
* @throws NoSuchEntityException
*/
private function mapProductLinkToBundleSelectionModel(
Selection $selectionModel,
LinkInterface $productLink,
ProductInterface $parentProduct,
int $linkedProductId
): Selection {
$linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField();
$selectionModel->setProductId($linkedProductId);
$selectionModel->setParentProductId($parentProduct->getData($linkField));
if ($productLink->getSelectionId() !== null) {
$selectionModel->setSelectionId($productLink->getSelectionId());
}
if ($productLink->getOptionId() !== null) {
$selectionModel->setOptionId($productLink->getOptionId());
}
if ($productLink->getPosition() !== null) {
$selectionModel->setPosition($productLink->getPosition());
}
if ($productLink->getQty() !== null) {
$selectionModel->setSelectionQty($productLink->getQty());
}
if ($productLink->getPriceType() !== null) {
$selectionModel->setSelectionPriceType($productLink->getPriceType());
}
if ($productLink->getPrice() !== null) {
$selectionModel->setSelectionPriceValue($productLink->getPrice());
}
if ($productLink->getCanChangeQuantity() !== null) {
$selectionModel->setSelectionCanChangeQty($productLink->getCanChangeQuantity());
}
if ($productLink->getIsDefault() !== null) {
$selectionModel->setIsDefault($productLink->getIsDefault());
}
$selectionModel->setWebsiteId((int)$this->storeManager->getStore($parentProduct->getStoreId())->getWebsiteId());
// Start customize
$selectionImage = $this->getSelectionImage($parentProduct, $productLink->getSelectionImage());
$selectionModel->setSelectionImage($selectionImage);
// End customize
return $selectionModel;
}
/**
* Move selection image file and get selection image path
* @param $parentProduct
* @param $selectionImageData
* @return string|null
* @throws FileSystemException
*/
private function getSelectionImage($parentProduct, $selectionImageData)
{
$selectionImage = null;
if ($selectionImageData !== null) {
$baseMediaDir = $parentProduct->getStore()->getBaseMediaDir();
$selectionImageName = $selectionImageData[0]['name'];
if (isset($selectionImageData[0]['tmp_name'])) {
// Save new uploaded selection image
try {
$newImgRelativePath = $this->imageUploader->moveFileFromTmp(
$selectionImageName,
true
);
$selectionImage = $baseMediaDir . '/' . $newImgRelativePath;
} catch (\Exception $e) {
$message = __('Saving selection image: ') . $e->getMessage();
$this->messageManager->addErrorMessage($message);
}
} else {
// Save existing selection image
/**
* It was converted in Modifier for rendering in the admin interface:
* Magetu\BundleSelectionImage\Ui\DataProvider\Product\Form\Modifier\SelectionImage::modifyData
*/
$selectionImage = $baseMediaDir . '/' . $this->imageUploader->getBasePath() . '/' . $selectionImageName;
}
}
return $selectionImage;
}
}
In this post, I'll give instructions on how to create and save a custom image in the bundle product selection step-by-step.
- Create a modifier class which is defined in
di.xml
in step 4 in order to modify bundle selection structure to add the selection image field:
Due to Magento 2.4.3 replacing the code of method to map product link to bundle selection model, the content for LinkManagement model class should be different:
<?php
namespace Magetu\BundleSelectionImage\Model;
use Magento\Bundle\Api\Data\LinkInterface;
use Magento\Bundle\Api\Data\LinkInterfaceFactory;
use Magento\Bundle\Model\ResourceModel\Bundle;
use Magento\Bundle\Model\ResourceModel\BundleFactory;
use Magento\Bundle\Model\ResourceModel\Option\CollectionFactory;
use Magento\Bundle\Model\Selection;
use Magento\Bundle\Model\SelectionFactory;
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Catalog\Model\ImageUploader;
use Magento\Catalog\Model\Product;
use Magento\Framework\Api\DataObjectHelper;
use Magento\Framework\EntityManager\MetadataPool;
use Magento\Framework\Exception\CouldNotSaveException;
use Magento\Framework\Exception\InputException;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Framework\Message\Manager as MessageManager;
use Magento\Store\Model\StoreManagerInterface;
class LinkManagement extends \Magento\Bundle\Model\LinkManagement
{
/**
* @var StoreManagerInterface
*/
private $storeManager;
/**
* @var MetadataPool
*/
private $metadataPool;
/**
* @var ImageUploader
*/
private $imageUploader;
/**
* @var MessageManager
*/
private $messageManager;
public function __construct(
ProductRepositoryInterface $productRepository,
LinkInterfaceFactory $linkFactory,
SelectionFactory $bundleSelection,
BundleFactory $bundleFactory,
CollectionFactory $optionCollection,
StoreManagerInterface $storeManager,
DataObjectHelper $dataObjectHelper,
MetadataPool $metadataPool,
ImageUploader $imageUploader,
MessageManager $messageManager
) {
parent::__construct(
$productRepository,
$linkFactory,
$bundleSelection,
$bundleFactory,
$optionCollection,
$storeManager,
$dataObjectHelper,
$metadataPool
);
$this->storeManager = $storeManager;
$this->metadataPool = $metadataPool;
$this->imageUploader = $imageUploader;
$this->messageManager = $messageManager;
}
/**
* This method has same content as in the parent class
* Declare it here to allow override mapProductLinkToBundleSelectionModel which is private
*
* @inheritDoc
*/
public function saveChild(
$sku,
LinkInterface $linkedProduct
) {
$product = $this->productRepository->get($sku, true);
if ($product->getTypeId() != Product\Type::TYPE_BUNDLE) {
throw new InputException(
__('The product with the "%1" SKU isn\'t a bundle product.', [$product->getSku()])
);
}
/** @var Product $linkProductModel */
$linkProductModel = $this->productRepository->get($linkedProduct->getSku());
if ($linkProductModel->isComposite()) {
throw new InputException(__('The bundle product can\'t contain another composite product.'));
}
if (!$linkedProduct->getId()) {
throw new InputException(__('The product link needs an ID field entered. Enter and try again.'));
}
/** @var Selection $selectionModel */
$selectionModel = $this->bundleSelection->create();
$selectionModel->load($linkedProduct->getId());
if (!$selectionModel->getId()) {
throw new InputException(
__(
'The product link with the "%1" ID field wasn\'t found. Verify the ID and try again.',
[$linkedProduct->getId()]
)
);
}
$selectionModel = $this->mapProductLinkToBundleSelectionModel(
$selectionModel,
$linkedProduct,
$product,
(int)$linkProductModel->getId()
);
try {
$selectionModel->save();
} catch (\Exception $e) {
throw new CouldNotSaveException(__('Could not save child: "%1"', $e->getMessage()), $e);
}
return true;
}
/**
* This method has same content as in the parent class
* Declare it here to allow override mapProductLinkToBundleSelectionModel which is private
*
* @inheritDoc
*/
public function addChild(
ProductInterface $product,
$optionId,
LinkInterface $linkedProduct
) {
if ($product->getTypeId() != Product\Type::TYPE_BUNDLE) {
throw new InputException(
__('The product with the "%1" SKU isn\'t a bundle product.', $product->getSku())
);
}
$linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField();
$options = $this->optionCollection->create();
$options->setIdFilter($optionId);
$options->setProductLinkFilter($product->getData($linkField));
$existingOption = $options->getFirstItem();
if (!$existingOption->getId()) {
throw new InputException(
__(
'Product with specified sku: "%1" does not contain option: "%2"',
[$product->getSku(), $optionId]
)
);
}
/* @var $resource Bundle */
$resource = $this->bundleFactory->create();
$selections = $resource->getSelectionsData($product->getData($linkField));
/** @var Product $linkProductModel */
$linkProductModel = $this->productRepository->get($linkedProduct->getSku());
if ($linkProductModel->isComposite()) {
throw new InputException(__('The bundle product can\'t contain another composite product.'));
}
if ($selections) {
foreach ($selections as $selection) {
if ($selection['option_id'] == $optionId &&
$selection['product_id'] == $linkProductModel->getEntityId() &&
$selection['parent_product_id'] == $product->getData($linkField)) {
if (!$product->getCopyFromView()) {
throw new CouldNotSaveException(
__(
'Child with specified sku: "%1" already assigned to product: "%2"',
[$linkedProduct->getSku(), $product->getSku()]
)
);
}
return $this->bundleSelection->create()->load($linkProductModel->getEntityId());
}
}
}
$selectionModel = $this->bundleSelection->create();
$selectionModel = $this->mapProductLinkToBundleSelectionModel(
$selectionModel,
$linkedProduct,
$product,
(int)$linkProductModel->getEntityId()
);
$selectionModel->setOptionId($optionId);
try {
$selectionModel->save();
$resource->addProductRelation($product->getData($linkField), $linkProductModel->getEntityId());
} catch (\Exception $e) {
throw new CouldNotSaveException(__('Could not save child: "%1"', $e->getMessage()), $e);
}
return (int)$selectionModel->getId();
}
/**
* Fill selection model with product link data.
* Save selection image value
*
* @param Selection $selectionModel
* @param LinkInterface $productLink
* @param ProductInterface $parentProduct
* @param int $linkedProductId
* @return Selection
* @throws NoSuchEntityException
*/
private function mapProductLinkToBundleSelectionModel(
Selection $selectionModel,
LinkInterface $productLink,
ProductInterface $parentProduct,
int $linkedProductId
): Selection {
$linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField();
$selectionModel->setProductId($linkedProductId);
$selectionModel->setParentProductId($parentProduct->getData($linkField));
if ($productLink->getSelectionId() !== null) {
$selectionModel->setSelectionId($productLink->getSelectionId());
}
if ($productLink->getOptionId() !== null) {
$selectionModel->setOptionId($productLink->getOptionId());
}
if ($productLink->getPosition() !== null) {
$selectionModel->setPosition($productLink->getPosition());
}
if ($productLink->getQty() !== null) {
$selectionModel->setSelectionQty($productLink->getQty());
}
if ($productLink->getPriceType() !== null) {
$selectionModel->setSelectionPriceType($productLink->getPriceType());
}
if ($productLink->getPrice() !== null) {
$selectionModel->setSelectionPriceValue($productLink->getPrice());
}
if ($productLink->getCanChangeQuantity() !== null) {
$selectionModel->setSelectionCanChangeQty($productLink->getCanChangeQuantity());
}
if ($productLink->getIsDefault() !== null) {
$selectionModel->setIsDefault($productLink->getIsDefault());
}
$selectionModel->setWebsiteId((int)$this->storeManager->getStore($parentProduct->getStoreId())->getWebsiteId());
// Start customize
$selectionImage = null;
if ($productLink->getSelectionImage() !== null) {
$selectionImage = $this->getSelectionImage($productLink->getSelectionImage());
}
$selectionModel->setSelectionImage($selectionImage);
// End customize
return $selectionModel;
}
/**
* Move uploaded selection image file and get selection image path
* @param $selectionImageData
* @return string|null
* @throws NoSuchEntityException
*/
private function getSelectionImage($selectionImageData)
{
$selectionImage = null;
$store = $this->storeManager->getStore();
$baseMediaDir = $store->getBaseMediaDir();
$imageName = $selectionImageData[0]['name'];
if (isset($selectionImageData[0]['tmp_name'])) {
// Save new uploaded selection image
try {
$newImgRelativePath = $this->imageUploader->moveFileFromTmp($imageName, true);
$selectionImage = $baseMediaDir . '/' . $newImgRelativePath;
} catch (\Exception $e) {
$message = __('Saving selection image: ') . $e->getMessage();
$this->messageManager->addErrorMessage($message);
}
} else {
// Save existing selection image
/**
* It was converted in Modifier for rendering in the admin interface:
* Magetu\BundleSelectionImage\Ui\DataProvider\Product\Form\Modifier\SelectionImage::modifyData
*/
$selectionImage = $baseMediaDir . '/' . $this->imageUploader->getBasePath() . '/' . $imageName;
}
return $selectionImage;
}
}
- For Magento version >= 2.4.3: Due to this post reached the limit characters, please visit https://magetu.com/how-to-add-a-custom-image-field-in-product-bundle-selection-in-magento-2/ to see the file content.
In this post, I'll give instructions on how to create and save the product custom selection image step-by-step.
For the purposes of this tutorial Magetu
is the name of the vendor associated with a module named BundleSelectionImage
.
- Add a new field for selection image to
catalog_product_bundle_selection
tableCreate registration.php:
Assuming you use Magento version >= 2.3, createFile path:<your module dir>app/etccode/db_schemaMagetu/BundleSelectionImage/registration.xmlphp
<?xml version="1.0"?>php
<schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
use xsiMagento\Framework\Component\ComponentRegistrar;
ComponentRegistrar:noNamespaceSchemaLocation="urn:magentoregister(ComponentRegistrar:framework:Setup/Declaration/Schema/etc/schema.xsd">
<table name="catalog_product_bundle_selection">
<column xsi:type="varchar" name="selection_image" nullable="true" length="255" comment="Selection Image"/>
MODULE, 'Magetu_BundleSelectionImage', </table>__DIR__);
</schema>
- In your custom
BundleCustomOptions
Modifier, addCreate module.xml:
File path:uploaderConfigapp/code/Magetu/BundleSelectionImage/etc/module.xml
to your selection image field to point to the uploader controller.
'uploaderConfig'<?xml =>version="1.0"?>
<config [xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
'url'<module =>name="Magetu_BundleSelectionImage">
$this->urlBuilder->getUrl( <sequence>
'bundleselectionimage <module name="Magento_Bundle"/product_image>
<module name="Magento_Catalog"/upload'>
), </sequence>
], </module>
</config>
- Add an upload controller to handle selection image uploadCreate db_schema.
- Override
Magento\Bundle\Model\LinkManagement
xml to save theadd a new column for bundle selection image value to database.
At the end of themapProductLinkToBundleSelectionModel
method, beforeinreturn $selectionModel;catalog_product_bundle_selection
, adding the below code: table.
File path: app/code/Magetu/BundleSelectionImage/etc/db_schema.xml
if<?xml ($productLinkversion="1.0"?>
<schema xmlns:xsi="http://www.w3.org/2001/XMLSchema->getSelectionImage()instance"
!== null) { xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">
$selectionModel->setSelectionImage($productLink->getSelectionImage());<table name="catalog_product_bundle_selection">
} <column xsi:type="varchar" name="selection_image" nullable="true" length="255" comment="Selection Image"/>
</table>
</schema>
- Create the di.xml file to:
- define a modifier to modify bundle selection structure to add the selection image field
- config the Upload class
- override
Magento\Bundle\Model\LinkManagement
class to save the bundle selection image value
File path: app/code/Magetu/BundleSelectionImage/etc/adminhtml/di.xml
<?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="bundle_selection_image" xsi:type="array">
<item name="class" xsi:type="string">Magetu\BundleSelectionImage\Ui\DataProvider\Product\Form\Modifier\SelectionImage</item>
<item name="sortOrder" xsi:type="number">181</item><!-- run after core bundle modifier -->
</item>
</argument>
</arguments>
</virtualType>
<virtualType name="Magetu\BundleSelectionImage\ImageUpload" type="Magento\Catalog\Model\ImageUploader">
<arguments>
<argument name="baseTmpPath" xsi:type="string">catalog/tmp/product/selection_images</argument>
<argument name="basePath" xsi:type="string">catalog/product/selection_images</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="gif" xsi:type="string">gif</item>
<item name="png" xsi:type="string">png</item>
<item name="svg" xsi:type="string">svg</item>
<item name="svgz" xsi:type="string">svgz</item>
<item name="webp" xsi:type="string">webp</item>
<item name="avif" xsi:type="string">avif</item>
<item name="avifs" xsi:type="string">avifs</item>
</argument>
<argument name="allowedMimeTypes" xsi:type="array">
<item name="jpg" xsi:type="string">image/jpg</item>
<item name="jpeg" xsi:type="string">image/jpeg</item>
<item name="gif" xsi:type="string">image/gif</item>
<item name="png" xsi:type="string">image/png</item>
<item name="svg" xsi:type="string">image/svg+xml</item>
<item name="svgz" xsi:type="string">image/svg+xml</item>
<item name="webp" xsi:type="string">image/webp</item>
<item name="avif" xsi:type="string">image/avif</item>
<item name="avifs" xsi:type="string">image/avif-sequence</item>
</argument>
</arguments>
</virtualType>
<type name="Magetu\BundleSelectionImage\Controller\Adminhtml\Product\Image\Upload">
<arguments>
<argument name="imageUploader" xsi:type="object">Magetu\BundleSelectionImage\ImageUpload</argument>
</arguments>
</type>
<preference for="Magento\Bundle\Model\LinkManagement" type="Magetu\BundleSelectionImage\Model\LinkManagement"/>
<type name="Magetu\BundleSelectionImage\Model\LinkManagement">
<arguments>
<argument name="imageUploader" xsi:type="object">Magetu\BundleSelectionImage\ImageUpload</argument>
</arguments>
</type>
</config>
- Run the commandsCreate routes.xml to upgrade database, re-compile code, re-deploy static contentuse the upload controller.
File path: app/code/Magetu/BundleSelectionImage/etc/adminhtml/routes.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd">
<router id="admin">
<route id="bundleselectionimage" frontName="bundleselectionimage">
<module name="Magetu_BundleSelectionImage" before="Magento_Backend"/>
</route>
</router>
</config>
- Create modifier class which defined in
di.xml
in the step 4 to modify bundle selection structure to add the selection image field:
File path: app/code/Magetu/BundleSelectionImage/Ui/DataProvider/Product/Form/Modifier/SelectionImage.php
<?php
namespace Magetu\BundleSelectionImage\Ui\DataProvider\Product\Form\Modifier;
use Magento\Bundle\Model\Product\Type;
use Magento\Bundle\Model\ResourceModel\Selection as SelectionResource;
use Magento\Bundle\Model\SelectionFactory;
use Magento\Bundle\Ui\DataProvider\Product\Form\Modifier\BundlePanel;
use Magento\Catalog\Model\Category\FileInfo;
use Magento\Catalog\Model\Locator\LocatorInterface;
use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\AbstractModifier;
use Magento\Framework\UrlInterface;
class SelectionImage extends AbstractModifier
{
public const FIELD_IMAGE = 'selection_image';
/**
* @var LocatorInterface
*/
private $locator;
/**
* @var UrlInterface
*/
private $urlBuilder;
/**
* @var FileInfo
*/
private $fileInfo;
/**
* @var SelectionFactory
*/
private $selectionFactory;
/**
* @var SelectionResource
*/
private $selectionResource;
public function __construct(
LocatorInterface $locator,
UrlInterface $urlBuilder,
FileInfo $fileInfo,
SelectionFactory $selectionFactory,
SelectionResource $selectionResource
) {
$this->locator = $locator;
$this->urlBuilder = $urlBuilder;
$this->fileInfo = $fileInfo;
$this->selectionFactory = $selectionFactory;
$this->selectionResource = $selectionResource;
}
/**
* {@inheritdoc}
*
* Converts selection image data to acceptable for rendering format
* Display selection image
*/
public function modifyData(array $data)
{
$product = $this->locator->getProduct();
$modelId = $product->getId();
$isBundleProduct = $product->getTypeId() === Type::TYPE_CODE;
if ($isBundleProduct && $modelId) {
$selectionModel = $this->selectionFactory->create();
foreach ($data[$modelId][BundlePanel::CODE_BUNDLE_OPTIONS][BundlePanel::CODE_BUNDLE_OPTIONS] as &$option) {
foreach ($option['bundle_selections'] as &$selection) {
$this->selectionResource->load($selectionModel, $selection['selection_id']);
$selectionImage = $selectionModel->getData('selection_image');
if ($selectionImage !== null && $this->fileInfo->isExist($selectionImage)) {
$stat = $this->fileInfo->getStat($selectionImage);
$mime = $this->fileInfo->getMimeType($selectionImage);
// phpcs:ignore Magento2.Functions.DiscouragedFunction
$imageUrl = $product->getStore()->getBaseUrl() . $selectionImage;
$imageRendering = [];
$imageRendering[0]['name'] = basename($selectionImage);
$imageRendering[0]['url'] = $imageUrl;
$imageRendering[0]['size'] = $stat['size'];
$imageRendering[0]['type'] = $mime;
$selectionModel->setData('selection_image', $imageRendering);
}
$selection['selection_image'] = $selectionModel->getData('selection_image');
}
}
}
return $data;
}
/**
* Add selection image field
*
* @param array $meta
* @return array
*/
public function modifyMeta(array $meta) {
$groupCode = BundlePanel::CODE_BUNDLE_DATA;
$meta[$groupCode]['children']['bundle_options']['children']['record']['children']
['product_bundle_container']['children']['bundle_selections']
['children']['record']['children'][static::FIELD_IMAGE] = $this->getSelectionImageFieldConfig();
return $meta;
}
/**
* Get selection image field config
*
* @return array
*/
private function getSelectionImageFieldConfig()
{
return [
'arguments' => [
'data' => [
'config' => [
'componentType' => 'imageUploader',
'formElement' => 'imageUploader',
'template' => 'Magetu_BundleSelectionImage/form/element/uploader/image',
'fileInputName' => static::FIELD_IMAGE,
'uploaderConfig' => [
'url' => $this->urlBuilder->getUrl(
'bundleselectionimage/product_image/upload'
),
],
'dataScope' => static::FIELD_IMAGE,
'sortOrder' => 129,// Before the last field (action_delete)
],
],
],
];
}
}
- Create image uploader html file to upload the selection image file:
File path: app/code/Magetu/BundleSelectionImage/view/adminhtml/web/template/form/element/uploader/image.html
<!--
/**
* Custom template for upload image file in bundle selection,
* based on Magento_Ui::form/element/uploader/image.html
*
* NOTE: the variable name of the upload file input name should not be declared as 'inputName'
* to allow the upload controller get the file
*/
-->
<div class="admin__field" visible="visible" css="$data.additionalClasses">
<div class="admin__field-control" css="'_with-tooltip': $data.tooltip">
<div class="file-uploader image-uploader" data-role="drop-zone" css="_loading: isLoading">
<div class="file-uploader-area">
<input type="file" afterRender="onElementRender" attr="id: uid, name: fileInputName, multiple: isMultipleFiles" disable="disabled" />
<label class="file-uploader-button action-default" attr="for: uid, disabled: disabled" disable="disabled" translate="'Upload'"></label>
<render args="fallbackResetTpl" if="$data.showFallbackReset && $data.isDifferedFromDefault"></render>
<p class="image-upload-requirements">
<span if="$data.maxFileSize">
<span translate="'Maximum file size'"></span>: <text args="formatSize($data.maxFileSize)"></text>.
</span>
<span if="$data.allowedExtensions">
<span translate="'Allowed file types'"></span>: <text args="getAllowedFileExtensionsInCommaDelimitedFormat()"></text>.
</span>
</p>
</div>
<render args="tooltipTpl" if="$data.tooltip"></render>
<div class="admin__field-note" if="$data.notice" attr="id: noticeId">
<span html="notice"></span>
</div>
<label class="admin__field-error" if="error" attr="for: uid" text="error"></label>
<each args="data: value, as: '$file'" render="$parent.getPreviewTmpl($file)"></each>
<div if="!hasData()" class="image image-placeholder" click="triggerImageUpload">
<div class="file-uploader-summary product-image-wrapper">
<div class="file-uploader-spinner image-uploader-spinner"></div>
<p class="image-placeholder-text" translate="'Browse to find or drag image here'"></p>
</div>
</div>
</div>
<render args="$data.service.template" if="$data.hasService()"></render>
</div>
</div>
- Create the uploader controller to handle upload the selection image file:
File path: app/code/Magetu/BundleSelectionImage/Controller/Adminhtml/Product/Image/Upload.php
<?php
namespace Magetu\BundleSelectionImage\Controller\Adminhtml\Product\Image;
use Magento\Backend\App\Action as BackendAction;
use Magento\Backend\App\Action\Context;
use Magento\Catalog\Model\ImageUploader;
use Magento\Framework\App\Action\HttpPostActionInterface;
use Magento\Framework\Controller\ResultFactory;
use Magento\Framework\Controller\ResultInterface;
/**
* The bundle selection image upload controller
*/
class Upload extends BackendAction implements HttpPostActionInterface
{
/**
* Authorization level of a basic admin session
*
* @see _isAllowed()
*/
public const ADMIN_RESOURCE = 'Magento_Catalog::products';
/**
* @var ImageUploader
*/
private $imageUploader;
public function __construct(
Context $context,
ImageUploader $imageUploader
) {
parent::__construct($context);
$this->imageUploader = $imageUploader;
}
/**
* Upload selection image file controller action
*/
public function execute(): ResultInterface
{
$imageId = $this->_request->getParam('param_name', 'selection_image');
try {
$result = $this->imageUploader->saveFileToTmpDir($imageId);
} catch (\Exception $e) {
$result = ['error' => $e->getMessage(), 'errorcode' => $e->getCode()];
}
return $this->resultFactory->create(ResultFactory::TYPE_JSON)->setData($result);
}
}
- Create LinkManagement model class to override
Magento\Bundle\Model\LinkManagement
class to save the bundle selection image value in the database:
File path:app/code/Magetu/BundleSelectionImage/Model/LinkManagement.php
Due to Magento 2.4.3 replaced the code method to map product link to bundle selection model, the content of our LinkManagement model class should be different:
- For Magento version >= 2.4.3
<?php
namespace Magetu\BundleSelectionImage\Model;
use Magento\Bundle\Api\Data\LinkInterface;
use Magento\Bundle\Api\Data\LinkInterfaceFactory;
use Magento\Bundle\Model\ResourceModel\Bundle;
use Magento\Bundle\Model\ResourceModel\BundleFactory;
use Magento\Bundle\Model\ResourceModel\Option\CollectionFactory;
use Magento\Bundle\Model\Selection;
use Magento\Bundle\Model\SelectionFactory;
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Catalog\Model\ImageUploader;
use Magento\Catalog\Model\Product;
use Magento\Framework\Api\DataObjectHelper;
use Magento\Framework\App\Filesystem\DirectoryList;
use Magento\Framework\EntityManager\MetadataPool;
use Magento\Framework\Exception\CouldNotSaveException;
use Magento\Framework\Exception\FileSystemException;
use Magento\Framework\Exception\InputException;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Framework\Filesystem;
use Magento\Framework\Filesystem\Directory\WriteInterface;
use Magento\Framework\Message\Manager as MessageManager;
use Magento\Store\Model\StoreManagerInterface;
class LinkManagement extends \Magento\Bundle\Model\LinkManagement
{
/**
* @var StoreManagerInterface
*/
private $storeManager;
/**
* @var MetadataPool
*/
private $metadataPool;
/**
* @var ImageUploader
*/
private $imageUploader;
/**
* @var MessageManager
*/
private $messageManager;
public function __construct(
ProductRepositoryInterface $productRepository,
LinkInterfaceFactory $linkFactory,
SelectionFactory $bundleSelection,
BundleFactory $bundleFactory,
CollectionFactory $optionCollection,
StoreManagerInterface $storeManager,
DataObjectHelper $dataObjectHelper,
MetadataPool $metadataPool,
ImageUploader $imageUploader,
MessageManager $messageManager
) {
parent::__construct(
$productRepository,
$linkFactory,
$bundleSelection,
$bundleFactory,
$optionCollection,
$storeManager,
$dataObjectHelper,
$metadataPool
);
$this->storeManager = $storeManager;
$this->metadataPool = $metadataPool;
$this->imageUploader = $imageUploader;
$this->messageManager = $messageManager;
}
/**
* This method has same content as in the parent class
* Declare it here to allow override mapProductLinkToBundleSelectionModel which is private
*
* @inheritDoc
*
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
* @SuppressWarnings(PHPMD.NPathComplexity)
*/
public function saveChild(
$sku,
LinkInterface $linkedProduct
) {
$product = $this->productRepository->get($sku, true);
if ($product->getTypeId() != Product\Type::TYPE_BUNDLE) {
throw new InputException(
__('The product with the "%1" SKU isn\'t a bundle product.', [$product->getSku()])
);
}
/** @var Product $linkProductModel */
$linkProductModel = $this->productRepository->get($linkedProduct->getSku());
if ($linkProductModel->isComposite()) {
throw new InputException(__('The bundle product can\'t contain another composite product.'));
}
if (!$linkedProduct->getId()) {
throw new InputException(__('The product link needs an ID field entered. Enter and try again.'));
}
/** @var Selection $selectionModel */
$selectionModel = $this->bundleSelection->create();
$selectionModel->load($linkedProduct->getId());
if (!$selectionModel->getId()) {
throw new InputException(
__(
'The product link with the "%1" ID field wasn\'t found. Verify the ID and try again.',
[$linkedProduct->getId()]
)
);
}
$selectionModel = $this->mapProductLinkToBundleSelectionModel(
$selectionModel,
$linkedProduct,
$product,
(int)$linkProductModel->getId()
);
try {
$selectionModel->save();
} catch (\Exception $e) {
throw new CouldNotSaveException(__('Could not save child: "%1"', $e->getMessage()), $e);
}
return true;
}
/**
* This method has same content as in the parent class
* Declare it here to allow override mapProductLinkToBundleSelectionModel which is private
*
* @inheritDoc
*
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
*/
public function addChild(
ProductInterface $product,
$optionId,
LinkInterface $linkedProduct
) {
if ($product->getTypeId() != Product\Type::TYPE_BUNDLE) {
throw new InputException(
__('The product with the "%1" SKU isn\'t a bundle product.', $product->getSku())
);
}
$linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField();
$options = $this->optionCollection->create();
$options->setIdFilter($optionId);
$options->setProductLinkFilter($product->getData($linkField));
$existingOption = $options->getFirstItem();
if (!$existingOption->getId()) {
throw new InputException(
__(
'Product with specified sku: "%1" does not contain option: "%2"',
[$product->getSku(), $optionId]
)
);
}
/* @var $resource Bundle */
$resource = $this->bundleFactory->create();
$selections = $resource->getSelectionsData($product->getData($linkField));
/** @var Product $linkProductModel */
$linkProductModel = $this->productRepository->get($linkedProduct->getSku());
if ($linkProductModel->isComposite()) {
throw new InputException(__('The bundle product can\'t contain another composite product.'));
}
if ($selections) {
foreach ($selections as $selection) {
if ($selection['option_id'] == $optionId &&
$selection['product_id'] == $linkProductModel->getEntityId() &&
$selection['parent_product_id'] == $product->getData($linkField)) {
if (!$product->getCopyFromView()) {
throw new CouldNotSaveException(
__(
'Child with specified sku: "%1" already assigned to product: "%2"',
[$linkedProduct->getSku(), $product->getSku()]
)
);
}
return $this->bundleSelection->create()->load($linkProductModel->getEntityId());
}
}
}
$selectionModel = $this->bundleSelection->create();
$selectionModel = $this->mapProductLinkToBundleSelectionModel(
$selectionModel,
$linkedProduct,
$product,
(int)$linkProductModel->getEntityId()
);
$selectionModel->setOptionId($optionId);
try {
$selectionModel->save();
$resource->addProductRelation($product->getData($linkField), $linkProductModel->getEntityId());
} catch (\Exception $e) {
throw new CouldNotSaveException(__('Could not save child: "%1"', $e->getMessage()), $e);
}
return (int)$selectionModel->getId();
}
/**
* Fill selection model with product link data.
* Save selection image value
*
* @param Selection $selectionModel
* @param LinkInterface $productLink
* @param ProductInterface $parentProduct
* @param int $linkedProductId
* @return Selection
* @throws FileSystemException
* @throws NoSuchEntityException
*/
private function mapProductLinkToBundleSelectionModel(
Selection $selectionModel,
LinkInterface $productLink,
ProductInterface $parentProduct,
int $linkedProductId
): Selection {
$linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField();
$selectionModel->setProductId($linkedProductId);
$selectionModel->setParentProductId($parentProduct->getData($linkField));
if ($productLink->getSelectionId() !== null) {
$selectionModel->setSelectionId($productLink->getSelectionId());
}
if ($productLink->getOptionId() !== null) {
$selectionModel->setOptionId($productLink->getOptionId());
}
if ($productLink->getPosition() !== null) {
$selectionModel->setPosition($productLink->getPosition());
}
if ($productLink->getQty() !== null) {
$selectionModel->setSelectionQty($productLink->getQty());
}
if ($productLink->getPriceType() !== null) {
$selectionModel->setSelectionPriceType($productLink->getPriceType());
}
if ($productLink->getPrice() !== null) {
$selectionModel->setSelectionPriceValue($productLink->getPrice());
}
if ($productLink->getCanChangeQuantity() !== null) {
$selectionModel->setSelectionCanChangeQty($productLink->getCanChangeQuantity());
}
if ($productLink->getIsDefault() !== null) {
$selectionModel->setIsDefault($productLink->getIsDefault());
}
$selectionModel->setWebsiteId((int)$this->storeManager->getStore($parentProduct->getStoreId())->getWebsiteId());
// Start customize
$selectionImage = $this->getSelectionImage($parentProduct, $productLink->getSelectionImage());
$selectionModel->setSelectionImage($selectionImage);
// End customize
return $selectionModel;
}
/**
* Move selection image file and get selection image path
* @param $parentProduct
* @param $selectionImageData
* @return string|null
* @throws FileSystemException
*/
private function getSelectionImage($parentProduct, $selectionImageData)
{
$selectionImage = null;
if ($selectionImageData !== null) {
$baseMediaDir = $parentProduct->getStore()->getBaseMediaDir();
$selectionImageName = $selectionImageData[0]['name'];
if (isset($selectionImageData[0]['tmp_name'])) {
// Save new uploaded selection image
try {
$newImgRelativePath = $this->imageUploader->moveFileFromTmp(
$selectionImageName,
true
);
$selectionImage = $baseMediaDir . '/' . $newImgRelativePath;
} catch (\Exception $e) {
$message = __('Saving selection image: ') . $e->getMessage();
$this->messageManager->addErrorMessage($message);
}
} else {
// Save existing selection image
/**
* It was converted in Modifier for rendering in the admin interface:
* Magetu\BundleSelectionImage\Ui\DataProvider\Product\Form\Modifier\SelectionImage::modifyData
*/
$selectionImage = $baseMediaDir . '/' . $this->imageUploader->getBasePath() . '/' . $selectionImageName;
}
}
return $selectionImage;
}
}
Full module directory structure
The module is now complete. Your module’s directory structure under app/code
should look like the following:
Magetu
|-- BundleSelectionImage
|
|-- Controller
| |-- Adminhtml
| |-- Product
| |-- Image
| |-- Upload.php
|-- etc
| |-- adminhtml
| | |-- di.xml
| | |-- routes.xml
| |-- db_schema.xml
| |-- module.xml
|-- Model
| |-- LinkManagement.php
|-- Ui
| |-- DataProvider
| |-- Product
| |-- Form
| |-- Modifier
| |-- SelectionImage.php
|-- view
| |-- adminhtml
| |-- web
| |-- template
| |-- form
| |-- element
| |-- uploader
| |-- image.html
|-- registration.php
Installing the module
Run the following commands to register the module, upgrade database, compile code and deploy static content:
bin/magento setup:upgrade
bin/magento setup:di:compile
bin/magento setup:static-content:deploy -f
- Add a new field for selection image to
catalog_product_bundle_selection
table. Assuming you use Magento version >= 2.3, create<your module dir>/etc/db_schema.xml
<?xml version="1.0"?>
<schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">
<table name="catalog_product_bundle_selection">
<column xsi:type="varchar" name="selection_image" nullable="true" length="255" comment="Selection Image"/>
</table>
</schema>
- In your custom
BundleCustomOptions
Modifier, adduploaderConfig
to your selection image field to point to the uploader controller.
'uploaderConfig' => [
'url' => $this->urlBuilder->getUrl(
'bundleselectionimage/product_image/upload'
),
],
- Add an upload controller to handle selection image upload.
- Override
Magento\Bundle\Model\LinkManagement
to save the selection image value to database.
At the end of themapProductLinkToBundleSelectionModel
method, beforereturn $selectionModel;
, adding the below code:
if ($productLink->getSelectionImage() !== null) {
$selectionModel->setSelectionImage($productLink->getSelectionImage());
}
- Run the commands to upgrade database, re-compile code, re-deploy static content.
In this post, I'll give instructions on how to create and save the product custom selection image step-by-step.
For the purposes of this tutorial Magetu
is the name of the vendor associated with a module named BundleSelectionImage
.
- Create registration.php:
File path:app/code/Magetu/BundleSelectionImage/registration.php
<?php
use Magento\Framework\Component\ComponentRegistrar;
ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magetu_BundleSelectionImage', __DIR__);
- Create module.xml:
File path:app/code/Magetu/BundleSelectionImage/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="Magetu_BundleSelectionImage">
<sequence>
<module name="Magento_Bundle"/>
<module name="Magento_Catalog"/>
</sequence>
</module>
</config>
- Create db_schema.xml to add a new column for bundle selection image in
catalog_product_bundle_selection
table.
File path: app/code/Magetu/BundleSelectionImage/etc/db_schema.xml
<?xml version="1.0"?>
<schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">
<table name="catalog_product_bundle_selection">
<column xsi:type="varchar" name="selection_image" nullable="true" length="255" comment="Selection Image"/>
</table>
</schema>
- Create the di.xml file to:
- define a modifier to modify bundle selection structure to add the selection image field
- config the Upload class
- override
Magento\Bundle\Model\LinkManagement
class to save the bundle selection image value
File path: app/code/Magetu/BundleSelectionImage/etc/adminhtml/di.xml
<?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="bundle_selection_image" xsi:type="array">
<item name="class" xsi:type="string">Magetu\BundleSelectionImage\Ui\DataProvider\Product\Form\Modifier\SelectionImage</item>
<item name="sortOrder" xsi:type="number">181</item><!-- run after core bundle modifier -->
</item>
</argument>
</arguments>
</virtualType>
<virtualType name="Magetu\BundleSelectionImage\ImageUpload" type="Magento\Catalog\Model\ImageUploader">
<arguments>
<argument name="baseTmpPath" xsi:type="string">catalog/tmp/product/selection_images</argument>
<argument name="basePath" xsi:type="string">catalog/product/selection_images</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="gif" xsi:type="string">gif</item>
<item name="png" xsi:type="string">png</item>
<item name="svg" xsi:type="string">svg</item>
<item name="svgz" xsi:type="string">svgz</item>
<item name="webp" xsi:type="string">webp</item>
<item name="avif" xsi:type="string">avif</item>
<item name="avifs" xsi:type="string">avifs</item>
</argument>
<argument name="allowedMimeTypes" xsi:type="array">
<item name="jpg" xsi:type="string">image/jpg</item>
<item name="jpeg" xsi:type="string">image/jpeg</item>
<item name="gif" xsi:type="string">image/gif</item>
<item name="png" xsi:type="string">image/png</item>
<item name="svg" xsi:type="string">image/svg+xml</item>
<item name="svgz" xsi:type="string">image/svg+xml</item>
<item name="webp" xsi:type="string">image/webp</item>
<item name="avif" xsi:type="string">image/avif</item>
<item name="avifs" xsi:type="string">image/avif-sequence</item>
</argument>
</arguments>
</virtualType>
<type name="Magetu\BundleSelectionImage\Controller\Adminhtml\Product\Image\Upload">
<arguments>
<argument name="imageUploader" xsi:type="object">Magetu\BundleSelectionImage\ImageUpload</argument>
</arguments>
</type>
<preference for="Magento\Bundle\Model\LinkManagement" type="Magetu\BundleSelectionImage\Model\LinkManagement"/>
<type name="Magetu\BundleSelectionImage\Model\LinkManagement">
<arguments>
<argument name="imageUploader" xsi:type="object">Magetu\BundleSelectionImage\ImageUpload</argument>
</arguments>
</type>
</config>
- Create routes.xml to use the upload controller.
File path: app/code/Magetu/BundleSelectionImage/etc/adminhtml/routes.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd">
<router id="admin">
<route id="bundleselectionimage" frontName="bundleselectionimage">
<module name="Magetu_BundleSelectionImage" before="Magento_Backend"/>
</route>
</router>
</config>
- Create modifier class which defined in
di.xml
in the step 4 to modify bundle selection structure to add the selection image field:
File path: app/code/Magetu/BundleSelectionImage/Ui/DataProvider/Product/Form/Modifier/SelectionImage.php
<?php
namespace Magetu\BundleSelectionImage\Ui\DataProvider\Product\Form\Modifier;
use Magento\Bundle\Model\Product\Type;
use Magento\Bundle\Model\ResourceModel\Selection as SelectionResource;
use Magento\Bundle\Model\SelectionFactory;
use Magento\Bundle\Ui\DataProvider\Product\Form\Modifier\BundlePanel;
use Magento\Catalog\Model\Category\FileInfo;
use Magento\Catalog\Model\Locator\LocatorInterface;
use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\AbstractModifier;
use Magento\Framework\UrlInterface;
class SelectionImage extends AbstractModifier
{
public const FIELD_IMAGE = 'selection_image';
/**
* @var LocatorInterface
*/
private $locator;
/**
* @var UrlInterface
*/
private $urlBuilder;
/**
* @var FileInfo
*/
private $fileInfo;
/**
* @var SelectionFactory
*/
private $selectionFactory;
/**
* @var SelectionResource
*/
private $selectionResource;
public function __construct(
LocatorInterface $locator,
UrlInterface $urlBuilder,
FileInfo $fileInfo,
SelectionFactory $selectionFactory,
SelectionResource $selectionResource
) {
$this->locator = $locator;
$this->urlBuilder = $urlBuilder;
$this->fileInfo = $fileInfo;
$this->selectionFactory = $selectionFactory;
$this->selectionResource = $selectionResource;
}
/**
* {@inheritdoc}
*
* Converts selection image data to acceptable for rendering format
* Display selection image
*/
public function modifyData(array $data)
{
$product = $this->locator->getProduct();
$modelId = $product->getId();
$isBundleProduct = $product->getTypeId() === Type::TYPE_CODE;
if ($isBundleProduct && $modelId) {
$selectionModel = $this->selectionFactory->create();
foreach ($data[$modelId][BundlePanel::CODE_BUNDLE_OPTIONS][BundlePanel::CODE_BUNDLE_OPTIONS] as &$option) {
foreach ($option['bundle_selections'] as &$selection) {
$this->selectionResource->load($selectionModel, $selection['selection_id']);
$selectionImage = $selectionModel->getData('selection_image');
if ($selectionImage !== null && $this->fileInfo->isExist($selectionImage)) {
$stat = $this->fileInfo->getStat($selectionImage);
$mime = $this->fileInfo->getMimeType($selectionImage);
// phpcs:ignore Magento2.Functions.DiscouragedFunction
$imageUrl = $product->getStore()->getBaseUrl() . $selectionImage;
$imageRendering = [];
$imageRendering[0]['name'] = basename($selectionImage);
$imageRendering[0]['url'] = $imageUrl;
$imageRendering[0]['size'] = $stat['size'];
$imageRendering[0]['type'] = $mime;
$selectionModel->setData('selection_image', $imageRendering);
}
$selection['selection_image'] = $selectionModel->getData('selection_image');
}
}
}
return $data;
}
/**
* Add selection image field
*
* @param array $meta
* @return array
*/
public function modifyMeta(array $meta) {
$groupCode = BundlePanel::CODE_BUNDLE_DATA;
$meta[$groupCode]['children']['bundle_options']['children']['record']['children']
['product_bundle_container']['children']['bundle_selections']
['children']['record']['children'][static::FIELD_IMAGE] = $this->getSelectionImageFieldConfig();
return $meta;
}
/**
* Get selection image field config
*
* @return array
*/
private function getSelectionImageFieldConfig()
{
return [
'arguments' => [
'data' => [
'config' => [
'componentType' => 'imageUploader',
'formElement' => 'imageUploader',
'template' => 'Magetu_BundleSelectionImage/form/element/uploader/image',
'fileInputName' => static::FIELD_IMAGE,
'uploaderConfig' => [
'url' => $this->urlBuilder->getUrl(
'bundleselectionimage/product_image/upload'
),
],
'dataScope' => static::FIELD_IMAGE,
'sortOrder' => 129,// Before the last field (action_delete)
],
],
],
];
}
}
- Create image uploader html file to upload the selection image file:
File path: app/code/Magetu/BundleSelectionImage/view/adminhtml/web/template/form/element/uploader/image.html
<!--
/**
* Custom template for upload image file in bundle selection,
* based on Magento_Ui::form/element/uploader/image.html
*
* NOTE: the variable name of the upload file input name should not be declared as 'inputName'
* to allow the upload controller get the file
*/
-->
<div class="admin__field" visible="visible" css="$data.additionalClasses">
<div class="admin__field-control" css="'_with-tooltip': $data.tooltip">
<div class="file-uploader image-uploader" data-role="drop-zone" css="_loading: isLoading">
<div class="file-uploader-area">
<input type="file" afterRender="onElementRender" attr="id: uid, name: fileInputName, multiple: isMultipleFiles" disable="disabled" />
<label class="file-uploader-button action-default" attr="for: uid, disabled: disabled" disable="disabled" translate="'Upload'"></label>
<render args="fallbackResetTpl" if="$data.showFallbackReset && $data.isDifferedFromDefault"></render>
<p class="image-upload-requirements">
<span if="$data.maxFileSize">
<span translate="'Maximum file size'"></span>: <text args="formatSize($data.maxFileSize)"></text>.
</span>
<span if="$data.allowedExtensions">
<span translate="'Allowed file types'"></span>: <text args="getAllowedFileExtensionsInCommaDelimitedFormat()"></text>.
</span>
</p>
</div>
<render args="tooltipTpl" if="$data.tooltip"></render>
<div class="admin__field-note" if="$data.notice" attr="id: noticeId">
<span html="notice"></span>
</div>
<label class="admin__field-error" if="error" attr="for: uid" text="error"></label>
<each args="data: value, as: '$file'" render="$parent.getPreviewTmpl($file)"></each>
<div if="!hasData()" class="image image-placeholder" click="triggerImageUpload">
<div class="file-uploader-summary product-image-wrapper">
<div class="file-uploader-spinner image-uploader-spinner"></div>
<p class="image-placeholder-text" translate="'Browse to find or drag image here'"></p>
</div>
</div>
</div>
<render args="$data.service.template" if="$data.hasService()"></render>
</div>
</div>
- Create the uploader controller to handle upload the selection image file:
File path: app/code/Magetu/BundleSelectionImage/Controller/Adminhtml/Product/Image/Upload.php
<?php
namespace Magetu\BundleSelectionImage\Controller\Adminhtml\Product\Image;
use Magento\Backend\App\Action as BackendAction;
use Magento\Backend\App\Action\Context;
use Magento\Catalog\Model\ImageUploader;
use Magento\Framework\App\Action\HttpPostActionInterface;
use Magento\Framework\Controller\ResultFactory;
use Magento\Framework\Controller\ResultInterface;
/**
* The bundle selection image upload controller
*/
class Upload extends BackendAction implements HttpPostActionInterface
{
/**
* Authorization level of a basic admin session
*
* @see _isAllowed()
*/
public const ADMIN_RESOURCE = 'Magento_Catalog::products';
/**
* @var ImageUploader
*/
private $imageUploader;
public function __construct(
Context $context,
ImageUploader $imageUploader
) {
parent::__construct($context);
$this->imageUploader = $imageUploader;
}
/**
* Upload selection image file controller action
*/
public function execute(): ResultInterface
{
$imageId = $this->_request->getParam('param_name', 'selection_image');
try {
$result = $this->imageUploader->saveFileToTmpDir($imageId);
} catch (\Exception $e) {
$result = ['error' => $e->getMessage(), 'errorcode' => $e->getCode()];
}
return $this->resultFactory->create(ResultFactory::TYPE_JSON)->setData($result);
}
}
- Create LinkManagement model class to override
Magento\Bundle\Model\LinkManagement
class to save the bundle selection image value in the database:
File path:app/code/Magetu/BundleSelectionImage/Model/LinkManagement.php
Due to Magento 2.4.3 replaced the code method to map product link to bundle selection model, the content of our LinkManagement model class should be different:
- For Magento version >= 2.4.3
<?php
namespace Magetu\BundleSelectionImage\Model;
use Magento\Bundle\Api\Data\LinkInterface;
use Magento\Bundle\Api\Data\LinkInterfaceFactory;
use Magento\Bundle\Model\ResourceModel\Bundle;
use Magento\Bundle\Model\ResourceModel\BundleFactory;
use Magento\Bundle\Model\ResourceModel\Option\CollectionFactory;
use Magento\Bundle\Model\Selection;
use Magento\Bundle\Model\SelectionFactory;
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Catalog\Model\ImageUploader;
use Magento\Catalog\Model\Product;
use Magento\Framework\Api\DataObjectHelper;
use Magento\Framework\App\Filesystem\DirectoryList;
use Magento\Framework\EntityManager\MetadataPool;
use Magento\Framework\Exception\CouldNotSaveException;
use Magento\Framework\Exception\FileSystemException;
use Magento\Framework\Exception\InputException;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Framework\Filesystem;
use Magento\Framework\Filesystem\Directory\WriteInterface;
use Magento\Framework\Message\Manager as MessageManager;
use Magento\Store\Model\StoreManagerInterface;
class LinkManagement extends \Magento\Bundle\Model\LinkManagement
{
/**
* @var StoreManagerInterface
*/
private $storeManager;
/**
* @var MetadataPool
*/
private $metadataPool;
/**
* @var ImageUploader
*/
private $imageUploader;
/**
* @var MessageManager
*/
private $messageManager;
public function __construct(
ProductRepositoryInterface $productRepository,
LinkInterfaceFactory $linkFactory,
SelectionFactory $bundleSelection,
BundleFactory $bundleFactory,
CollectionFactory $optionCollection,
StoreManagerInterface $storeManager,
DataObjectHelper $dataObjectHelper,
MetadataPool $metadataPool,
ImageUploader $imageUploader,
MessageManager $messageManager
) {
parent::__construct(
$productRepository,
$linkFactory,
$bundleSelection,
$bundleFactory,
$optionCollection,
$storeManager,
$dataObjectHelper,
$metadataPool
);
$this->storeManager = $storeManager;
$this->metadataPool = $metadataPool;
$this->imageUploader = $imageUploader;
$this->messageManager = $messageManager;
}
/**
* This method has same content as in the parent class
* Declare it here to allow override mapProductLinkToBundleSelectionModel which is private
*
* @inheritDoc
*
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
* @SuppressWarnings(PHPMD.NPathComplexity)
*/
public function saveChild(
$sku,
LinkInterface $linkedProduct
) {
$product = $this->productRepository->get($sku, true);
if ($product->getTypeId() != Product\Type::TYPE_BUNDLE) {
throw new InputException(
__('The product with the "%1" SKU isn\'t a bundle product.', [$product->getSku()])
);
}
/** @var Product $linkProductModel */
$linkProductModel = $this->productRepository->get($linkedProduct->getSku());
if ($linkProductModel->isComposite()) {
throw new InputException(__('The bundle product can\'t contain another composite product.'));
}
if (!$linkedProduct->getId()) {
throw new InputException(__('The product link needs an ID field entered. Enter and try again.'));
}
/** @var Selection $selectionModel */
$selectionModel = $this->bundleSelection->create();
$selectionModel->load($linkedProduct->getId());
if (!$selectionModel->getId()) {
throw new InputException(
__(
'The product link with the "%1" ID field wasn\'t found. Verify the ID and try again.',
[$linkedProduct->getId()]
)
);
}
$selectionModel = $this->mapProductLinkToBundleSelectionModel(
$selectionModel,
$linkedProduct,
$product,
(int)$linkProductModel->getId()
);
try {
$selectionModel->save();
} catch (\Exception $e) {
throw new CouldNotSaveException(__('Could not save child: "%1"', $e->getMessage()), $e);
}
return true;
}
/**
* This method has same content as in the parent class
* Declare it here to allow override mapProductLinkToBundleSelectionModel which is private
*
* @inheritDoc
*
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
*/
public function addChild(
ProductInterface $product,
$optionId,
LinkInterface $linkedProduct
) {
if ($product->getTypeId() != Product\Type::TYPE_BUNDLE) {
throw new InputException(
__('The product with the "%1" SKU isn\'t a bundle product.', $product->getSku())
);
}
$linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField();
$options = $this->optionCollection->create();
$options->setIdFilter($optionId);
$options->setProductLinkFilter($product->getData($linkField));
$existingOption = $options->getFirstItem();
if (!$existingOption->getId()) {
throw new InputException(
__(
'Product with specified sku: "%1" does not contain option: "%2"',
[$product->getSku(), $optionId]
)
);
}
/* @var $resource Bundle */
$resource = $this->bundleFactory->create();
$selections = $resource->getSelectionsData($product->getData($linkField));
/** @var Product $linkProductModel */
$linkProductModel = $this->productRepository->get($linkedProduct->getSku());
if ($linkProductModel->isComposite()) {
throw new InputException(__('The bundle product can\'t contain another composite product.'));
}
if ($selections) {
foreach ($selections as $selection) {
if ($selection['option_id'] == $optionId &&
$selection['product_id'] == $linkProductModel->getEntityId() &&
$selection['parent_product_id'] == $product->getData($linkField)) {
if (!$product->getCopyFromView()) {
throw new CouldNotSaveException(
__(
'Child with specified sku: "%1" already assigned to product: "%2"',
[$linkedProduct->getSku(), $product->getSku()]
)
);
}
return $this->bundleSelection->create()->load($linkProductModel->getEntityId());
}
}
}
$selectionModel = $this->bundleSelection->create();
$selectionModel = $this->mapProductLinkToBundleSelectionModel(
$selectionModel,
$linkedProduct,
$product,
(int)$linkProductModel->getEntityId()
);
$selectionModel->setOptionId($optionId);
try {
$selectionModel->save();
$resource->addProductRelation($product->getData($linkField), $linkProductModel->getEntityId());
} catch (\Exception $e) {
throw new CouldNotSaveException(__('Could not save child: "%1"', $e->getMessage()), $e);
}
return (int)$selectionModel->getId();
}
/**
* Fill selection model with product link data.
* Save selection image value
*
* @param Selection $selectionModel
* @param LinkInterface $productLink
* @param ProductInterface $parentProduct
* @param int $linkedProductId
* @return Selection
* @throws FileSystemException
* @throws NoSuchEntityException
*/
private function mapProductLinkToBundleSelectionModel(
Selection $selectionModel,
LinkInterface $productLink,
ProductInterface $parentProduct,
int $linkedProductId
): Selection {
$linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField();
$selectionModel->setProductId($linkedProductId);
$selectionModel->setParentProductId($parentProduct->getData($linkField));
if ($productLink->getSelectionId() !== null) {
$selectionModel->setSelectionId($productLink->getSelectionId());
}
if ($productLink->getOptionId() !== null) {
$selectionModel->setOptionId($productLink->getOptionId());
}
if ($productLink->getPosition() !== null) {
$selectionModel->setPosition($productLink->getPosition());
}
if ($productLink->getQty() !== null) {
$selectionModel->setSelectionQty($productLink->getQty());
}
if ($productLink->getPriceType() !== null) {
$selectionModel->setSelectionPriceType($productLink->getPriceType());
}
if ($productLink->getPrice() !== null) {
$selectionModel->setSelectionPriceValue($productLink->getPrice());
}
if ($productLink->getCanChangeQuantity() !== null) {
$selectionModel->setSelectionCanChangeQty($productLink->getCanChangeQuantity());
}
if ($productLink->getIsDefault() !== null) {
$selectionModel->setIsDefault($productLink->getIsDefault());
}
$selectionModel->setWebsiteId((int)$this->storeManager->getStore($parentProduct->getStoreId())->getWebsiteId());
// Start customize
$selectionImage = $this->getSelectionImage($parentProduct, $productLink->getSelectionImage());
$selectionModel->setSelectionImage($selectionImage);
// End customize
return $selectionModel;
}
/**
* Move selection image file and get selection image path
* @param $parentProduct
* @param $selectionImageData
* @return string|null
* @throws FileSystemException
*/
private function getSelectionImage($parentProduct, $selectionImageData)
{
$selectionImage = null;
if ($selectionImageData !== null) {
$baseMediaDir = $parentProduct->getStore()->getBaseMediaDir();
$selectionImageName = $selectionImageData[0]['name'];
if (isset($selectionImageData[0]['tmp_name'])) {
// Save new uploaded selection image
try {
$newImgRelativePath = $this->imageUploader->moveFileFromTmp(
$selectionImageName,
true
);
$selectionImage = $baseMediaDir . '/' . $newImgRelativePath;
} catch (\Exception $e) {
$message = __('Saving selection image: ') . $e->getMessage();
$this->messageManager->addErrorMessage($message);
}
} else {
// Save existing selection image
/**
* It was converted in Modifier for rendering in the admin interface:
* Magetu\BundleSelectionImage\Ui\DataProvider\Product\Form\Modifier\SelectionImage::modifyData
*/
$selectionImage = $baseMediaDir . '/' . $this->imageUploader->getBasePath() . '/' . $selectionImageName;
}
}
return $selectionImage;
}
}
Full module directory structure
The module is now complete. Your module’s directory structure under app/code
should look like the following:
Magetu
|-- BundleSelectionImage
|
|-- Controller
| |-- Adminhtml
| |-- Product
| |-- Image
| |-- Upload.php
|-- etc
| |-- adminhtml
| | |-- di.xml
| | |-- routes.xml
| |-- db_schema.xml
| |-- module.xml
|-- Model
| |-- LinkManagement.php
|-- Ui
| |-- DataProvider
| |-- Product
| |-- Form
| |-- Modifier
| |-- SelectionImage.php
|-- view
| |-- adminhtml
| |-- web
| |-- template
| |-- form
| |-- element
| |-- uploader
| |-- image.html
|-- registration.php
Installing the module
Run the following commands to register the module, upgrade database, compile code and deploy static content:
bin/magento setup:upgrade
bin/magento setup:di:compile
bin/magento setup:static-content:deploy -f