SearchCriteria: búsquedas y filtros en Magento2

Clase SearchCriteriaBuilder

Cuando trabajamos con datos de una entidad, por ejemplo, consultando desde un endpoint, nos puede pasar que necesitemos filtrar información en base a un parámetro.

Como ya hemos ido viendo, en Magento2 no tenemos solamente Models. En Magento2 tenemos toda una nueva capa llamada Service Layer (en realidad tenemos Service Layer, Domain Layer y Persistence Layer).

Nuestra Service Layer, entre otras cosas, actúa con intermediario entre la capa de presentación y la capa de dominio. Esto sucede gracias a los Service Contracts.

A manera de recordatorio, los Service Contracts son interfases y existen tres posibles tipos de estas interfases:

  • Repository
  • Managment
  • Metada

¿Por qué esta introducción si el título del post tiene que ver con búsquedas?

Porque cuando operemos con estas entidades no podremos (o, por lo menos, no deberemos) hacer uso y abuso de consultas planas por SQL. Aquí deberemos respetar los Service Contracts y hacernos amigos de nuevas herramientas para operar con los datos del dominio.

En el post sobre la creación de un endpoint no hacía nada más que devolver un mensaje. Supongamos ahora que quisiéramos devolver un set de datos basados en algún parámetro. Supongamos también que lo que quisiera filtrar en mi endpoint custom fueran productos.

En términos de Magento1, recibiríamos y sanearíamos el parámetro para luego instanciar el modelo catalog/product y tomar la colección para luego hacer un addAttributeToFilter y pasar nuestro atributo y nuestro valor seleccionado.

En Magento2, si tenemos intención de apegarnos a la Service Layer (y demás amiguitos), para poder filtrar una colección de datos vamos a apoyarnos en el objeto SearchCriteria.

Para poder filtrar con SearchCriteria necesitaremos, al menos, de:

  • Repositorio de la entidad a filtrar (por ejemplo: Magento\Catalog\Api\ProductRepositoryInterface)
  • Magento\Framework\Api\SearchCriteriaBuilder
  • Magento\Framework\Api\FilterBuilder

Voy a dar por sentado que tenemos una clase en la cual en el constructor, le hemos inyectado las 3 clases mencionadas.

Ahora, en mi método en cuestión, voy a escribir mi filtro, consulta y el response.

//Definimos nuestro filtro, indicando atributo, operador y valor.
$filter[] = $this->filterBuilder
   ->setField('type_id')
   ->setConditionType('eq')
   ->setValue('simple')
   ->create();
 
//Agregamos el filtro al objeto searchCriteria.
$searchCriteria = $this->searchCriteriaBuilder->addFilters($filter);
 
//Construimos la instancia del criterio de búsqueda y lo inyectamos en el método de listado del repositorio.
$searchResults = $this->productRepository->getList($searchCriteria->create());
 
$response = array();
 
foreach ($searchResults->getItems() as $products) {
 
    $response[] = array(
        'sku' => $products->getSku(),
        'price' => $products->getPrice(),
        'name' => $products->getName()
    );
}

Voy a hacer un poco de trampa, porque en mi prueba real estoy metiendo este código dentro del método que devuelve resultado a un endpoint. Igualmente, lo que queremos destacar del ejemplo es el filtrado, no el output.

En mi ejemplo, el output de este ejemplo fue:

Output de filtro de un atributo

Y esto tiene sentido si miro mi catálogo.

Output de filtro de un atributo

Hasta aquí todo bien. Incluso el filtro funcionó porque mi catálogo tiene más productos (en la grilla no aparecen los productos con ID 1 y 2). Supongamos que quiero filtrar además de por tipo de producto, por su precio.

En este caso, vamos a agregar un filtro extra.

//Definimos nuestro primer filtro, indicando atributo, operador y valor.
$filter[] = $this->filterBuilder
   ->setField('type_id')
   ->setConditionType('eq')
   ->setValue('simple')
   ->create();
 
//Definimos un sergundo filtro que será agregado al array $filter
$filter[] = $this->filterBuilder
   ->setField('price')
   ->setConditionType('gt')
   ->setValue('100')
   ->create();
 
//Agregamos el filtro al objeto searchCriteria.
$searchCriteria = $this->searchCriteriaBuilder->addFilters($filter);
 
//Construimos la instancia del criterio de búsqueda y lo inyectamos en el método de listado del repositorio.
$searchResults = $this->productRepository->getList($searchCriteria->create());
 
$response = array();
 
foreach ($searchResults->getItems() as $products) {
 
    $response[] = array(
        'sku' => $products->getSku(),
        'price' => $products->getPrice(),
        'name' => $products->getName()
    );
}

La primer lectura sería que debería obtener, en base al resultado que hemos visto, 1 único producto que cumpla con las dos condiciones: ser de tipo simple y tener un precio mayor a 100.

El resultado ha sido:

Output de filtro de dos atributos

Si bien podría parecer un error, no lo es. Esto sucede porque lo que hemos hecho fue agregar filtros solamente, y cuando se agregan sólo filtros, los mismos se organizan dentro de un mismo grupo.

Todos los filtros dentro de un mismo grupo operan con OR. Es decir, que mi filtrado del último ejemplo fue algo como esto:

SELECT sku, price, name FROM catalog_product WHERE type_id = 'simple' OR price > '100';

(Claramente este query es figurativo y no representa las operaciones contra la entidad Product de Magento).

Ahora, ¿cómo debo hacer para filtrar con el operador AND?

Para poder usar el operador AND en una consulta debemos apoyarnos en los grupos de filtros. Si volvemos a nuestro ejemplo, para poder aplicar ambos filtros, el código necesitaría algunos pequeños cambios.

Lo primero, sería inyectar otra clase en nuestro constructor, de forma tal que ahora inyectaríamos:

  • Magento\Catalog\Api\ProductRepositoryInterface
  • Magento\Framework\Api\SearchCriteriaBuilder
  • Magento\Framework\Api\FilterBuilder
  • Magento\Framework\Api\Search\FilterGroup

Y ya en nuestro código, hacemos un ligero cambio:

//Definimos nuestro primer filtro, indicando atributo, operador y valor.
$filterType = $this->filterBuilder
   ->setField('type_id')
   ->setConditionType('eq')
   ->setValue('simple')
   ->create();
 
//Definimos un sergundo filtro independiente de $filterType
$filterPrice = $this->filterBuilder
   ->setField('price')
   ->setConditionType('gt')
   ->setValue('100')
   ->create();
 
//Creamos un primer grupo
$filterGroupType = $this->filterGroup;
//Clonamos el objeto para poder usarlo de forma independiente
$filterGroupPrice = clone $filterGroupType;
 
//Agregamos el filtro a un grupo
$filterGroupType->setFilters([$filterType]);
//Agregamos el otro filtro al segundo grupo
$filterGroupPrice->setFilters([$filterPrice]);
 
//Agregamos los grupos de filtros al objeto searchCriteria.
$searchCriteria = $this->searchCriteriaBuilder->setFilterGroups([$filterGroupType, $filterGroupPrice]);
 
//Construimos la instancia del criterio de búsqueda y lo inyectamos en el método de listado del repositorio.
$searchResults = $this->productRepository->getList($searchCriteria->create());
 
$response = array();
 
foreach ($searchResults->getItems() as $products) {
 
    $response[] = array(
        'sku' => $products->getSku(),
        'price' => $products->getPrice(),
        'name' => $products->getName()
    );
}

Y ahora revisamos nuestro output.

Output de filtro de dos atributos en grupos separados

Efectivamente, el filtrado fue correcto. Para que se comprenda un poco mejor el efecto de los grupos, vuelvo a hacer uso del pseudo-ejemplo en SQL.

Al haber usado los grupos, lo que sucedió fue:

SELECT sku, price, name FROM catalog_product WHERE (type_id = 'simple') AND (price > '100');

Es decir, cada filterGroup puede contener N filtros, y serán organizados dentro de un, si, grupo.

Como hemos visto, todos los atributos dentro de un grupo (o ante la ausencia de un grupo definido) funcionarán con el operador lógico OR.

Cuando hubieran múltiples grupos, entre grupo y grupo se aplicará el operador lógico AND.