SearchCriteria

SearchCriteria describes a query as data: filter groups, sort orders and paging, built with SearchCriteriaBuilder.

Syntax$searchCriteriaBuilder->addFilter(string $field, mixed $value, string $conditionType = 'eq')

Magento\Framework\Api\SearchCriteriaInterface is a plain object that describes a query: filter groups, sort orders, page size and current page. It holds no SQL and knows no table. Because it is only data, the same query can arrive from PHP, from a REST query string or from an admin grid and reach the same getList().

Groups are AND, filters inside a group are OR

A filter is a field, a value and a condition type. SearchCriteriaBuilder::addFilter($field, $value, $conditionType = 'eq') is the shorthand: it builds one Filter with FilterBuilder and puts it in a group of its own. The longhand is addFilters(array $filters) with the filters built by hand. They are the same mechanism, and the shorthand calls the longhand internally.

Several filters passed to one addFilters() call land in one group and are OR'd. Each separate call adds another group, and the groups are AND'd.

Condition types include eq, neq, like, in, nin, gt, gteq, lt, lteq, null and notnull.

Sorting, paging, reading

SortOrderBuilder produces a SortOrder. setAscendingDirection() is shorthand for setDirection(SortOrder::SORT_ASC); the descending pair matches. setPageSize() and setCurrentPage() page the result, and the first page is 1.

getList() returns a SearchResultsInterface: getItems() is the current page, getTotalCount() is the total ignoring paging, getSearchCriteria() hands the criteria back.

create() empties the builder, so one injected builder can build the next query safely.

Example

Example · php
<?php
// app/code/Vendor/Faq/Console/Command/ListQuestions.php
declare(strict_types=1);

namespace Vendor\Faq\Console\Command;

use Magento\Framework\Api\SearchCriteriaBuilder;
use Magento\Framework\Api\SortOrderBuilder;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Vendor\Faq\Api\QuestionRepositoryInterface;

class ListQuestions extends Command
{
    public function __construct(
        private readonly QuestionRepositoryInterface $questionRepository,
        private readonly SearchCriteriaBuilder $searchCriteriaBuilder,
        private readonly SortOrderBuilder $sortOrderBuilder,
        ?string $name = null
    ) {
        parent::__construct($name);
    }

    protected function configure(): void
    {
        $this->setName('vendor:faq:list')->setDescription('List active FAQ questions');
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        // setAscendingDirection() is shorthand for
        // setDirection(\Magento\Framework\Api\SortOrder::SORT_ASC).
        $sortOrder = $this->sortOrderBuilder->setField('sort_order')
            ->setAscendingDirection()
            ->create();

        $criteria = $this->searchCriteriaBuilder
            ->addFilter('is_active', 1)
            ->addFilter('store_id', [0, 1], 'in')
            ->addSortOrder($sortOrder)
            ->setPageSize(2)
            ->setCurrentPage(1)
            ->create();

        $result = $this->questionRepository->getList($criteria);
        foreach ($result->getItems() as $question) {
            $output->writeln($question->getQuestionId() . ' ' . $question->getTitle());
        }
        $output->writeln('total: ' . $result->getTotalCount());

        return Command::SUCCESS;
    }
}

// bin/magento vendor:faq:list
// 4 How do I return an item?
// 7 Where is my order?
// total: 11

When to use it

  • Fetch the active records of an entity, newest first, twenty at a time.
  • Build an OR search across two columns for an admin filter.
  • Export a large table in batches without exhausting memory.

More examples

The longhand, and how to get an OR

addFilter() can only ever produce AND, because each call makes its own group. The moment you need an OR you must drop to FilterBuilder and pass several filters to one addFilters() call. This is the usual reason a search that looks right returns nothing: three addFilter calls for three alternatives ask for all three at once.

Example · php
<?php
// app/code/Vendor/Faq/Model/QuestionSearch.php (fragment)
// Injected: FilterBuilder $filterBuilder, SearchCriteriaBuilder $searchCriteriaBuilder

$active = $this->filterBuilder
    ->setField('is_active')->setValue(1)->setConditionType('eq')->create();

$byTitle = $this->filterBuilder
    ->setField('title')->setValue('%' . $term . '%')->setConditionType('like')->create();

$byAnswer = $this->filterBuilder
    ->setField('answer')->setValue('%' . $term . '%')->setConditionType('like')->create();

$criteria = $this->searchCriteriaBuilder
    ->addFilters([$active])               // one group: the longhand of addFilter('is_active', 1)
    ->addFilters([$byTitle, $byAnswer])   // one group with two filters: OR
    ->create();

$result = $this->questionRepository->getList($criteria);

// Resulting shape:
// WHERE (is_active = 1) AND (title LIKE '%...%' OR answer LIKE '%...%')

Telling the repository how to apply the criteria

CollectionProcessorInterface::process(SearchCriteriaInterface $searchCriteria, AbstractDb $collection) is what turns the criteria object into filters, order and limit on a collection. A virtual type assembles the three stock processors for your entity, which is exactly how Magento_Cms configures its block repository. After this edit run bin/magento cache:clean config, and bin/magento setup:di:compile in production mode.

Example · xml
<?xml version="1.0"?>
<!-- app/code/Vendor/Faq/etc/di.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <virtualType name="Vendor\Faq\Model\Api\SearchCriteria\QuestionCollectionProcessor"
                 type="Magento\Framework\Api\SearchCriteria\CollectionProcessor">
        <arguments>
            <argument name="processors" xsi:type="array">
                <item name="filters" xsi:type="object">Magento\Framework\Api\SearchCriteria\CollectionProcessor\FilterProcessor</item>
                <item name="sorting" xsi:type="object">Magento\Framework\Api\SearchCriteria\CollectionProcessor\SortingProcessor</item>
                <item name="pagination" xsi:type="object">Magento\Framework\Api\SearchCriteria\CollectionProcessor\PaginationProcessor</item>
            </argument>
        </arguments>
    </virtualType>
    <type name="Vendor\Faq\Model\QuestionRepository">
        <arguments>
            <argument name="collectionProcessor" xsi:type="object">Vendor\Faq\Model\Api\SearchCriteria\QuestionCollectionProcessor</argument>
        </arguments>
    </type>
</config>

Paging through a large result

The pattern for anything that might return thousands of rows: page in fixed batches instead of asking for everything. It works because getTotalCount() ignores paging, so it is a stable loop bound. Note that the builder is reusable inside the loop: create() clears its state, so each pass starts from an empty criteria object.

Example · php
<?php
// app/code/Vendor/Faq/Model/QuestionExport.php (fragment)

$page = 1;
$pageSize = 200;

do {
    $criteria = $this->searchCriteriaBuilder
        ->addFilter('is_active', 1)
        ->setPageSize($pageSize)
        ->setCurrentPage($page)
        ->create();

    $result = $this->questionRepository->getList($criteria);

    foreach ($result->getItems() as $question) {
        $this->writeRow($question);
    }

    $written = ($page - 1) * $pageSize + count($result->getItems());
    $page++;
} while ($written < $result->getTotalCount());

// getTotalCount() stays 4310 on every pass; count(getItems()) is 200.

Discussion

  • Be the first to comment on this lesson.