Magento – Magento 2 : How faceted data works for layered navigation

filterlayered-navigationmagento2price

I have created module for custom filter on category page every thing is working fine except price range in layered navigation.

Please anybody can explain me how getFacetedData('price') works in magento2

$productCollection->getFacetedData('price');

This function gives me price ranges based on default product collection not based on my filtered collection.

FYI:
I have filtered collection as below,

$productCollection = $layer->getProductCollection()
->clear()
->addAttributeToSelect(['name','price'])
->addAttributeToFilter('sku', array('in' => ['sku1','sku2']));

Best Answer

The code below applies to Magento 2.2.5.

First off, in the sidebar, all possible ranges for all possible filters need to be produced. Additionally, to this, you will have the overview of the count of product found within the given range.

enter image description here

For example, I will be focusing on using one filter: the price.

Before anything else, in order for a given product attribute to be eligible for layered navigation, it should be properly configured.

To check, browse in the admin to Stores -> Attribute -> Product, then select the price attribute and observe that in the Storefront Properties tab, Use in Layered Navigation is set to Filterable (with results)

On this picture we see that for the price filter, we see the range from 50.00-59.99 contains 10 results, and for 80+ only 1.

This view is produced inside

/vendor/magento/theme-frontend-luma/Magento_LayeredNavigation/templates/layer/view.phtml

There is a code similar to

<?php foreach ($block->getFilters() as $filter): ?>
    <?php if ($filter->getItemsCount()): ?>

Which eventually stacks up to

private function prepareData($key, $count)

and this is a method from

vendor/magento/module-catalog-search/Model/Layer/Filter/Price.php

So, we have identified the class that is responsible for the price filtering, and we see it was already used to produce the available ranges.


The more important stack is to check what happens when a particular range is selected.

For example, I will click on the 40.00-49.99 range, which is expected to return 4 results.

First up is method _prepareLayout() from

/vendor/magento/module-layered-navigation/Block/Navigation.php

The code is

protected function _prepareLayout()
{
    foreach ($this->filterList->getFilters($this->_catalogLayer) as $filter) {
        $filter->apply($this->getRequest());
    }
    $this->getLayer()->apply();
    return parent::_prepareLayout();
}

In essence, this says, get me all filters and foreach of them do apply.

Now, the getFilters() alone, eventually leads to constructing an object from

vendor/magento/module-catalog-search/Model/Layer/Filter/Price.php

A calling step that leads to the __construct of Price is

protected function createAttributeFilter(
    \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute,
    \Magento\Catalog\Model\Layer $layer
) {
    $filterClassName = $this->getAttributeFilterClass($attribute);

    $filter = $this->objectManager->create(
        $filterClassName,
        ['data' => ['attribute_model' => $attribute], 'layer' => $layer]
    );
    return $filter;
}

And this is code from

vendor/module-catalog/Model/Layer/FilterList.php

Anyways, if we focus back to the $filter->apply($this->getRequest()); code from above, this means that this code will get executed

public function apply(\Magento\Framework\App\RequestInterface $request)
{
    /**
     * Filter must be string: $fromPrice-$toPrice
     */
    $filter = $request->getParam($this->getRequestVar());
    if (!$filter || is_array($filter)) {
        return $this;
    }

    $filterParams = explode(',', $filter);
    $filter = $this->dataProvider->validateFilter($filterParams[0]);
    if (!$filter) {
        return $this;
    }

    $this->dataProvider->setInterval($filter);
    $priorFilters = $this->dataProvider->getPriorFilters($filterParams);
    if ($priorFilters) {
        $this->dataProvider->setPriorIntervals($priorFilters);
    }

    list($from, $to) = $filter;

    $this->getLayer()->getProductCollection()->addFieldToFilter(
        'price',
        ['from' => $from, 'to' =>  empty($to) || $from == $to ? $to : $to - self::PRICE_DELTA]
    );

    $this->getLayer()->getState()->addFilter(
        $this->_createItem($this->_renderRangeLabel(empty($from) ? 0 : $from, $to), $filter)
    );

    return $this;
}

and again, this code is from

vendor/magento/module-catalog-search/Model/Layer/Filter/Price.php

If I closely follow variable values, again, given that I have selected the 40.00-49.99 range, then $filter is array consiting of two elements: [0 => 40, 1 => 50 ]

After this line is executed

list($from, $to) = $filter;

Obviously, the $from variable is now 40, and the $to variable is now 50.

The next line is crucial

    $this->getLayer()->getProductCollection()->addFieldToFilter(
        'price',
        ['from' => $from, 'to' =>  empty($to) || $from == $to ? $to : $to - self::PRICE_DELTA]
    );

This is where the already present collection associated with the Layer, gets further reduced by calling the addFieldToFilter().

Perhaps, this is where the attention should be placed in order to detect bugs, if any.

Eventually, the program calls getLoadedProductCollection() from

vendor/magento/module-catalog/Block/Product/ListProduct.php

which in efect returns the protected collection that this object encapsulates.


Magento is a complex application.

In this ones single click that selected one single range of prices, we saw code from three different modules interacting

  • module-catalog
  • module-catalog-search
  • module-layered-navigation

It may feel overwhelming at moments, but it seems to me, there is a nice synergy between this modules.

Thank you for reading. I hope this does explain and you are now equipped with slightly better understanding of the layered navigation.