Filtro customizado na layered navigation do Magento 2
Os pedidos de filtro customizado na layered navigation vêm de vários jeitos. Mostrar só produtos em destaque. Adicionar um filtro de promoção. Renderizar um atributo de sim ou não como um único checkbox em vez de uma lista com Yes e No. Antes de escrever código, ajuda saber como o Magento monta a lista de filtros no 2.4, porque a resposta certa costuma ser menos código do que se espera.
Como a layered navigation funciona no 2.4
O block Magento\LayeredNavigation\Block\Navigation pede seus filtros a um Magento\Catalog\Model\Layer\FilterList. Existem dois virtual types dessa classe, um para páginas de categoria e outro para resultados de busca, configurados no di.xml do Magento_LayeredNavigation. O FilterList retorna um filtro de categoria mais um filtro por atributo marcado como filtrável naquele contexto.
Durante a preparação do layout, o block chama apply() em cada filtro com a requisição atual. Um filtro que encontra seu parâmetro na URL adiciona uma condição à product collection do layer e uma entrada ao layer state, que é a caixa "Now Shopping by". Depois o template renderiza os itens de cada filtro com as contagens.
A product collection numa página de categoria ou de busca é uma search collection. As condições adicionadas com addFieldToFilter() viram uma query do OpenSearch, e as contagens ao lado de cada opção vêm de agregações do OpenSearch, que o Magento chama de facets. Isso importa para qualquer filtro customizado: o campo em que você filtra precisa existir no índice de busca, e as contagens precisam vir de um facet, não de uma query MySQL separada por opção.
Opção 1: um atributo de produto
A maioria dos filtros customizados são na verdade atributos. Se a loja quer filtrar por "featured", crie um atributo Yes/No ou dropdown, defina Use in Layered Navigation como Filterable (with results) e atribua aos attribute sets. O Magento adiciona ao índice, monta a agregação e renderiza o filtro. Nenhum código customizado.
Quando o valor é calculado, por exemplo "em promoção" ou "envio em 24 horas", a abordagem mais simples e confiável continua sendo um atributo, preenchido por um job de cron ou pela integração que conhece o valor. Filtrar por um atributo armazenado é barato. Calcular um valor na hora da query não é algo para o qual a camada de busca foi feita.
Crie o atributo com um data patch para que todo ambiente o receba. O padrão completo está em criando um atributo de produto com data patch. As propriedades que importam aqui são filterable igual a 1, e filterable_in_search se ele deve aparecer também nos resultados de busca.
Opção 2: mudar como um filtro é renderizado
Se o atributo está certo e só a aparência está errada, sobrescreva o template do renderer no seu tema. O template padrão dos itens de filtro é Magento_LayeredNavigation::layer/filter.phtml. Atributos de swatch usam o renderer do Magento_Swatches. Um override de tema em app/design/frontend/Vendor/theme/Magento_LayeredNavigation/templates/layer/filter.phtml permite mudar o markup de todos os filtros. Se você só precisa que um filtro tenha aparência diferente, faça um branch pela variável de requisição do filtro dentro do template.
Opção 3: uma classe de filtro customizada
Uma classe customizada vale a pena quando o filtro se comporta de forma diferente de um filtro de atributo normal. Um caso comum é mostrar um atributo booleano como um único link "Featured only" que aplica o valor 1, esconder a opção No e controlar o rótulo e a posição. O exemplo abaixo faz isso para um atributo chamado is_featured. O atributo precisa existir e ser filtrável, para ser indexado e ter uma agregação. O plugin então remove o filtro de atributo padrão e adiciona o customizado.
A classe do filtro
<?php
declare(strict_types=1);
namespace Vendor\LayeredNav\Model\Layer\Filter;
use Magento\Catalog\Model\Layer;
use Magento\Catalog\Model\Layer\Filter\AbstractFilter;
use Magento\Catalog\Model\Layer\Filter\Item\DataBuilder;
use Magento\Catalog\Model\Layer\Filter\ItemFactory;
use Magento\Framework\App\RequestInterface;
use Magento\Store\Model\StoreManagerInterface;
class Featured extends AbstractFilter
{
private const FIELD = 'is_featured';
public function __construct(
ItemFactory $filterItemFactory,
StoreManagerInterface $storeManager,
Layer $layer,
DataBuilder $itemDataBuilder,
array $data = []
) {
parent::__construct($filterItemFactory, $storeManager, $layer, $itemDataBuilder, $data);
$this->_requestVar = 'featured';
}
public function apply(RequestInterface $request)
{
if ((string)$request->getParam($this->getRequestVar()) !== '1') {
return $this;
}
$this->getLayer()->getProductCollection()->addFieldToFilter(self::FIELD, 1);
$this->getLayer()->getState()->addFilter(
$this->_createItem((string)__('Featured only'), '1')
);
$this->setItems([]);
return $this;
}
public function getName()
{
return __('Featured');
}
protected function _getItemsData()
{
$facets = $this->getLayer()->getProductCollection()->getFacetedData(self::FIELD);
$count = (int)($facets[1]['count'] ?? 0);
if ($count === 0) {
return [];
}
$this->itemDataBuilder->addItemData((string)__('Featured only'), '1', $count);
return $this->itemDataBuilder->build();
}
}
Alguns detalhes merecem explicação. O _requestVar é o parâmetro da URL, então a URL filtrada termina em ?featured=1. O getFacetedData() lê a agregação que o Magento já pediu para o atributo filtrável, então a contagem não custa nada a mais. Chamar setItems([]) depois de aplicar esconde a opção quando ela está ativa, que é como os filtros de atributo do core se comportam para valores de seleção única.
O plugin do FilterList
<?php
declare(strict_types=1);
namespace Vendor\LayeredNav\Plugin;
use Magento\Catalog\Model\Layer;
use Magento\Catalog\Model\Layer\Filter\AbstractFilter;
use Magento\Catalog\Model\Layer\FilterList;
use Vendor\LayeredNav\Model\Layer\Filter\FeaturedFactory;
class AddFeaturedFilter
{
private array $filters = [];
public function __construct(private readonly FeaturedFactory $featuredFactory)
{
}
public function afterGetFilters(FilterList $subject, array $result, Layer $layer): array
{
$key = spl_object_id($subject);
if (isset($this->filters[$key])) {
return $this->filters[$key];
}
$filters = [];
foreach ($result as $filter) {
if ($this->isFeaturedAttribute($filter)) {
continue;
}
$filters[] = $filter;
}
$filters[] = $this->featuredFactory->create(['layer' => $layer]);
return $this->filters[$key] = $filters;
}
private function isFeaturedAttribute(AbstractFilter $filter): bool
{
return $filter->hasAttributeModel()
&& $filter->getAttributeModel()->getAttributeCode() === 'is_featured';
}
}
O block de navegação chama getFilters() mais de uma vez por requisição, uma para aplicar e outra para renderizar. O FilterList faz cache do próprio resultado, e o plugin faz o mesmo para que o filtro aplicado seja a mesma instância que renderiza. O FeaturedFactory é gerado pelo Magento, então você não o escreve.
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">
<type name="Magento\Catalog\Model\Layer\FilterList">
<plugin name="vendor_layerednav_featured"
type="Vendor\LayeredNav\Plugin\AddFeaturedFilter"/>
</type>
</config>
Coloque em etc/frontend/di.xml para rodar só no storefront. Plugins declarados numa classe também se aplicam a virtual types construídos a partir dela, então isso cobre páginas de categoria e de busca. Se você quer só nas páginas de categoria, anexe o plugin ao nome do virtual type de categoria do di.xml do core.
Depois do deploy, rode bin/magento setup:di:compile em production mode e limpe o cache. Se o filtro aparece mas a contagem é sempre zero, o atributo não é filtrável naquele contexto ou os produtos não foram reindexados. O full page cache também guarda a navegação renderizada, então teste com o cache limpo.
Filtros que não são atributos
Às vezes o dado não está no produto, como um valor vindo de um sistema externo. Você pode adicionar campos customizados ao documento do produto no índice de busca, mas isso significa estender o mapeamento de dados do índice e a configuração da search request, e manter os dois em sincronia durante reindexações parciais. Na maioria dos projetos eu prefiro guardar o valor num atributo e deixar o core cuidar da indexação. É menos código para manter entre upgrades, e o comportamento é igual ao de todos os outros filtros.
Um filtro baseado num join MySQL adicionado à collection é a opção que eu evito. A search collection não suporta isso de forma limpa, as contagens acabam erradas e quebra no próximo upgrade.
Notas de performance
Cada atributo filtrável adiciona uma agregação a toda requisição de categoria, e cada opção é renderizada com uma URL. Listas grandes de opções, por exemplo um filtro de marca com duas mil marcas, deixam o HTML pesado e atrasam a renderização. Mantenha a lista de filtráveis curta, limite listas longas no template e confira o resultado com os passos do checklist de páginas de categoria lentas. As URLs da layered navigation também multiplicam as páginas rastreáveis, então decida cedo quais URLs filtradas os buscadores devem indexar.
FAQ
Por que meu atributo novo não aparece na layered navigation?
Confira se Use in Layered Navigation está como Filterable, se o atributo está nos attribute sets dos produtos, se a categoria é uma anchor category e se o catalogsearch_fulltext foi reindexado. Atributos sem valores nos produtos listados ficam escondidos.
Posso filtrar a layered navigation por faixas de preço que eu defino?
Sim. Em Catalog, Layered Navigation na configuração você pode escolher faixas automáticas ou um passo manual. Para faixas totalmente customizadas você estenderia o filtro de preço, o que é uma mudança maior que um filtro de atributo simples.
Filtros customizados funcionam com full page cache?
Sim. Cada URL filtrada é uma entrada de cache diferente porque a query string é diferente. Garanta que o filtro dependa só da requisição e não de dados de sessão, senão as páginas em cache mostrarão resultados errados.
Devo usar uma extensão de layered navigation de terceiros?
Para filtros de seleção múltipla, filtragem AJAX e URLs de filtro amigáveis para SEO, uma extensão madura economiza tempo. Para um ou dois filtros simples, um módulo pequeno como o acima é mais fácil de manter funcionando entre upgrades.
Atendo como freelancer em Magento e Adobe Commerce, incluindo mudanças de catálogo e layered navigation. Se você precisa de ajuda com isso, fale comigo.
Artigos relacionados
Como criar um tipo de produto customizado no Magento 2
Quando vale a pena um tipo de produto customizado no Magento 2 e como construir um: product_types.xml, type e price models, apply_to, inventário, indexação e GraphQL.
Magento 2: tipos de cache, clean, flush e cache type próprio
O que cada cache type do Magento 2 guarda, a diferença real entre cache:clean e cache:flush, caches invalidados e como criar seu próprio cache type.
Shipping method customizado no Magento 2: crie um módulo de carrier
Como criar um shipping method customizado no Magento 2.4: config.xml, system.xml, um carrier model com collectRates, regras de frete grátis e APIs externas de tarifa.
Atributos de produto no Magento 2: boas práticas para um catálogo limpo
Como projetar atributos de produto no Magento 2: input types, scope, flags de busca e filtro, attribute sets, data patches e limpeza de atributos sem uso.