Hello Friends!!!
In this blog, we are going to learn how we can create based on Magento 2 layered navigation for our custom collection on a custom page.
Sometimes, when we create a custom collection page in our module, we extend the \Magento\Catalog\Block\Product\ListProduct class and apply the filters in collection for our custom collection and it works.
But sometimes, we face wrong product count or pagination issues, or layered navigation issues on our custom collection page.
So, here I will explain in following steps to achieve the correct result for the custom collection page with the toolbar and layered navigation.
Step 1: Create di.xml file inside the app/code/Webkul/MyCustomCollection/etc/ directory.
<?xml version="1.0"?> <!-- /** * Webkul Software. * * @category Webkul * @package Webkul_MyCustomCollection * @author Webkul * @copyright Copyright (c) Webkul Software Private Limited (https://webkul.com) * @license https://store.webkul.com/license.html */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../lib/internal/Magento/Framework/ObjectManager/etc/config.xsd"> <!-- here custom layer class extends the default Magento layer class. That class instantiates a number of interfaces. --> <preference for="Magento\Catalog\Model\Layer\ContextInterface" type="Magento\Catalog\Model\Layer\Context" /> <preference for="Magento\Catalog\Model\Layer\ItemCollectionProviderInterface" type="Magento\Catalog\Model\Layer\Category\ItemCollectionProvider" /> <preference for="Magento\Catalog\Model\Layer\StateKeyInterface" type="Magento\Catalog\Model\Layer\Category\StateKey" /> <preference for="Magento\Catalog\Model\Layer\CollectionFilterInterface" type="Magento\Catalog\Model\Layer\Category\CollectionFilter" /> <preference for="Magento\Catalog\Model\Layer\FilterableAttributeListInterface" type="Magento\Catalog\Model\Layer\Category\FilterableAttributeList" /> <preference for="Magento\Catalog\Model\Layer\AvailabilityFlagInterface" type="Magento\Catalog\Model\Layer\Category\AvailabilityFlag" /> <preference for="Magento\Catalog\Model\ResourceModel\Category" type="Webkul\MyCustomCollection\Model\ResourceModel\Category" /> <preference for="Magento\Catalog\Model\ResourceModel\Layer\Filter\Price" type="Webkul\MyCustomCollection\Model\ResourceModel\Layer\Filter\Price" /> </config>
Step 2: Now, create Model files to override the collection data. Here, we will create Layer.php file inside the app/code/Webkul/MyCustomCollection/Model/ directory.
<?php /** * Webkul Software. * * @category Webkul * @package Webkul_MyCustomCollection * @author Webkul * @copyright Copyright (c) Webkul Software Private Limited (https://webkul.com) * @license https://store.webkul.com/license.html */ namespace Webkul\MyCustomCollection\Model; class Layer extends \Magento\Catalog\Model\Layer { public function getProductCollection() { $collection = parent::getProductCollection(); $collection->addAttributeToFilter('deal_status', ['eq' => '1']); $idArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; if (!empty($idArray)) { $collection->addAttributeToFilter('entity_id', ["in"=>$idArray]); } return $collection; } }
Now, we will create Resolver.php file inside the app/code/Webkul/MyCustomCollection/Model/Layer directory.
<?php /** * Webkul Software. * * @category Webkul * @package Webkul_MyCustomCollection * @author Webkul * @copyright Copyright (c) Webkul Software Private Limited (https://webkul.com) * @license https://store.webkul.com/license.html */ namespace Webkul\MyCustomCollection\Model\Layer; class Resolver extends \Magento\Catalog\Model\Layer\Resolver { public function __construct( \Magento\Framework\ObjectManagerInterface $objectManager, \Webkul\MyCustomCollection\Model\Layer $layer, array $layersPool ) { $this->layer = $layer; parent::__construct($objectManager, $layersPool); } public function create($layerType) { //$this->layer gets set in the constructor, so this create function //doesn't need to do anything. } }
Now, we will create Category.php file inside the app/code/Webkul/MyCustomCollection/Model/ResourceModel/ directory.
<?php /** * Webkul Software. * * @category Webkul * @package Webkul_MyCustomCollection * @author Webkul * @copyright Copyright (c) Webkul Software Private Limited (https://webkul.com) * @license https://store.webkul.com/license.html */ namespace Webkul\MyCustomCollection\Model\ResourceModel; class Category extends \Magento\Catalog\Model\ResourceModel\Category { public function getProductCount($category) { $collection = $category->getProductCollection(); $collection->addAttributeToFilter('deal_status', ['eq' => '1']); $idArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; if (!empty($idArray)) { $collection->addAttributeToFilter('entity_id', ["in"=>$idArray]); } return intval($collection->count()); } }
Now, we will create Price.php file inside the app/code/Webkul/MyCustomCollection/Model/ResourceModel/Layer/Filter directory.
<?php /** * Webkul Software. * * @category Webkul * @package Webkul_MyCustomCollection * @author Webkul * @copyright Copyright (c) Webkul Software Private Limited (https://webkul.com) * @license https://store.webkul.com/license.html */ namespace Webkul\MyCustomCollection\Model\ResourceModel\Layer\Filter; class Price extends \Magento\Catalog\Model\ResourceModel\Layer\Filter\Price { /** * @param \Magento\Framework\Model\ResourceModel\Db\Context $context * @param \Magento\Framework\Event\ManagerInterface $eventManager * @param \Magento\Catalog\Model\Layer\Resolver $layerResolver * @param \Magento\Customer\Model\Session $session * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Webkul\MyCustomCollection\Model\Layer $layer * @param null $connectionName */ public function __construct( \Magento\Framework\Model\ResourceModel\Db\Context $context, \Magento\Framework\Event\ManagerInterface $eventManager, \Magento\Catalog\Model\Layer\Resolver $layerResolver, \Magento\Customer\Model\Session $session, \Magento\Store\Model\StoreManagerInterface $storeManager, \Webkul\MyCustomCollection\Model\Layer $layer, $connectionName = null ) { $this->layer = $layerResolver->get(); $this->session = $session; $this->storeManager = $storeManager; parent::__construct( $context, $eventManager, $layerResolver, $session, $storeManager, $connectionName ); } public function getCount($range) { $select = $this->getSelect(); $priceExpression = $this->_getFullPriceExpression($select); /** * Check and set correct variable values to prevent SQL-injections */ $range = floatval($range); if ($range == 0) { $range = 1; } $countExpr = new \Zend_Db_Expr('COUNT(*)'); $rangeExpr = new \Zend_Db_Expr("FLOOR(({$priceExpression}) / {$range}) + 1"); $select->columns(['range' => $rangeExpr, 'count' => $countExpr]); $select->group($rangeExpr)->order(new \Zend_Db_Expr("({$rangeExpr}) ASC")); return $this->getConnection()->fetchPairs($select); } public function getSelect() { $collection = $this->layer->getProductCollection(); $collection->addAttributeToFilter('deal_status', ['eq' => '1']); $idArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; if (!empty($idArray)) { $collection->addAttributeToFilter('entity_id', ["in"=>$idArray]); } $collection->addPriceData( $this->session->getCustomerGroupId(), $this->storeManager->getStore()->getWebsiteId() ); $select = clone $collection->getSelect(); // reset columns, order and limitation conditions $select->reset(\Magento\Framework\DB\Select::COLUMNS); $select->reset(\Magento\Framework\DB\Select::ORDER); $select->reset(\Magento\Framework\DB\Select::LIMIT_COUNT); $select->reset(\Magento\Framework\DB\Select::LIMIT_OFFSET); // remove join with main table $fromPart = $select->getPart(\Magento\Framework\DB\Select::FROM); if (!isset( $fromPart[\Magento\Catalog\Model\ResourceModel\Product\Collection::INDEX_TABLE_ALIAS] ) || !isset( $fromPart[\Magento\Catalog\Model\ResourceModel\Product\Collection::MAIN_TABLE_ALIAS] ) ) { return $select; } // processing FROM part $priceIndexJoinPart = $fromPart[\Magento\Catalog\Model\ResourceModel\Product\Collection::INDEX_TABLE_ALIAS]; $priceIndexJoinConditions = explode('AND', $priceIndexJoinPart['joinCondition']); $priceIndexJoinPart['joinType'] = \Magento\Framework\DB\Select::FROM; $priceIndexJoinPart['joinCondition'] = null; $fromPart[\Magento\Catalog\Model\ResourceModel\Product\Collection::MAIN_TABLE_ALIAS] = $priceIndexJoinPart; unset($fromPart[\Magento\Catalog\Model\ResourceModel\Product\Collection::INDEX_TABLE_ALIAS]); $select->setPart(\Magento\Framework\DB\Select::FROM, $fromPart); foreach ($fromPart as $key => $fromJoinItem) { $fromPart[$key]['joinCondition'] = $this->_replaceTableAlias($fromJoinItem['joinCondition']); } $select->setPart(\Magento\Framework\DB\Select::FROM, $fromPart); // processing WHERE part $wherePart = $select->getPart(\Magento\Framework\DB\Select::WHERE); foreach ($wherePart as $key => $wherePartItem) { $wherePart[$key] = $this->_replaceTableAlias($wherePartItem); } $select->setPart(\Magento\Framework\DB\Select::WHERE, $wherePart); $excludeJoinPart = \Magento\Catalog\Model\ResourceModel\Product\Collection::MAIN_TABLE_ALIAS . '.entity_id'; foreach ($priceIndexJoinConditions as $condition) { if (strpos($condition, $excludeJoinPart) !== false) { continue; } $select->where($this->_replaceTableAlias($condition)); } $select->where($this->_getPriceExpression($select) . ' IS NOT NULL'); return $select; } }
Step 3: Create layout file mycollection_index_index.xml inside the app/code/Webkul/MyCustomCollection/view/frontend/layout/ directory.
<?xml version="1.0"?> <!-- /** * Webkul Software. * * @category Webkul * @package Webkul_MyMyCustomCollection * @author Webkul * @copyright Copyright (c) Webkul Software Private Limited (https://webkul.com) * @license https://store.webkul.com/license.html */ --> <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="2columns-left" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <body> <!--adding attribute tag to inherit the default stying for products--> <attribute name="class" value="page-products"/> <referenceContainer name="content"> <block class="Webkul\MyCustomCollection\Block\Product\ListProduct" name="mycollection_index_index" as="product_list" template="Magento_Catalog::product/list.phtml"> <container name="category.product.list.additional" as="additional" /> <block class="Magento\Framework\View\Element\RendererList" name="category.product.type.details.renderers" as="details.renderers"> <block class="Magento\Framework\View\Element\Template" name="category.product.type.details.renderers.default" as="default"/> </block> <block class="Magento\Catalog\Block\Product\ProductList\Item\Container" name="category.product.addto" as="addto"> <block class="Magento\Catalog\Block\Product\ProductList\Item\AddTo\Compare" name="category.product.addto.compare" as="compare" template="Magento_Catalog::product/list/addto/compare.phtml"/> </block> <block class="Magento\Catalog\Block\Product\ProductList\Toolbar" name="product_list_toolbar" template="Magento_Catalog::product/list/toolbar.phtml"> <block class="Magento\Theme\Block\Html\Pager" name="product_list_toolbar_pager"/> </block> <action method="setToolbarBlockName"> <argument name="name" xsi:type="string">product_list_toolbar</argument> </action> </block> </referenceContainer> <referenceContainer name="sidebar.main"> <block class="Webkul\MyCustomCollection\Block\Navigation" name="catalog.leftnav" as="navigation" before="-" template="Magento_LayeredNavigation::layer/view.phtml"> <block class="Webkul\MyCustomCollection\Block\Navigation\State" name="catalog.navigation.state" as="state" template="Magento_LayeredNavigation::layer/state.phtml" /> <block class="Magento\LayeredNavigation\Block\Navigation\FilterRenderer" name="catalog.navigation.renderer" as="renderer" template="Magento_LayeredNavigation::layer/filter.phtml"> <arguments> <argument name="product_layer_view_model" xsi:type="object">Magento\LayeredNavigation\ViewModel\Layer\Filter</argument> </arguments> </block> </block> </referenceContainer> <referenceBlock name="mycollection_index_index"> <arguments> <argument name="viewModel" xsi:type="object">Magento\Catalog\ViewModel\Product\OptionsData</argument> </arguments> </referenceBlock> </body> </page>
Step 4: Now, we will override the block files to get the required data.
Here, we will create Navigation.php file inside the app/code/Webkul/MyCustomCollection/Block/ directory.
<?php /** * Webkul Software. * * @category Webkul * @package Webkul_MyCustomCollection * @author Webkul * @copyright Copyright (c) Webkul Software Private Limited (https://webkul.com) * @license https://store.webkul.com/license.html */ namespace Webkul\MyCustomCollection\Block; class Navigation extends \Magento\LayeredNavigation\Block\Navigation { public function __construct( \Magento\Framework\View\Element\Template\Context $context, \Webkul\MyCustomCollection\Model\Layer\Resolver $layerResolver, \Magento\Catalog\Model\Layer\FilterList $filterList, \Magento\Catalog\Model\Layer\AvailabilityFlagInterface $visibilityFlag, array $data = [] ) { parent::__construct( $context, $layerResolver, $filterList, $visibilityFlag ); } } ?>
Then, create State.php file inside the app/code/Webkul/MyCustomCollection/Block/Navigation/ directory.
<?php /** * Webkul Software. * * @category Webkul * @package Webkul_MyCustomCollection * @author Webkul * @copyright Copyright (c) Webkul Software Private Limited (https://webkul.com) * @license https://store.webkul.com/license.html */ namespace Webkul\MyCustomCollection\Block\Navigation; class State extends \Magento\LayeredNavigation\Block\Navigation\State { public function __construct( \Magento\Framework\View\Element\Template\Context $context, \Webkul\MyCustomCollection\Model\Layer\Resolver $layerResolver, array $data = [] ) { parent::__construct( $context, $layerResolver, $data ); } }
Create ListProduct.php file inside the app/code/Webkul/MyCustomCollection/Block/Product/ directory.
<?php /** * Webkul Software. * * @category Webkul * @package Webkul_MyCustomCollection * @author Webkul * @copyright Copyright (c) Webkul Software Private Limited (https://webkul.com) * @license https://store.webkul.com/license.html */ namespace Webkul\MyCustomCollection\Block\Product; class ListProduct extends \Magento\Catalog\Block\Product\ListProduct { public function __construct( \Magento\Catalog\Block\Product\Context $context, \Magento\Framework\Data\Helper\PostHelper $postDataHelper, \Webkul\MyCustomCollection\Model\Layer\Resolver $layerResolver, \Magento\Catalog\Api\CategoryRepositoryInterface $categoryRepository, \Magento\Framework\Url\Helper\Data $urlHelper, array $data = [] ) { parent::__construct( $context, $postDataHelper, $layerResolver, $categoryRepository, $urlHelper, $data ); } }
Now, check the result on the front end. For reference, please look into the following image:
For a better understanding of layered-navigation related classes, please check “Anatomy of Magento 2: Layered Navigation”
Download the code.
Hope this will be helpful. Thanks 🙂
I have checked it on Magento 2.4.5, and it is working.
Do you have any third-party themes or third-party modules on your instance?
That can be conflicting with this.
You can share what exact error you are facing so that I can help.
Thanks.