While viewing a list of products or clients in our store, we sometimes need to carry out the same action on several records. For example, if we want to delete test users, we simply mark them, choose Actions > Delete > Submit and our store is in order in one click.

Mass actions on a data collection make it easier for us to run operations that we would normally have to repeat for every single record. There are 3 mass actions available in the backend by default:

  • Delete
  • Change status
  • Update attributes.

Although we can edit most of the product options with Update attributes, today we’ll add another mass action for changing the visibility of the product. This opens up the way to creating our own mass actions that we could user later on in our modules.

How does it work?

Magento enables us to use ui_component – the grid of products or clients based on what Magento sources from the XML files. Thanks to XML, we can overwrite, remove, and add mass actions, filters, or columns to our grids.

Let’s get to it!

As always, let’s start with the module:

//file: /app/code/Magently/Mass/registration.php

<?php
\Magento\Framework\Component\ComponentRegistrar::register(
    \Magento\Framework\Component\ComponentRegistrar::MODULE,
    'Magently_Mass',
    __DIR__
);
//file: /app/code/Magently/Mass/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="Magently_Mass" setup_version="0.1.0">
        <sequence>
            <module name="Magento_Catalog" />
        </sequence>
    </module>
</config>

Next, we need to extend the ui_component located in

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

//file: /app/code/Magently/Mass/view/adminhtml/ui_component/product_listing.xml

<?xml version="1.0" encoding="UTF-8"?>
<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd">
    <listingToolbar name="listing_top">
        <massaction name="listing_massaction">
            <action name="change_visibility">
                <argument name="data" xsi:type="array">
                    <item name="config" xsi:type="array">
                        <item name="type" xsi:type="string">visibility_status</item>
                        <item name="label" xsi:type="string" translate="true">Change Visibility</item>
                    </item>
                </argument>
                <argument name="actions" xsi:type="configurableObject">
                    <argument name="class" xsi:type="string">Magently\Mass\Ui\Component\MassAction\Visibility\ChangeStatus</argument>
                    <argument name="data" xsi:type="array">
                        <item name="urlPath" xsi:type="string">magently_mass/action/changeStatus</item>
                        <item name="paramName" xsi:type="string">visibility</item>
                    </argument>
                </argument>
            </action>
        </massaction>
    </listingToolbar>
</listing>

In the above code, we extend the default product listing by adding change_visibility to listing_massaction. In the actions argument, we pass on the name of the class that we will use to handle our mass action. In the data sent to the object, we convey the urlPath – the action that will be executed upon submitting the form.

//file: /app/code/Magently/Mass/Ui/Component/MassAction/Visibility/ChangeStatus.php

<?php

namespace Magently\Mass\Ui\Component\MassAction\Visibility;

class ChangeStatus implements \Zend\Stdlib\JsonSerializable
{
    /**
     * @var \Magento\Catalog\Model\Product\VisibilityFactory
     */
    protected $visibilityFactory;
    /**
     * @var array
     */
    protected $options;

    /**
     * @var array
     */
    protected $data;

    /**
     * @var \Magento\Framework\UrlInterface
     */
    protected $urlBuilder;

    /**
     * @var string
     */
    protected $urlPath;

    /**
     * @var string
     */
    protected $paramName;

    /**
     * @var array
     */
    protected $additionalData = [];

    /**
     * @param \Magento\Catalog\Model\Product\VisibilityFactory $visibilityFactory
     * @param \Magento\Framework\UrlInterface $urlBuilder
     * @param array $data
     */
    public function __construct(
        \Magento\Catalog\Model\Product\VisibilityFactory $visibilityFactory,
        \Magento\Framework\UrlInterface $urlBuilder,
        array $data = []
    ) {
        $this->data = $data;
        $this->urlBuilder = $urlBuilder;
        $this->visibilityFactory = $visibilityFactory;
    }

    /**
     * Get action options
     *
     * @return array
     */
    public function jsonSerialize()
    {
        if ($this->options === null) {
            $options = $this->visibilityFactory->create()->toOptionArray();
            $this->prepareData();
            foreach ($options as $option) {
                $this->options[$option['value']] = [
                    'type' => 'visibility_status' . $option['value'],
                    'label' => $option['label'],
                ];

                if ($this->urlPath && $this->paramName) {
                    $this->options[$option['value']]['url'] = $this->urlBuilder->getUrl(
                        $this->urlPath,
                        [$this->paramName => $option['value']]
                    );
                }

                $this->options[$option['value']] = array_merge_recursive(
                    $this->options[$option['value']],
                    $this->additionalData
                );
            }

            $this->options = array_values($this->options);
        }

        return $this->options;
    }

    /**
     * Prepare addition data for subactions
     *
     * @return void
     */
    protected function prepareData()
    {
        foreach ($this->data as $key => $value) {
            switch ($key) {
                case 'urlPath':
                    $this->urlPath = $value;
                    break;
                case 'paramName':
                    $this->paramName = $value;
                    break;
                default:
                    $this->additionalData[$key] = $value;
                    break;
            }
        }
    }
}

In this class, in the jsonSerialize() method, we use a factory to fetch the available Visibility statuses and then we return them – this will allows us to render the dropdown with our modification. When we go to the product listing now, we’ll see the new option in the mass actions.

Our mass action won’t work until we have a controller that will handle it. We need to create it in the adminhtml scope:

//file: /app/code/Magently/Mass/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="magently_mass" frontName="magently_mass">
            <module name="Magently_Mass"/>
        </route>
    </router>
</config>
//file: /app/code/Magently/Mass/Controller/Adminhtml/Action/ChangeStatus.php

<?php

namespace Magently\Mass\Controller\Adminhtml\Action;

class ChangeStatus extends \Magento\Catalog\Controller\Adminhtml\Product
{
    /**
     * @var \Magento\Catalog\Api\ProductRepositoryInterface
     */
    protected $productRepository;

    /**
     * @var \Magento\Ui\Component\MassAction\Filter $filter
     */
    protected $filter;

    /**
     * @var \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory
     */
    protected $collectionFactory;

    /**
     * @param \Magento\Backend\App\Action\Context $context
     * @param \Magento\Catalog\Controller\Adminhtml\Product\Builder $productBuilder
     * @param \Magento\Ui\Component\MassAction\Filter $filter
     * @param \Magento\Catalog\Api\ProductRepositoryInterface $productRepositoryInterface
     * @param \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $collectionFactory
     */
    public function __construct(
        \Magento\Backend\App\Action\Context $context,
        \Magento\Catalog\Controller\Adminhtml\Product\Builder $productBuilder,
        \Magento\Ui\Component\MassAction\Filter $filter,
        \Magento\Catalog\Api\ProductRepositoryInterface $productRepositoryInterface,
        \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $collectionFactory
    ) {
        parent::__construct($context, $productBuilder);
        $this->productRepository = $productRepositoryInterface;
        $this->filter = $filter;
        $this->collectionFactory = $collectionFactory;
    }

    /**
     * Execute controller
     * @return Magento\Framework\Controller\ResultFactor
     */
    public function execute()
    {
        $collection = $this->filter->getCollection($this->collectionFactory->create());
        $visibilityStatus = $this->getRequest()->getParam('visibility');

        $productsUpdated = 0;
        foreach ($collection->getAllIds() as $productId) {
            $productDataObject = $this->productRepository->getById($productId);
            $productDataObject->setData(
                'visibility',
                $visibilityStatus
            );

            $this->productRepository->save($productDataObject);
            $productsUpdated++;
        }

        if ($productsUpdated) {
            $this->messageManager->addSuccess(__('A total of %1 record(s) were updated.', $productsUpdated));
        }

        $resultRedirect = $this->resultFactory->create(\Magento\Framework\Controller\ResultFactory::TYPE_REDIRECT);
        $resultRedirect->setPath('catalog/product/index');
        return $resultRedirect;
    }
}

We fetch the collection of the selected products, and then update the Visibility status.

Here’s the effect of changing the product Visibility status in Search:

We hope that you enjoyed the article! If you have any questions, let us know in the comments below.