Когда ваш проект на Yii2 разрастается, стандартный список записей перестает быть удобным. Пользователям нужен быстрый поиск по названию, фильтрация по статусу, дате или цене. Если вы думаете, что для этого придется писать кучу сложного кода, спешу вас обрадовать: Yii2 предоставляет отличные инструменты, которые позволяют реализовать это элегантно и без лишней боли. В этой статье мы разберем, как создать кастомные фильтры и поиск, используя стандартные механизмы фреймворка, и я поделюсь проверенными приемами из своей практики.

Digital-студия WNDER

Почему стандартный GridView не всегда подходит

В Yii2 есть встроенный виджет GridView, который позволяет выводить данные в таблице с сортировкой и фильтрацией. Но он часто работает «из коробки» только с простыми полями модели. Когда начинаются хитрости — поиск по связанным таблицам, сложные условия, диапазоны дат или кастомные вычисляемые поля — стандартный подход перестает быть удобным. Код разрастается, условия начинают дублироваться, и поддерживать это становится трудно.

Основная идея кастомного подхода — вынести всю логику поиска в отдельный класс, который наследуется от ActiveRecord или SearchModel. Это позволяет:

  • Держать логику поиска в одном месте.
  • Использовать одно и то же условие в разных контроллерах и действиях.
  • Легко тестировать и расширять.
  • Избегать «магических» запросов прямо в контроллере.

Такой подход называют «Search Model» — это стандартный паттерн, который Yii2 предлагает через Gii. Но мы пойдем дальше и настроим его под свои нужды.

Создаем базовый SearchModel: от простого к сложному

Предположим, у нас есть модель Post (статья блога) с полями: title, status (0 или 1), created_at. Мы хотим сделать поиск по заголовку и фильтрацию по статусу. Создадим класс PostSearch в папке models.


namespace app\models;

use yii\base\Model;
use yii\data\ActiveDataProvider;

class PostSearch extends Model
{
    public $title;
    public $status;
    public $created_at;

    public function rules()
    {
        return [
            [['title', 'status'], 'safe'],
            [['created_at'], 'date', 'format' => 'php:Y-m-d'],
        ];
    }

    public function search($params)
    {
        $query = Post::find();

        $dataProvider = new ActiveDataProvider([
            'query' => $query,
            'sort' => [
                'defaultOrder' => ['created_at' => SORT_DESC],
            ],
        ]);

        // Загружаем данные из запроса
        if (!$this->load($params) || !$this->validate()) {
            return $dataProvider;
        }

        // Применяем фильтры
        $query->andFilterWhere(['like', 'title', $this->title]);
        $query->andFilterWhere(['status' => $this->status]);
        $query->andFilterWhere(['>=', 'created_at', $this->created_at ? strtotime($this->created_at . ' 00:00:00') : null]);

        return $dataProvider;
    }
}

Что здесь происходит? Мы создаем модель, которая хранит значения фильтров. Метод search() строит запрос, создает DataProvider (он отвечает за пагинацию и сортировку) и применяет условия. Важный момент — использование andFilterWhere. Этот метод автоматически игнорирует пустые значения, поэтому если пользователь ничего не ввел, условие не добавится.

Теперь в контроллере мы можем использовать этот класс:


public function actionIndex()
{
    $searchModel = new PostSearch();
    $dataProvider = $searchModel->search(Yii::$app->request->queryParams);

    return $this->render('index', [
        'searchModel' => $searchModel,
        'dataProvider' => $dataProvider,
    ]);
}

Это основа. Теперь можно усложнять.

Фильтры по связанным данным: когда нужно заглянуть в другую таблицу

Часто нужно фильтровать записи по полям связанных моделей. Например, у нас есть модель Comment (комментарий), связанная с Post. Мы хотим искать посты, у которых есть комментарии с определенным текстом. Для этого нужно присоединить связь и искать по ней.


public function search($params)
{
    $query = Post::find();

    // Добавляем условие: если ищем по комментарию, присоединяем связь
    $query->joinWith(['comments']);

    $dataProvider = new ActiveDataProvider([
        'query' => $query,
    ]);

    if (!$this->load($params) || !$this->validate()) {
        return $dataProvider;
    }

    // Фильтр по полю комментария
    $query->andFilterWhere(['like', 'comment.text', $this->comment_text]);

    // Чтобы избежать дубликатов (если у поста несколько комментариев), добавляем distinct
    $query->distinct();

    return $dataProvider;
}

Обратите внимание на distinct() — без него, если у поста несколько подходящих комментариев, он будет выводиться несколько раз. Также важно правильно настроить сортировку по связанным полям, если она нужна.

Сложные условия: диапазоны дат, мультифильтры и вычисляемые поля

Теперь представьте, что нужно фильтровать по диапазону дат. Вместо одного поля created_at у нас будет два: created_from и created_to. В модели добавляем эти атрибуты и правила валидации.


public $created_from;
public $created_to;

public function rules()
{
    return [
        [['created_from', 'created_to'], 'date', 'format' => 'php:Y-m-d'],
        // ...
    ];
}

public function search($params)
{
    // ...
    $query->andFilterWhere(['>=', 'created_at', $this->created_from ? strtotime($this->created_from . ' 00:00:00') : null]);
    $query->andFilterWhere(['<=', 'created_at', $this->created_to ? strtotime($this->created_to . ' 23:59:59') : null]);
    // ...
}

Для диапазонов чисел можно использовать аналогично >= и <=. А если нужно фильтровать по вычисляемому полю (например, сумма заказов), то можно добавить в запрос выражение:


$query->andFilterWhere([
    'and',
    ['>=', 'SUM(order_items.price)', $this->min_sum],
    ['<=', 'SUM(order_items.price)', $this->max_sum],
]);

Но помните, что здесь нужно группировать по первичному ключу, иначе условия будут некорректными. В таких случаях лучше использовать подзапросы или HAVING.

Интерфейс: как вывести форму фильтра без боли

Теперь о представлении. Вместо того чтобы вручную писать HTML для каждого поля, удобно использовать ActiveForm. Но чтобы не раздувать код, можно вынести фильтры в отдельный файл _search.php и подключать его в нужном месте.


<?php
use yii\helpers\Html;
use yii\widgets\ActiveForm;
?>

<div class="post-search">
    <?php $form = ActiveForm::begin([
        'method' => 'get',
        'action' => ['index'],
    ]); ?>

    <?= $form->field($model, 'title') ?>
    <?= $form->field($model, 'status')->dropDownList([0 => 'Черновик', 1 => 'Опубликовано'], ['prompt' => 'Выберите статус']) ?>
    <?= $form->field($model, 'created_from') ?>
    <?= $form->field($model, 'created_to') ?>

    <div class="form-group">
        <?= Html::submitButton('Искать', ['class' => 'btn btn-primary']) ?>
        <?= Html::a('Сбросить', ['index'], ['class' => 'btn btn-default']) ?>
    </div>

    <?php ActiveForm::end(); ?>
</div>

Вот и все. Форма будет отправлять GET-запрос на тот же экшен, а SearchModel сам разберется, что заполнено, а что нет. Важно использовать method => 'get', чтобы фильтры были в URL — это позволяет сохранять ссылку на результат поиска и удобно для пагинации.

Если вы не хотите использовать ActiveForm, можно просто вручную вывести поля и указать их имена в формате PostSearch[title].

Практические советы и лайфхаки

На основе своего опыта я выделяю несколько правил, которые помогают избежать типичных ошибок и сделать код чище.

Правило чистых фильтров: В методе search используйте только andFilterWhere или andWhere с явной проверкой на пустоту. Никогда не подставляйте значения напрямую в строку запроса — это опасно для безопасности и приводит к ошибкам.
Метод Когда использовать Пример
andFilterWhere Когда нужно игнорировать пустые значения ->andFilterWhere(['like', 'title', $this->title])
andWhere Когда условие обязательное или уже проверено на пустоту ->andWhere(['status' => 1])
andFilterCompare Для сравнений (>, <, >=, <=) ->andFilterCompare('created_at', $this->created_at, '>=')

Лайфхак 1: Если нужно фильтровать по нескольким полям одновременно, используйте andFilterWhere с массивом условий. Например, ->andFilterWhere(['like', 'title', $this->title]) и ->andFilterWhere(['like', 'description', $this->description]) — они автоматически объединятся через AND. Если нужно OR, используйте orFilterWhere.

Лайфхак 2: Для полнотекстового поиска по нескольким полям можно использовать такой трюк:


$query->andWhere([
    'or',
    ['like', 'title', $this->q],
    ['like', 'content', $this->q],
]);

Здесь q — общее поле поиска.

Лайфхак 3: Не забывайте про валидацию в SearchModel. Если поле не прошло валидацию, лучше не применять фильтр, чтобы избежать ошибок. В методе search мы вызываем validate(), и если возвращается false, возвращаем DataProvider без фильтров.

Что в итоге?

Создание кастомных фильтров в Yii2 — это не ракетостроение. Главное — правильно организовать код: вынести логику в отдельный SearchModel, использовать специальные методы для построения условий и аккуратно работать с формами. Это позволит вам добавлять новые фильтры за минуты, не раздувая контроллеры и представления.

Помните: фильтры — это часть пользовательского интерфейса, поэтому делайте их интуитивно понятными. Не перегружайте форму лишними полями, давайте понятные подсказки и всегда предусматривайте возможность сбросить фильтры.

Надеюсь, эта статья помогла вам разобраться. Теперь вы можете смело добавлять поиск и фильтрацию в свои проекты на Yii2, и это будет выглядеть профессионально и работать без сюрпризов.

Студия WNDER