Zend certified PHP/Magento developer

Update layer navigation filter with the proper custom collection

Goal : Create a /test/index/index route that will load a product listing page with a custom collection.

Expectation : The custom collection printed in listing and the filters must be properly defined from the collection we asked.

Issue : I can get the custom collection in the listing, but the layer navigation is not updated.

Technical requirements :

magento version : 2.4.3

"smile/elasticsuite": "~2.10.13"

Controller code :

/**
 * Execute action
 *
 * @return MagentoFrameworkViewResultPage
 */
public function execute()
{
    // Load the page with the filtered product collection
    $resultPage = $this->pageFactory->create();
    $resultPage->getConfig()->getTitle()->set((__('Search result')));
    return $resultPage;
}

Associated layout

<?xml version="1.0"?>
<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>
        <attribute name="class" value="spa-cat" />

        <referenceContainer name="sidebar.main">
            <block class="MagentoLayeredNavigationBlockNavigationCategory" name="catalog.leftnav" template="Smile_ElasticsuiteCatalog::layer/view.phtml">
                <block class="MagentoLayeredNavigationBlockNavigationState" name="catalog.navigation.state" as="state" />
                <block class="SmileElasticsuiteCatalogBlockNavigationFilterRenderer" name="catalog.navigation.renderer" as="renderer">
                    <block class="SmileElasticsuiteCatalogBlockNavigationRendererAttribute" name="catalogsearch.navigation.renderer.attribute" template="Smile_ElasticsuiteCatalog::layer/filter/attribute.phtml" after="-" />
                    <block class="SmileElasticsuiteCatalogBlockNavigationRendererCategory" name="catalogsearch.navigation.renderer.category" template="Smile_ElasticsuiteCatalog::layer/filter/default.phtml" />
                    <block class="SmileElasticsuiteCatalogBlockNavigationRendererSlider" name="catalog.navigation.renderer.slider" template="Smile_ElasticsuiteCatalog::layer/filter/slider.phtml" />
                    <block class="SmileElasticsuiteCatalogBlockNavigationRendererPriceSlider" name="catalog.navigation.renderer.price.slider" template="Smile_ElasticsuiteCatalog::layer/filter/slider.phtml" />
                </block>
            </block>
        </referenceContainer>

        <referenceContainer name="content">
            <block class="SaCustomCollectionBlockCollection" name="testplate_index_index" template="Sa_CustomCollection::product_list.phtml">
                <container name="category.product.list.additional" as="additional" />
                <block class="MagentoFrameworkViewElementRendererList" name="category.product.type.details.renderers" as="details.renderers">
                    <block class="MagentoFrameworkViewElementTemplate" name="category.product.type.details.renderers.default" as="default" />
                </block>
                <block class="MagentoCatalogBlockProductProductListToolbar" name="product_list_toolbar" template="Magento_Catalog::product/list/toolbar.phtml">
                    <block class="MagentoThemeBlockHtmlPager" name="product_list_toolbar_pager" />
                </block>
                <action method="setToolbarBlockName">
                    <argument name="name" xsi:type="string">product_list_toolbar</argument>
                </action>
            </block>
        </referenceContainer>

    </body>
</page>

Relevant part of the product_list template

<?php
/**
 * @var SaCustomCollectionBlockCollection $block
 * @var SmileElasticsuiteCatalogModelResourceModelProductFulltextCollection $_productCollection
 */


$_productCollection = $block->getFilteredProductCollection();
<?php if (!$_productCollection->count()) { ?>
    <div class="message info empty">
        <div><?= $escaper->escapeHtml(__('We can't find products matching the selection.')); ?></div>
    </div>
<?php } else { ?>
    <?= $block->getToolbarHtml(); ?>
    <?= $block->getAdditionalHtml(); ?>

    <div class="products wrapper grid products-grid">
        <ol class="spa-product-list">
            <?php foreach ($_productCollection as $_product) {

Collection Block that actually filter the collection (i’ve applied a filter on a local field, feel free to update with any other fields)

<?php

namespace SaCustomCollectionBlock;
use MagentoCatalogApiCategoryRepositoryInterface;
use MagentoCatalogBlockProductListProduct;
use MagentoCatalogHelperOutput as OutputHelper;
use MagentoCatalogModelLayerResolver;
use MagentoFrameworkDataHelperPostHelper;
use MagentoFrameworkUrlHelperData;
use SmileElasticsuiteCatalogModelResourceModelProductFulltextCollectionFactory as ElasticCollectionFactory;

class Collection extends ListProduct
{
    /**
     * @var ElasticCollectionFactory
     */
    protected $elasticCollectionFactory;

    public function __construct(MagentoCatalogBlockProductContext $context, PostHelper $postDataHelper,
                                Resolver $layerResolver, CategoryRepositoryInterface $categoryRepository,
                                ElasticCollectionFactory $elasticCollectionFactory,
                                Data $urlHelper, array $data = [], ?OutputHelper $outputHelper = null)
    {
        $this->elasticCollectionFactory = $elasticCollectionFactory;
        parent::__construct($context, $postDataHelper, $layerResolver, $categoryRepository, $urlHelper, $data, $outputHelper);
    }


    /**
     * Retrieve the filtered product collection
     *
     * @return MagentoCatalogModelResourceModelProductCollection
     */
    public function getFilteredProductCollection()
    {


        // Get the ElasticSuite product collection
        $productCollection = $this->elasticCollectionFactory->create()
            ->addFieldToSelect('name')
            ->addFieldToSelect('sku')
            ->addFieldToSelect('small_image')
            ->addFieldToSelect('price')
            ->addFieldToSelect('tws_piece_libelle')
            ->addFieldToSelect('tws_piece_marque_veh')
            ->addFieldToSelect('tws_piece_modele_veh')
            ->addFieldToSelect('tws_piece_annee_veh')
            ->addFieldToFilter('tws_piece_libelle', ['eq' =>'TRAPPE A CARBURANT'])
            ->setCurPage(1)
            ->setPageSize(12);

        $dir = "ASC";
        $attributeToSort = 'position';

        foreach ($this->getRequest()->getParams() as $name => $value){
            if($name != "product_list_order" && $name != "product_list_dir"){
                $productCollection->addFieldToFilter($name,['eq' => $value]);
            }else{
                if($name == "product_list_dir"){
                    $dir = "DESC";
                }
                if($name == "product_list_order"){
                    $attributeToSort = $value;
                }
            }
        }
        $productCollection->addAttributeToSort($attributeToSort,$dir);

        $this->getToolbarBlock()->setCollection($productCollection);
        $this->getLayer()->prepareProductCollection($productCollection);

        return $productCollection;
    }
}

I’m now currently working with a Navigation block which I think is responsible of my issue (i’m not using it so far cause i’m failing).

src/app/code/Sa/CustomCollection/Block/Navigation.php

<?php

namespace SaCustomCollectionBlock;

class Navigation extends SmileElasticsuiteCatalogBlockNavigation
{

    /**
     * @var MagentoFrameworkObjectManagerInterface
     */
    private $objectManager;

    /**
     * @var MagentoFrameworkModuleManager
     */
    private $moduleManager;

    /**
     * @var string[]
     */
    private $inlineLayouts = ['1column'];

    /**
     * @var string|NULL
     */
    private $pageLayout;

    /**
     * @var MagentoCatalogHelperData
     */
    protected $helperData;

    /**
     * @var Collection
     */
    protected $customCollection;

    /**
     * Navigation constructor.
     * @param MagentoFrameworkViewElementTemplateContext $context
     * @param MagentoCatalogModelLayerResolver $layerResolver
     * @param MagentoCatalogModelLayerFilterList $filterList
     * @param MagentoCatalogModelLayerAvailabilityFlagInterface $visibilityFlag
     * @param MagentoFrameworkObjectManagerInterface $objectManager
     * @param MagentoFrameworkModuleManager $moduleManager
     * @param MagentoCatalogHelperData $helperData
     * @param array $data
     * @throws MagentoFrameworkExceptionLocalizedException
     */
    public function __construct(
        MagentoFrameworkViewElementTemplateContext $context,
        MagentoCatalogModelLayerResolver $layerResolver,
        MagentoCatalogModelLayerFilterList $filterList,
        MagentoCatalogModelLayerAvailabilityFlagInterface $visibilityFlag,
        MagentoFrameworkObjectManagerInterface $objectManager,
        MagentoFrameworkModuleManager $moduleManager,
        MagentoCatalogHelperData $helperData,
        Collection $customCollection,
        array $data
    )
    {
        parent::__construct($context, $layerResolver, $filterList, $visibilityFlag, $objectManager, $moduleManager, $data);
        $this->pageLayout         = $context->getPageConfig()->getPageLayout() ?: $this->getLayout()->getUpdate()->getPageLayout();
        $this->objectManager      = $objectManager;
        $this->moduleManager      = $moduleManager;
        $this->helperData = $helperData;

        $this->customCollection = $customCollection;
        $customCollectionFiltered = $customCollection->getFilteredProductCollection();

        //$before = $this->_catalogLayer->getProductCollection();
        //var_dump("BEFORE: ".count($before));
        $this->_catalogLayer->prepareProductCollection($customCollectionFiltered);
        //$after = $this->_catalogLayer->getProductCollection();
        //var_dump("AFTER:" . count($after));
        //$this->getLayer()->prepareProductCollection($customCollectionFiltered);
    }

    /**
     * Check if we can show this block.
     * According to @return bool
     * @see MagentoLayeredNavigationStagingBlockNavigation::canShowBlock
     * We should not show the block if staging is enabled and if we are currently previewing the results.
     *
     */
    public function canShowBlock()
    {
        $canShowBlock = parent::canShowBlock();

        if ($this->moduleManager->isEnabled('Magento_Staging')) {
            try {
                $versionManager = $this->objectManager->get('MagentoStagingModelVersionManager');

                $canShowBlock = $canShowBlock && !$versionManager->isPreviewVersion();
            } catch (Exception $exception) {
                ;
            }
        }

        if ($this->getLayer() instanceof MagentoCatalogModelLayerCategory &&
            $this->getLayer()->getCurrentCategory()->getDisplayMode() === MagentoCatalogModelCategory::DM_PAGE) {
            $canShowBlock = false;
        }

        return $canShowBlock;
    }

    /**
     * Return index of the facets that are expanded for the current page :
     *
     *  - nth first facets (depending of config)
     *  - facets with at least one selected filter
     *
     * @return string
     */
    public function getActiveFilters()
    {
        $activeFilters = [];
        var_dump("SURCHARGE");
        if (!$this->isInline()) {
            $requestParams = array_keys($this->getRequest()->getParams());
            $displayedFilters = $this->getDisplayedFilters();
            $expandedFacets = $this->_scopeConfig->getValue(self::DEFAULT_EXPANDED_FACETS_COUNT_CONFIG_XML_PATH);
            $activeFilters = [];
            if ($expandedFacets > 0) {
                $activeFilters = range(0, min(count($displayedFilters), $expandedFacets) - 1);
            }

            foreach ($displayedFilters as $index => $filter) {
                if (in_array($filter->getRequestVar(), $requestParams)) {
                    $activeFilters[] = $index;
                }
            }
        }

        return json_encode($activeFilters);
    }

    /**
     * Returns facet that are displayed.
     *
     * @return array
     */
    public function getDisplayedFilters()
    {
        $displayedFilters = array_filter(
            $this->getFilters(),
            function ($filter) {
                return $filter->getItemsCount() > 0;
            }
        );

        return array_values($displayedFilters);
    }

    /**
     * Indicates if the block is displayed inline or not.
     *
     * @return boolean
     */
    public function isInline()
    {
        return in_array($this->pageLayout, $this->inlineLayouts);
    }

    /**
     * @return string
     */
    public function getCurrentCategory()
    {
        return is_null($this->helperData->getCategory()) ? '' : $this->helperData->getCategory()->getName();
    }
}

As you can see, the products printed are good. But the counting of categories (and all other filterable attributes) is false and not updated to use the custom collection we are providing).

product listing

That being said the filtering process is working properly, cause if i go from this page result to apply a filter then the product listing is properly filtered

product list after filter

But as you can see even after that, the filters counting and data printed are not coherent as i should only have 24 result max in counting of each filterable attribute.

Obviously if i was passing the attribute i filtered in the collection block, the result of both printed collection and layer navigation would be fine; but this is not something i want to do as i need to filter by so many parameter that it triggers an html error due to url being too long. So it has to be with a custom collection block.

I’m also expecting not to have the attribute filtered in collection block appearing as an active filter it has to be hidden from customer side. Otherwise i will have a thousand filter printed may be. Thats not what we want.

Basically, the behaviour should be the same as a category listing. When you filter on a category…the category is not printed as an active filter but still it’s there. So i’m looking to replicate that behaviour. It looks simple, but it really isn’t.

If in my layout I replace with this line

<block class="SaCustomCollectionBlockNavigation" name="catalog.leftnav" template="Smile_ElasticsuiteCatalog::layer/view.phtml">

Then this is the error i’m getting

Error: Cannot instantiate interface MagentoCatalogModelLayerFilterableAttributeListInterface in /var/www/magento/vendor/magento/framework/ObjectManager/Factory/Dynamic/Developer.php:50
Stack trace:
#0 /var/www/magento/vendor/magento/framework/ObjectManager/ObjectManager.php(70): MagentoFrameworkObjectManagerFactoryDynamicDeveloper->create()
#1 /var/www/magento/vendor/magento/framework/ObjectManager/Factory/AbstractFactory.php(170): MagentoFrameworkObjectManagerObjectManager->get()
#2 /var/www/magento/vendor/magento/framework/ObjectManager/Factory/AbstractFactory.php(276): MagentoFrameworkObjectManagerFactoryAbstractFactory->resolveArgument()
#3 /var/www/magento/vendor/magento/framework/ObjectManager/Factory/AbstractFactory.php(239): MagentoFrameworkObjectManagerFactoryAbstractFactory->getResolvedArgument()
#4 /var/www/magento/vendor/magento/framework/ObjectManager/Factory/Dynamic/Developer.php(34): MagentoFrameworkObjectManagerFactoryAbstractFactory->resolveArgumentsInRuntime()
#5 /var/www/magento/vendor/magento/framework/ObjectManager/Factory/Dynamic/Developer.php(59): MagentoFrameworkObjectManagerFactoryDynamicDeveloper->_resolveArguments()
#6 /var/www/magento/vendor/magento/framework/ObjectManager/ObjectManager.php(70): MagentoFrameworkObjectManagerFactoryDynamicDeveloper->create()
#7 /var/www/magento/vendor/magento/framework/ObjectManager/Factory/AbstractFactory.php(170): MagentoFrameworkObjectManagerObjectManager->get()
#8 /var/www/magento/vendor/magento/framework/ObjectManager/Factory/AbstractFactory.php(276): MagentoFrameworkObjectManagerFactoryAbstractFactory->resolveArgument()
#9 /var/www/magento/vendor/magento/framework/ObjectManager/Factory/AbstractFactory.php(239): MagentoFrameworkObjectManagerFactoryAbstractFactory->getResolvedArgument()
#10 /var/www/magento/vendor/magento/framework/ObjectManager/Factory/Dynamic/Developer.php(34): MagentoFrameworkObjectManagerFactoryAbstractFactory->resolveArgumentsInRuntime()
#11 /var/www/magento/vendor/magento/framework/ObjectManager/Factory/Dynamic/Developer.php(59): MagentoFrameworkObjectManagerFactoryDynamicDeveloper->_resolveArguments()
#12 /var/www/magento/vendor/magento/framework/ObjectManager/ObjectManager.php(56): MagentoFrameworkObjectManagerFactoryDynamicDeveloper->create()
#13 /var/www/magento/vendor/magento/framework/View/Element/BlockFactory.php(46): MagentoFrameworkObjectManagerObjectManager->create()
#14 /var/www/magento/vendor/magento/framework/View/Layout/Generator/Block.php(272): MagentoFrameworkViewElementBlockFactory->createBlock()
#15 /var/www/magento/vendor/magento/framework/View/Layout/Generator/Block.php(252): MagentoFrameworkViewLayoutGeneratorBlock->getBlockInstance()
#16 /var/www/magento/vendor/magento/framework/View/Layout/Generator/Block.php(229): MagentoFrameworkViewLayoutGeneratorBlock->createBlock()
#17 /var/www/magento/vendor/magento/framework/View/Layout/Generator/Block.php(134): MagentoFrameworkViewLayoutGeneratorBlock->generateBlock()
#18 /var/www/magento/vendor/magento/framework/View/Layout/GeneratorPool.php(93): MagentoFrameworkViewLayoutGeneratorBlock->process()
#19 /var/www/magento/vendor/magento/framework/View/Layout.php(365): MagentoFrameworkViewLayoutGeneratorPool->process()
#20 /var/www/magento/vendor/magento/framework/Interception/Interceptor.php(58): MagentoFrameworkViewLayout->generateElements()
#21 /var/www/magento/vendor/magento/framework/Interception/Interceptor.php(138): MagentoFrameworkViewLayoutInterceptor->___callParent()
#22 /var/www/magento/vendor/magento/framework/Interception/Interceptor.php(153): MagentoFrameworkViewLayoutInterceptor->MagentoFrameworkInterception{closure}()
#23 /var/www/magento/generated/code/Magento/Framework/View/Layout/Interceptor.php(68): MagentoFrameworkViewLayoutInterceptor->___callPlugins()
#24 /var/www/magento/vendor/magento/framework/View/Layout/Builder.php(129): MagentoFrameworkViewLayoutInterceptor->generateElements()
#25 /var/www/magento/vendor/magento/framework/View/Page/Builder.php(65): MagentoFrameworkViewLayoutBuilder->generateLayoutBlocks()
#26 /var/www/magento/vendor/magento/framework/View/Layout/Builder.php(65): MagentoFrameworkViewPageBuilder->generateLayoutBlocks()
#27 /var/www/magento/vendor/magento/framework/View/Page/Config.php(224): MagentoFrameworkViewLayoutBuilder->build()
#28 /var/www/magento/vendor/magento/framework/View/Page/Config.php(247): MagentoFrameworkViewPageConfig->build()
#29 /var/www/magento/app/code/Sa/CustomCollection/Controller/Index/Index.php(58): MagentoFrameworkViewPageConfig->getTitle()
#30 /var/www/magento/vendor/magento/framework/Interception/Interceptor.php(58): SaCustomCollectionControllerIndexIndex->execute()
#31 /var/www/magento/vendor/magento/framework/Interception/Interceptor.php(138): SaCustomCollectionControllerIndexIndexInterceptor->___callParent()
#32 /var/www/magento/vendor/magento/framework/Interception/Interceptor.php(153): SaCustomCollectionControllerIndexIndexInterceptor->MagentoFrameworkInterception{closure}()
#33 /var/www/magento/generated/code/Sa/CustomCollection/Controller/Index/Index/Interceptor.php(23): SaCustomCollectionControllerIndexIndexInterceptor->___callPlugins()
#34 /var/www/magento/vendor/magento/framework/App/Action/Action.php(111): SaCustomCollectionControllerIndexIndexInterceptor->execute()
#35 /var/www/magento/vendor/magento/framework/Interception/Interceptor.php(58): MagentoFrameworkAppActionAction->dispatch()
#36 /var/www/magento/vendor/magento/framework/Interception/Interceptor.php(138): SaCustomCollectionControllerIndexIndexInterceptor->___callParent()
#37 /var/www/magento/vendor/magento/framework/Interception/Interceptor.php(153): SaCustomCollectionControllerIndexIndexInterceptor->MagentoFrameworkInterception{closure}()
#38 /var/www/magento/generated/code/Sa/CustomCollection/Controller/Index/Index/Interceptor.php(32): SaCustomCollectionControllerIndexIndexInterceptor->___callPlugins()
#39 /var/www/magento/vendor/magento/framework/App/FrontController.php(245): SaCustomCollectionControllerIndexIndexInterceptor->dispatch()
#40 /var/www/magento/vendor/magento/framework/App/FrontController.php(212): MagentoFrameworkAppFrontController->getActionResponse()
#41 /var/www/magento/vendor/magento/framework/App/FrontController.php(147): MagentoFrameworkAppFrontController->processRequest()
#42 /var/www/magento/vendor/magento/framework/Interception/Interceptor.php(58): MagentoFrameworkAppFrontController->dispatch()
#43 /var/www/magento/vendor/magento/framework/Interception/Interceptor.php(138): MagentoFrameworkAppFrontControllerInterceptor->___callParent()
#44 /var/www/magento/vendor/magento/module-store/App/FrontController/Plugin/RequestPreprocessor.php(99): MagentoFrameworkAppFrontControllerInterceptor->MagentoFrameworkInterception{closure}()
#45 /var/www/magento/vendor/magento/framework/Interception/Interceptor.php(135): MagentoStoreAppFrontControllerPluginRequestPreprocessor->aroundDispatch()
#46 /var/www/magento/vendor/magento/module-page-cache/Model/App/FrontController/BuiltinPlugin.php(75): MagentoFrameworkAppFrontControllerInterceptor->MagentoFrameworkInterception{closure}()
#47 /var/www/magento/vendor/magento/framework/Interception/Interceptor.php(135): MagentoPageCacheModelAppFrontControllerBuiltinPlugin->aroundDispatch()
#48 /var/www/magento/vendor/magento/framework/Interception/Interceptor.php(153): MagentoFrameworkAppFrontControllerInterceptor->MagentoFrameworkInterception{closure}()
#49 /var/www/magento/generated/code/Magento/Framework/App/FrontController/Interceptor.php(23): MagentoFrameworkAppFrontControllerInterceptor->___callPlugins()
#50 /var/www/magento/vendor/magento/framework/App/Http.php(116): MagentoFrameworkAppFrontControllerInterceptor->dispatch()
#51 /var/www/magento/generated/code/Magento/Framework/App/Http/Interceptor.php(23): MagentoFrameworkAppHttp->launch()
#52 /var/www/magento/vendor/magento/framework/App/Bootstrap.php(264): MagentoFrameworkAppHttpInterceptor->launch()
#53 /var/www/magento/pub/index.php(29): MagentoFrameworkAppBootstrap->run()
#54 {main}

Navigation is defined as follow in di.xml too

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">

    <virtualType name="MagentoLayeredNavigationBlockNavigationCategory" type="SaCustomCollectionBlockNavigation">
        <arguments>
            <argument name="filterList" xsi:type="object">categoryFilterList</argument>
        </arguments>
    </virtualType>

    <virtualType name="MagentoLayeredNavigationBlockNavigationSearch" type="SaCustomCollectionBlockNavigation">
        <arguments>
            <argument name="filterList" xsi:type="object">searchFilterList</argument>
        </arguments>
    </virtualType>

    <virtualType name="MagentoLayeredNavigationStagingBlockNavigationCategory" type="SaCustomCollectionBlockNavigation">
        <arguments>
            <argument name="filterList" xsi:type="object">categoryFilterList</argument>
        </arguments>
    </virtualType>

    <virtualType name="MagentoLayeredNavigationStagingBlockNavigationSearch" type="SaCustomCollectionBlockNavigation">
        <arguments>
            <argument name="filterList" xsi:type="object">searchFilterList</argument>
        </arguments>
    </virtualType>
</config>