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>
I've been trying to achieve the same thing for a long time. I would really like if someone like Amit Bera could help us with this because there are many questions about "how to get layered navigation with custom collection" and not many usefull answers. Maybe this will help you a little and if you do succeed please share your answer.
I was able to get product collection by using layerResolver like this:
Vendor/Module/Controller/Index/Index.php
public function __construct(
\Magento\Framework\App\Action\Context $context,
\Magento\Framework\View\Result\PageFactory $pageFactory,
\Magento\Catalog\Model\Layer\Resolver $layerResolver
) {
$this->layerResolver = $layerResolver;
$this->pageFactory = $pageFactory;
$this->context = $context;
parent::__construct($context);
}
public function execute()
{
$date = new \Zend_Date();
$result = $this->pageFactory->create();
$this->layerResolver->create('search');
$collection = $this->layerResolver->get()->getProductCollection();
$collection->('special_price', ['gt'=>0],'left');
$collection
->addAttributeToFilter(
'special_from_date',
[
'or' => [
0 => [
'date' => true,
'to' => $date->get('YYYY-MM-dd').' 23:59:59'],
1 => [
'is' => new \Zend_Db_Expr('null')
],
]
],
'left'
)->addAttributeToFilter(
'special_to_date',
[
'or' => [
0 => [
'date' => true,
'from' => $date->get('YYYY-MM-dd').' 00:00:00'],
1 => [
'is' => new \Zend_Db_Expr('null')
],
]
],
'left'
);
$list = $result->getLayout()->getBlock('custom.products.list');
$list->setProductCollection($collection);
return $result;
}
To avoid problems with aggregations and buckets I use this:
Vendor/Module/etc/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">
<!-- 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="Vendor\Module\Model\Layer\FilterList">
<arguments>
<argument name="filterableAttributes" xsi:type="object">Vendor\Module\Model\Layer\FilterableAttributeList</argument>
<argument name="filters" xsi:type="array">
<item name="attribute" xsi:type="string">Vendor\Module\Model\Layer\Filter\Attribute</item>
<item name="category" xsi:type="string">Vendor\Module\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="Vendor\Module\Block\Navigation\Custnavigation" type="Magento\LayeredNavigation\Block\Navigation">
<arguments>
<argument name="filterList" xsi:type="object">customFilterList</argument>
</arguments>
</virtualType>
</config>
Vendor/Module/Model/Layer/FilterableAttributeList.php
namespace Vendor\Module\Model\Layer;
class FilterableAttributeList extends \Magento\Catalog\Model\Layer\Category\FilterableAttributeList
{
}
Vendor/Module/Model/Layer/FilterList.php
namespace Vendor\Module\Model\Layer;
class FilterList extends \Magento\Catalog\Model\Layer\FilterList
{
}
Vendor/Module/Model/Layer/Filter/Attribute.php
namespace Vendor\Module\Model\Layer\Filter;
class Attribute extends \Magento\Catalog\Model\Layer\Filter\Attribute
{
}
Vendor/Module/Model/Layer/Filter/Category.php
namespace Vendor\Module\Model\Layer\Filter;
class Category extends \Magento\CatalogSearch\Model\Layer\Filter\Category
{
}
Vendor/Module/view/frontend/layout/custompage_index_index.xml
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="2columns-left">
<body>
<attribute name="class" value="page-with-filter"/>
<referenceContainer name="sidebar.main">
<block class="Vendor\Module\Block\Navigation\Custnavigation" name="catalog.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>
<referenceContainer name="content">
<block class="Vendor\Module\Block\Product\CustomList" name="custom.products.list" 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" as="default"/>
</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>
</body>
</page>
Vendor/Module/Block/Product/CustomList.php
namespace Vendor\Module\Block\Product;
use Magento\Catalog\Block\Product\ListProduct;
use Magento\Catalog\Model\ResourceModel\Collection\AbstractCollection;
class CustomList extends ListProduct
{
public function getLoadedProductCollection()
{
return $this->_productCollection;
}
public function setProductCollection(AbstractCollection $collection)
{
$this->_productCollection = $collection;
}
}
This will give you filtered collection on custompage via avaiable special price today. You will have attributes to filter but i still cant get category filter to work properly ( count number for products in collection and layered navigation doesnt match and not all categories are displayed ). If anyone can provide answer on how to make category filter to work with this or other solutions please help.
Most of this code i found here on stackexchange in questions with layered-navigation tag (believe me when i say i searched a lot).
Hope this will get you somewhere. :)
Best Answer
I found the answer for this. (I think) While it makes sense at some level. The flow is kinda messed up. Here it goes:
As I stated in my question the collection used to display the products is a descendant of
Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection
that uses aSearchCriteriaBuilder
to perform the actual search.Still when debugging the actual display of the products I could not find any place where the filters were added. But in the _renderFiltersBefore() (method that is called before loading the collection) when the searchCriteriaBuilder was retrieved the filters were already there.
The searchCriteriaBuilder is retrieved using the objectManager::get() which implements a singleton pattern.
Long story short. The same collection(object type not instance) is used for the navigation block. And when rendering it the filters are applied here Magento\LayeredNavigation\Block\Navigation::_prepareLayout()
This are two separate blocks, working with different instances. But because of the singleton approach in retrieving the searchCriteriaBuilder when the ListProduct block loads the collection the filters are already set.
Basically what they've done (from what I can tell) is coupled the shit out of this two blocks.
I find this to be messed up on so many levels. If someone can prove me wrong. Please do.