Magento 2 How to add custom sort by option

If you want to use an attribute like created_at that it isn't in admin->stores->(attribute) product, because attributes defined in admin have the setting Sorting in Product Listing = Yes/No, you have to work with these two files:

\vendor\magento\module-catalog\Block\Product\ProductList\Toolbar.php \vendor\magento\module-catalog\Model\Config.php

In Toolbar.php you can see

$this->_availableOrder = $this->_catalogConfig->getAttributeUsedForSortByArray();

it calls getAttributeUsedForSortByArray() from Config.php that returns array of available attributes to sort listing collection.

Now, you have to add your created_at attribute here. How? I did it with a plugin

/**
 * Add sort order option created_at to frontend
 */
public function afterGetAttributeUsedForSortByArray(
    \Magento\Catalog\Model\Config $catalogConfig,
    $options
) {
    $options['created_at'] = __('New');
    return $options;
}

You inserted created_at in available attributes to sort, now you have only to build your custom collection to use it. Here i choose to override \vendor\magento\module-catalog\Block\Product\ProductList\Toolbar.php with mine Toolbar.php and override setCollection()

/**
 * Set collection to pager
 *
 * @param \Magento\Framework\Data\Collection $collection
 * @return $this
 */
 public function setCollection($collection) {
    $this->_collection = $collection;
    $this->_collection->setCurPage($this->getCurrentPage());

    // we need to set pagination only if passed value integer and more that 0
    $limit = (int)$this->getLimit();
    if ($limit) {
        $this->_collection->setPageSize($limit);
    }

    // switch between sort order options
    if ($this->getCurrentOrder()) {
        // create custom query for created_at option
        switch ($this->getCurrentOrder()) {
            case 'created_at':
                if ($this->getCurrentDirection() == 'desc') {
                    $this->_collection
                        ->getSelect()
                        ->order('e.created_at DESC');
                } elseif ($this->getCurrentDirection() == 'asc') {
                    $this->_collection
                        ->getSelect()
                        ->order('e.created_at ASC');           
                }
                break;
            default:
                $this->_collection->setOrder($this->getCurrentOrder(), $this->getCurrentDirection());
                break;
        }
    }

    // echo '<pre>';
    // var_dump($this->getCurrentOrder());
    // var_dump((string) $this->_collection->getSelect());
    // die;

    return $this;        
}

That's all, for me works like a charm.


We can achieve it by using Plugins. Please create following files in your module.

app/code/Package/CustomToolbar/etc/di.xml

<type name="Magento\Catalog\Model\Config">
    <plugin name="Package_CustomToolbar::addCustomOptions" type="Package\CustomToolbar\Plugin\Model\Config" />
</type>
<type name="Magento\Catalog\Block\Product\ProductList\Toolbar">
    <plugin name="Package_CustomToolbar::addPriceDecendingFilterInToolbar" type="Package\CustomToolbar\Plugin\Product\ProductList\Toolbar" />
</type>

app/code/Package/CustomToolbar/Plugin/Model/Config.php

namespace Package\CustomToolbar\Plugin\Model;
use Magento\Store\Model\StoreManagerInterface;
class Config
{
    protected $_storeManager;

public function __construct(
    StoreManagerInterface $storeManager
) {
    $this->_storeManager = $storeManager;

}

/**
 * Adding custom options and changing labels
 *
 * @param \Magento\Catalog\Model\Config $catalogConfig
 * @param [] $options
 * @return []
 */
public function afterGetAttributeUsedForSortByArray(\Magento\Catalog\Model\Config $catalogConfig, $options)
{
    $store = $this->_storeManager->getStore();
    $currencySymbol = $store->getCurrentCurrency()->getCurrencySymbol();

    //Remove specific default sorting options
    unset($options['position']);
    unset($options['name']);
    unset($options['price']);

    //Changing label
    $customOption['position'] = __('Relevance');

    //New sorting options
    $customOption['price_desc'] = __($currencySymbol.' (High to Low)');
    $customOption['price_asc'] = __($currencySymbol.' (Low to High)');

    //Merge default sorting options with custom options
    $options = array_merge($customOption, $options);

    return $options;
}
}

app/code/Package/CustomToolbar/Plugin/Product/ProductList/Toolbar.php

namespace Package\CustomToolbar\Plugin\Product\ProductList;
class Toolbar
{
    /**
     * Plugin
     *
     * @param \Magento\Catalog\Block\Product\ProductList\Toolbar $subject
     * @param \Closure $proceed
     * @param \Magento\Framework\Data\Collection $collection
     * @return \Magento\Catalog\Block\Product\ProductList\Toolbar
     */
    public function aroundSetCollection(
        \Magento\Catalog\Block\Product\ProductList\Toolbar $subject,
        \Closure $proceed,
        $collection
    ) {
        $currentOrder = $subject->getCurrentOrder();
        $result = $proceed($collection);

        if ($currentOrder) {
            if ($currentOrder == 'price_desc') {
                $subject->getCollection()->setOrder('price', 'desc');
            } elseif ($currentOrder == 'price_asc') {
                $subject->getCollection()->setOrder('price', 'asc');
            }
        }

        return $result;
    }
}

This is working fine for me without rewriting any Magento class.


If you want to use just Create At attribute, you can activate this attribute in admin panel in sorting options.

Example:

<?php

namespace Vendor\Module\Setup;

use Magento\Eav\Setup\EavSetup;
use Magento\Eav\Setup\EavSetupFactory;
use Magento\Framework\Setup\ModuleContextInterface;
use Magento\Framework\Setup\ModuleDataSetupInterface;
use Magento\Framework\Setup\UpgradeDataInterface;

class UpgradeData implements UpgradeDataInterface
{
    protected $eavSetupFactory;

    /**
     * UpgradeData constructor.
     *
     * @param EavSetupFactory $eavSetupFactory
     */
    public function __construct(
        EavSetupFactory $eavSetupFactory
    ) {
        $this->eavSetupFactory = $eavSetupFactory;
    }

    /**
     * @param ModuleDataSetupInterface $setup
     * @param ModuleContextInterface $context
     */
    public function upgrade(
        ModuleDataSetupInterface $setup,
        ModuleContextInterface $context
    ) {
        /** @var EavSetup $eavSetup */
        $eavSetup = $this->eavSetupFactory->create(['setup' => $setup]);

        if (version_compare($context->getVersion(), '2.1.1', '<')) {
            try {
                $entityType = $eavSetup->getEntityTypeId('catalog_product');
                $label = 'Created At';
                $eavSetup->updateAttribute($entityType, 'created_at', 'frontend_label', $label, null);
                $eavSetup->updateAttribute($entityType, 'created_at', 'used_for_sort_by', 1, null);
            } catch (LocalizedException $e) {
            }
        }
    }
}

This code from Setup/UpgradeData.php, but will be better to use InstallData.php instead.