Magento – Layered navigation for custom collection on custom page – magento2

collection;layered-navigationmagento2

I am working on fetching layered navigation in magento2 for a custom product collection. I am getting custom collection already on the custom page need to show layered navigation. Tried to adapt this magento1 solution but couldn't get far.

Any idea how could i achieve it in magento2. What i have done so far is as below:

Extended the Catalog ListProduct block for custom product list on my custom page.

class View extends \Magento\Catalog\Block\Product\ListProduct
{


    public function __construct(
    \Magento\Catalog\Block\Product\Context $context,
    \Magento\Framework\Data\Helper\PostHelper $postDataHelper,
    \Magento\Catalog\Model\Layer\Resolver $layerResolver,
    CategoryRepositoryInterface $categoryRepository,
    \Magento\Framework\Url\Helper\Data $urlHelper,
    array $data = [],
    \Custom\LayerNavigation\Model\Layer $testlayerobj
    ) {
        parent::__construct($context,$postDataHelper,$layerResolver,
        $categoryRepository,$urlHelper,$data);
        $this->_coreRegistry = $context->getRegistry();
        $this->_testlayer = $testlayerobj;
    }

    protected function _getProductCollection()
    {
        if ($this->_productCollection === null) {
          $this->_productCollection = $this->getLayer()->getProductCollection();
        }
        return $this->_productCollection;
     }

    public function getLayer()
    {

       $layer = $this->_coreRegistry->registry('current_layer');
       if ($layer) {
          return $layer;
        }
        return $this->_testlayer;
     }

}

Used the core Search block for layered navigation in layout file

<referenceContainer name="sidebar.main">
        <block class="Magento\LayeredNavigation\Block\Navigation\Search" name="catalogsearch.leftnav" before="-" template="layer/view.phtml">
            <block class="Magento\LayeredNavigation\Block\Navigation\State" name="catalogsearch.navigation.state" as="state" />
            <block class="Magento\LayeredNavigation\Block\Navigation\FilterRenderer" name="catalogsearch.navigation.renderer" as="renderer" template="layer/filter.phtml"/>
        </block>
</referenceContainer>

Extended Core layer model to modify the collection.

class Layer extends \Magento\Catalog\Model\Layer
{
    public function __construct(
      optionStoreFactory  $optionstoreFactory,
      Attrhelper $attrhelper,
      productCollectionFactory $productCollectionFactory,
      AttributevalueFactory $attributevalueFactory,
      CategoryRepositoryInterface $categoryRepository,
      \Magento\Store\Model\StoreManagerInterface $storeManager,
      \Magento\Framework\App\Request\Http $request,
      \Magento\Framework\Registry $registry,
      \Magento\Catalog\Model\Layer\Search\CollectionFilter $filter,
      array $data = []
    ) {
       $this->optionstoreFactory       = $optionstoreFactory;
       $this->attributevalueFactory    = $attributevalueFactory;
       $this->_attrhelper              = $attrhelper;
       $this->request                  = $request;
       $this->productCollectionFactory = $productCollectionFactory;
       $this->categoryRepository = $categoryRepository;
       $this->_storeManager = $storeManager;
       $this->filter = $filter;
       $this->registry = $registry;
    }

    public function getProductCollection()
    {
        $attributevalue = $this->getAttributeValue(); //eg: Manufacturer Attribute details
        $attr_code = $attributevalue->getAttributeCode();
        $attr_option_value = $attributevalue->getOptionId();
        $collection = $this->productCollectionFactory->create();
        $store_id = $this->request->getParam('store_id');
        $collection->addAttributeToSelect('*')
               ->addAttributeToFilter($attr_code , ['finset'=>$attr_option_value ])
               ->addAttributeToFilter('visibility','4')
               ->setStore($store_id)
               ->addFieldToFilter('status', array('eq' => 1))
               ->setOrder('id', 'DESC');
        $this->prepareProductCollection($collection);
        return $collection;
    }

    public function prepareProductCollection($collection)
    {
        $this->filter->filter($collection, $this->getCurrentCategory());
       return $this;
    }

    public function getCurrentCategory()
    {
       $category = $this->registry->registry('current_category');
       if ($category) {
           $this->setData('current_category', $category);
       } else {
           $category = $this->categoryRepository->get($this->getCurrentStore()->getRootCategoryId());
       $this->setData('current_category', $category);
       }
      return $category;
    }

    public function getCurrentStore()
    {
        return $this->_storeManager->getStore();
    }

}

As of now i am getting the layer navigation displayed but it ain't specific to my custom collection. As per my debugging the collection is filtered all the way from root category (Which contains whole product catalog) and according to it is fetching the layers.

Best Answer

I was able to achieve the navigation with below changes, I might not be fully correct with this solution so comments and suggestions are most welcome.

1) Prepare di.xml for frontend section as below:

<!-- Magento only includes 2 type of layer resolvers i.e Category and search whereas our custom page is neither a category page nor a search page so we need to add a new layer resolver on our custom page-->
<type name="Magento\Catalog\Model\Layer\Resolver">
    <arguments>
        <argument name="layersPool" xsi:type="array">
            <item name="category" xsi:type="string">Magento\Catalog\Model\Layer\Category</item>
            <item name="search" xsi:type="string">Magento\Catalog\Model\Layer\Search</item>
            <item name="customlayer" xsi:type="string">Custom\Navigation\Model\Layer</item>
        </argument>
    </arguments>
</type>

<!-- To prepare the filterlist for our custom collection which would be passed to the left navigation we need below virtual types for our custom page navigation -->
<virtualType name="customFilterList" type="Custom\Navigation\Model\Layer\FilterList">
    <arguments>
        <argument name="filterableAttributes" xsi:type="object">Custom\Navigation\Model\Layer\FilterableAttributeList</argument>
        <argument name="filters" xsi:type="array">
            <item name="attribute" xsi:type="string">Custom\Navigation\Model\Layer\Filter\Attribute</item>
            <item name="category" xsi:type="string">Custom\Navigation\Model\Layer\Filter\Category</item>
        </argument>
    </arguments>
</virtualType>

<!-- once the filter list virtual type is ready we can pass the same to our navigation , I have prepared the virtual type of the core navigation for my custom module and have passed the custom filter list to it -->
<virtualType name="Custom\Navigation\Block\Navigation\Custnavigation" type="Magento\LayeredNavigation\Block\Navigation">
    <arguments>
        <argument name="filterList" xsi:type="object">customFilterList</argument>
    </arguments>
</virtualType>

<!-- As we will be modifying the layer model collection we will need to extend the core model layer, Below virtual type will be required to extend the Catalog model layer else it will throw error for the context in construct method-->
<virtualType name="Custom\Navigation\Model\Layer\Context" type="Magento\Catalog\Model\Layer\Context">
    <arguments>
        <argument name="collectionProvider" xsi:type="object">Custom\Navigation\Model\Layer\ItemCollectionProvider</argument>
        <argument name="stateKey" xsi:type="object">Custom\Navigation\Model\Layer\StateKey</argument>
        <argument name="collectionFilter" xsi:type="object">Custom\Navigation\Model\Layer\CollectionFilter</argument>
    </arguments>
</virtualType>
<type name="Custom\Navigation\Model\Layer">
    <arguments>
        <argument name="context" xsi:type="object">Custom\Navigation\Model\Layer\Context</argument>
    </arguments>
</type>

2) Model Layer file: Extend the model layer file with your custom model and modify the collection.

namespace Custom\Navigation\Model;
class Layer extends \Magento\Catalog\Model\Layer
{

//Apart from the default construct argument you need to add your model from which your product collection is fetched.

    public function __construct(
        \Magento\Catalog\Model\Layer\ContextInterface $context,
        \Magento\Catalog\Model\Layer\StateFactory $layerStateFactory,
        AttributeCollectionFactory $attributeCollectionFactory,
        \Magento\Catalog\Model\ResourceModel\Product $catalogProduct,
        \Magento\Store\Model\StoreManagerInterface $storeManager,
        \Magento\Framework\Registry $registry,
        CategoryRepositoryInterface $categoryRepository,
        array $data = []
    ) {
    parent::__construct(
            $context,
            $layerStateFactory,
            $attributeCollectionFactory,
            $catalogProduct,
            $storeManager,
            $registry,
            $categoryRepository,
            $data
        );
    }

    public function getProductCollection()
    {

        /*Unique id is needed so that when product is loaded /filtered in the custom listing page it will be set in the
         $this->_productCollections array with unique key else you will not get the updated or proper collection.
        */

        if (isset($this->_productCollections['some_uinique_id'])) {
            $collection = $this->_productCollections['some_uinique_id'];
        } else {
            //$collection = Your logic to get your custom collection.
            $this->prepareProductCollection($collection);
            $this->_productCollections['some_unique_id'] = $collection;
        }

        return $collection;
    }

3) I have extended below files as used in di.xml (Have kept the file by just extending have not instantiated the construct method as i didn't required any changes in these file if needed you can modify the specific function in the extended file accordingly), Currently the solution i have applied wasn't able to resolve the category filter issue it still includes the root category filters so had to do a workaround to include the faceteddata (workaround mentioned in 4th point)

- Custom\Navigation\Model\Layer\FilterList extends
           \Magento\Catalog\Model\Layer\FilterList



 - Custom\Navigation\Model\Layer\FilterableAttributeList extends
   \Magento\Catalog\Model\Layer\Category\FilterableAttributeList



 - Custom\Navigation\Model\Layer\Filter\Attribute extends
   \Magento\Catalog\Model\Layer\Filter\Attribute



 - Custom\Navigation\Model\Layer\Filter\Category extends
   \Magento\CatalogSearch\Model\Layer\Filter\Category (Why catalog
           search is used i have mentioned the same in 4th point)



 - Custom\Navigation\Model\Layer\ItemCollectionProvider extends
   \Magento\Catalog\Model\Layer\Category\ItemCollectionProvider



 - Custom\Navigation\Model\Layer\StateKey extends
   \Magento\Catalog\Model\Layer\Category\StateKey



 - Custom\Navigation\Model\Layer\CollectionFilter extends
   \Magento\Catalog\Model\Layer\Category\CollectionFilter

4) Had to do a workaround for category filter in layered navigation as it was not showing option with respect to collection filtered, If any one finds the solution please do update. Below is the code i have used to fix the faceteddata error faced while including the category in my customfilterlist , Due to applying this patch the category option was not displayed in my navigation rest of the filters were appropriate as per my collection.

namespace Custom\Navigation\Model\Layer\Filter;

/**
 * Layer category filter
 */
class Category extends \Magento\CatalogSearch\Model\Layer\Filter\Category
{
     /**
      * @var \Magento\Framework\Escaper
      */
    private $escaper;

    /**
     * @var CategoryDataProvider
     */
    private $dataProvider;

    /**
     * @param \Magento\Catalog\Model\Layer\Filter\ItemFactory $filterItemFactory
     * @param \Magento\Store\Model\StoreManagerInterface $storeManager
     * @param \Magento\Catalog\Model\Layer $layer
     * @param \Magento\Catalog\Model\Layer\Filter\Item\DataBuilder $itemDataBuilder
     * @param \Magento\Catalog\Model\CategoryFactory $categoryFactory
     * @param \Magento\Framework\Escaper $escaper
     * @param CategoryManagerFactory $categoryManager
     * @param array $data
     */
    public function __construct(
        \Magento\Catalog\Model\Layer\Filter\ItemFactory $filterItemFactory,
        \Magento\Store\Model\StoreManagerInterface $storeManager,
        \Magento\Catalog\Model\Layer $layer,
        \Magento\Catalog\Model\Layer\Filter\Item\DataBuilder $itemDataBuilder,
        \Magento\Framework\Escaper $escaper,
        \Magento\Catalog\Model\Layer\Filter\DataProvider\CategoryFactory $categoryDataProviderFactory,
        array $data = []
    ) {
        parent::__construct(
            $filterItemFactory,
            $storeManager,
            $layer,
            $itemDataBuilder,
            $escaper,
            $categoryDataProviderFactory,
            $data
        );
        $this->_escaper = $escaper;
        $this->_requestVar = 'cat';
        $this->dataProvider = $categoryDataProviderFactory->create(['layer' => $this->getLayer()]);
    }

    /**
     * Get data array for building category filter items
     *
     * @return array
     */
    protected function _getItemsData()
    {
        /** @var \Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection $productCollection */
        $productCollection = $this->getLayer()->getProductCollection();

        $optionsFacetedData = '' ;// $productCollection->getFacetedData('category'); (Here i have set $optionsFacetedData as blank so that category option will not be included in layered navigation)
        $category = $this->dataProvider->getCategory();
        $categories = $category->getChildrenCategories();

        $collectionSize = $productCollection->getSize();

        if ($category->getIsActive()) {
            foreach ($categories as $category) {
                if ($category->getIsActive()
                    && isset($optionsFacetedData[$category->getId()])
                    && $this->isOptionReducesResults($optionsFacetedData[$category->getId()]['count'], $collectionSize)
                ) {
                    $this->itemDataBuilder->addItemData(
                        $this->escaper->escapeHtml($category->getName()),
                        $category->getId(),
                        $optionsFacetedData[$category->getId()]['count']
                    );
                }
            }
        }
        return $this->itemDataBuilder->build();
    }
}

5) When your custom page is loaded inside your controller execute method you need to set your custom layer which we added in the di.xml along with category and search layer.

 - include the below argument in your controller construct method.

     "\Magento\Catalog\Model\Layer\Resolver $layerResolver",

 - inside execute method set your custom layer resolver for your module.

    $this->layerResolver->create('customlayer');

6) In your layout xml file for the custom page add below code in body section.

<attribute name="class" value="page-with-filter"/>

<referenceContainer name="sidebar.main">
<!-- below is the virtual type of the core navigation we created -->
    <block class="Custom\Navigation\Block\Navigation\Custnavigation" name="custom.leftnav" before="-" template="Magento_LayeredNavigation::layer/view.phtml">
        <block class="Magento\LayeredNavigation\Block\Navigation\State" name="catalog.navigation.state" as="state" />
        <block class="Magento\LayeredNavigation\Block\Navigation\FilterRenderer" name="catalog.navigation.renderer" as="renderer" template="Magento_LayeredNavigation::layer/filter.phtml"/>
    </block>
</referenceContainer>
Related Topic