yii2: Сортировка и фильтр gridview по связанным и вычисляемым полям

yii2 gridview настройка фильтра и сортировкиСортировка и фильтр gridview по вычисляемым или связанным полям не является сложной задачей, но она требует понимание принципов устройства модели в Yii 2.0.

Для тех, кто любит пощупать рабочий код руками, есть приложение. Ставится как и приложение Yii 2 basic. Миграция создаст нужные таблицы.

Все самое интересное в models/Person.php и models/PersonSearch.php.

Итак, приступим…

Исходные данные

Допустим, что мы имеем следующие связанные таблицы в базе данных:

/* Страны */
CREATE TABLE `tbl_country` (
    `id` INT(11) NOT NULL AUTO_INCREMENT COMMENT 'Уникальный идентификатор страны',
    `country_name` VARCHAR(150) NOT NULL COMMENT 'Название страны',
     PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='Основная таблица Страны';

/* Люди*/
CREATE TABLE `tbl_person` (
    `id` INT(11) NOT NULL AUTO_INCREMENT COMMENT 'Уникальный идентификатор человека',
    `first_name` VARCHAR(60) NOT NULL COMMENT 'Имя',
    `last_name` VARCHAR(60) NOT NULL COMMENT 'Фамилия',
    `country_id` INT(11) COMMENT 'Страна проживания',
    `parent_id` INT(11) COMMENT 'Родитель',
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='Основная таблица Люди';

/* Ключи */
ALTER TABLE `tbl_person`
ADD CONSTRAINT `tbl_person_FK1`
FOREIGN KEY (`country_id`)
REFERENCES `tbl_country` (`id`)
, ADD INDEX `tbl_person_FK1` (`country_id` ASC);

Подготовка

Воспользуемся замечательным gii для генерации моделей и crud. В итоге у нас должны получиться следующие классы:

  1. Person: Модель для таблицы tbl_person;
  2. PersonSearch: Класс поиска и фильтра для модели Person;
  3. Country: Модель для таблицы tbl_country;
  4. CountrySearch: Класс поиска и фильтра для модели Country.

План действий

Рассмотрим 3 варианта использования gridview в представлении index для класса Person.

Вариант первый: Сортировка и фильтр по вычисляемому полю

Настроим сортировку и фильтрацию по полю fullName класса Person, объединяющему first_name и second_name, разделенные пробелами.

Вариант второй: Сортировка и фильтр по вычисляемому полю из связанной таблицы

Добавим поле Страна в gridview класса Person из таблицы tbl_country, используя связь по ключу country_id и организуем сортировку и фильтр по этому полю.

Вариант третий: связанные записи из этой же таблицы

Для примера, добавим поле parentName, для отображения связанной записи этой же таблицы Person и настроим сортировку и фильтр.

Вариант первый

Шаг 1

Добавим геттер для поля fullName в модель Person:

/* Геттер для полного имени человека */
public function getFullName() {
    return $this->first_name . ' ' . $this->last_name;
}

/* Название атрибута для вывода на экран */
public function attributeLabels() {
    return [
        /* Другие атрибуты */
        'fullName' => 'Full Name'
    ];
}

Шаг 2

Добавим атрибут fullName в класс PersonSearch и настроим правила:

/* Вычисляемое поле */
public $fullName;

/* Настройка правил */
public function rules() {
   return [
    /* другие правила */
    [['fullName'], 'safe']
   ];
}

/**
 * Настроим поиск для использования
 * поля fullName
 */
public function search($params) {
    $query = Person::find();
    $dataProvider = new ActiveDataProvider([
        'query' => $query,
    ]);

    /**
     * Настройка параметров сортировки
     * Важно: должна быть выполнена раньше $this->load($params)
     */
    $dataProvider->setSort([
        'attributes' => [
            'id',
            'fullName' => [
                'asc' => ['first_name' => SORT_ASC, 'last_name' => SORT_ASC],
                'desc' => ['first_name' => SORT_DESC, 'last_name' => SORT_DESC],
                'label' => 'Full Name',
                'default' => SORT_ASC
            ],
            'country_id'
        ]
    ]);

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

    $this->addCondition($query, 'id');
    $this->addCondition($query, 'first_name', true);
    $this->addCondition($query, 'last_name', true);
    $this->addCondition($query, 'country_id');

    /* Настроим правила фильтрации */

    // фильтр по имени
    $query->andWhere('first_name LIKE "%' . $this->fullName . '%" ' .
        'OR last_name LIKE "%' . $this->fullName . '%"'
    );

    return $dataProvider;
}

Шаг 3

Добавим новое поле в виджет gridview:

echo GridView::widget([
    'dataProvider' => $dataProvider,
    'filterModel' => $searchModel,
    'columns' => [
        ['class' => 'yii\grid\SerialColumn'],
        'id',
        'fullName',
        ['class' => 'yii\grid\ActionColumn'],
    ]
]);

Готово! Теперь в gridview появилось синтетическое поле ФИО, по которому возможна сортировка и фильтрация.

Вариант 2

Шаг 1

Нужно убедиться, что в модели Person описана связь с моделью Country. Так же, желательно, описать геттер countryName.

/* Связь с моделью Страны*/
public function getCountry()
{
    return $this->hasOne(Country::className(), ['id' => 'country_id']);
}

/* Геттер для названия страны */
public function getCountryName() {
    return $this->country->country_name;
}

/* Название атрибута для вывода на экран */
public function attributeLabels() {
    return [
        /* Другие названия атрибутов */
        'fullName' => 'Full Name',
        'countryName' => 'Country Name',
    ];
}

Шаг 2

Добавим атрибут countryName в класс PersonSearch и настроим правила:

/* вычисляемый атрибут */
public $countryName;

/* правила валидации */
public function rules() {
   return [
    /* другие правила */
    [['countryName'], 'safe']
   ];
}

/**
 * Настроим поиск для использования
 * полей fullName и countryName
 */
public function search($params) {
    $query = Person::find();
    $dataProvider = new ActiveDataProvider([
        'query' => $query,
    ]);

    /**
     * Настройка параметров сортировки
     * Важно: должна быть выполнена раньше $this->load($params)
     * statement below
     */
     $dataProvider->setSort([
        'attributes' => [
            'id',
            'fullName' => [
                'asc' => ['first_name' => SORT_ASC, 'last_name' => SORT_ASC],
                'desc' => ['first_name' => SORT_DESC, 'last_name' => SORT_DESC],
                'label' => 'Full Name',
                'default' => SORT_ASC
            ],
            'countryName' => [
                'asc' => ['tbl_country.country_name' => SORT_ASC],
                'desc' => ['tbl_country.country_name' => SORT_DESC],
                'label' => 'Country Name'
            ]
        ]
    ]);

    if (!($this->load($params) && $this->validate())) {
        /**
         * Жадная загрузка данных модели Страны
         * для работы сортировки.
         */
        $query->joinWith(['country']);
        return $dataProvider;
    }

    $this->addCondition($query, 'id');
    $this->addCondition($query, 'first_name', true);
    $this->addCondition($query, 'last_name', true);
    $this->addCondition($query, 'country_id');

    // Фильтр по полному имени
    $query->andWhere('first_name LIKE "%' . $this->fullName . '%" ' .
        'OR last_name LIKE "%' . $this->fullName . '%"'
    );

    // Фильтр по стране
    $query->joinWith(['country' => function ($q) {
        $q->where('tbl_country.country_name LIKE "%' . $this->countryName . '%"');
    }]);

    return $dataProvider;
}

Шаг 3

Добавляем в gridview столбец для поля countryName:

echo GridView::widget([
    'dataProvider' => $dataProvider,
    'filterModel' => $searchModel,
    'columns' => [
        ['class' => 'yii\grid\SerialColumn'],
        'id',
        'fullName',
        'countryName',
        ['class' => 'yii\grid\ActionColumn'],
    ]
]);

Ура! Теперь у нас в gridview есть столбец с названием страны, которое берется из связанной таблицы.

Вариант третий

Шаг 1

Добавим модели Person связь к самой себе и геттер для нового атрибута ParentName:

/* Связь */
public function getParent() {
    return $this->hasOne(self::classname(),
           ['parent_id' => 'id'])->
           from(self::tableName() . ' AS parent');
}

/* Геттер для полного имени */
public function getFullName() {
    return $this->first_name . ' ' . $this->last_name;
}

/* Геттер для имени родителя */
public function getParentName() {
    return $this->parent->fullName;
}

/* Название атрибута для вывода на экран */
public function attributeLabels() {
    return [
        /* Другие названия атрибутов */
        'parentName' => 'Parent Name',
        'fullName' => 'Full Name',
    ];
}

Шаг 2

Добавим атрибут parentName в класс PersonSearch и настроим правила:

/* Вычисляемое поле */
public $parentName;

/* Настройка правил */
public function rules() {
   return [
    /* Другие правила */
    [['parentName'], 'safe']
   ];
}

/**
 * Настройка сортировки по полю parentName
 */
public function search($params) {
    $query = Person::find();
    $dataProvider = new ActiveDataProvider([
        'query' => $query,
    ]);

    /**
     * Настройка параметров сортировки
     * Важно: должна быть выполнена раньше $this->load($params)
     */
     $dataProvider->setSort([
        'attributes' => [
            'id',
            'parentName' => [
                'asc' => [
                    'parent.first_name' => SORT_ASC,
                    'parent.last_name' => SORT_ASC
                 ],
                'desc' => [
                     'parent.first_name' => SORT_DESC,
                     'parent.last_name' => SORT_DESC
                 ],
                'label' => 'Parent Name',
                'default' => SORT_ASC
            ],
            'fullName' => [
                'asc' => ['first_name' => SORT_ASC, 'last_name' => SORT_ASC],
                'desc' => ['first_name' => SORT_DESC, 'last_name' => SORT_DESC],
                'label' => 'Full Name',
                'default' => SORT_ASC
            ],
            'country_id'
        ]
    ]);

    if (!($this->load($params) && $this->validate())) {
        /**
 * Жадная загрузка данных модели Страны
 * для работы сортировки.
 */
        $query->joinWith(['parent']);
        return $dataProvider;
    }

    $this->addCondition($query, 'id');
    $this->addCondition($query, 'first_name', true);
    $this->addCondition($query, 'last_name', true);
    $this->addCondition($query, 'country_id');
    $this->addCondition($query, 'parent_id');

    // Фильтр по родительской записи
    $query->joinWith(['parent' => function ($q) {
        $q->where('parent.first_name LIKE "%' . $this->parentName . '%" ' .
        'OR parent.last_name LIKE "%' . $this->parentName . '%"');
    }]);

    return $dataProvider;
}

Шаг 3

Добавим в класс PersonSearch метод addCondition:

protected function addCondition($query, $attribute, $partialMatch = false)
{
    if (($pos = strrpos($attribute, '.')) !== false) {
        $modelAttribute = substr($attribute, $pos + 1);
    } else {
        $modelAttribute = $attribute;
    }

    $value = $this->$modelAttribute;
    if (trim($value) === '') {
        return;
    }

    /*
     * Для корректной работы фильтра со связью со
     * свой же моделью делаем:
     */
    $attribute = "tbl_person.$attribute";

    if ($partialMatch) {
        $query->andWhere(['like', $attribute, $value]);
    } else {
        $query->andWhere([$attribute => $value]);
    }
}

Шаг 4

Добавим столбец для отображения поля parentName в gridview:

echo GridView::widget([
    'dataProvider' => $dataProvider,
    'filterModel' => $searchModel,
    'columns' => [
        ['class' => 'yii\grid\SerialColumn'],
        'id',
        'fullName',
        'parentName',
        ['class' => 'yii\grid\ActionColumn'],
    ]
]);

Готово! Мы получили дополнительный столбец для поля, ссылающегося на связанную запись в этой же таблице с возможностью с работающим фильтром и сортировкой по этому полю.

32 thoughts on “yii2: Сортировка и фильтр gridview по связанным и вычисляемым полям

  1. П

    В первом варианте используется addCondition(), который описывается только в варианте 3. В оригинальном английском тексте такой же косяк.

  2. Сашка

    // Фильтр по стране
    $query->joinWith([‘country’ => function ($q) {
    $q->where(‘tbl_country.country_name LIKE «%’ . $this->countryName . ‘%»‘);
    }]);

    А конкретнее строку:
    $q->where(‘tbl_country.country_name LIKE «%’ . $this->countryName . ‘%»‘);

    лучше заменить на:
    $q->andFilterWhere([‘like’, ‘tbl_country.country_name’, $this->countryName]);

    Попробуйте в фильтре этого поля, вбить значение, выполнить поиск, потом значение стереть, и выполнить поиск.

  3. dm

    А как сделать чтоб вместо
    $query->joinWith([‘country’ => function ($q) {
    $q->where(‘tbl_country.country_name LIKE «%’ . $this->countryName . ‘%»‘);
    }]);

    Сделать left join так как в моем случае там может быть null?

  4. 2ray

    Что за классы PersonSearch, CountrySearch. По имени понятно, что для поиска. Где их размещать? И как вызывать?

  5. Степан

    Подскажите пожалуйста, нигде найти не могу ответ. У меня таблица также берет из другой данные ФИО и точно также её обрабатываю. Проблема в том, что мне нужно фильтровать данные по отделу. А он находится через ещё одну таблицу:
    основная таблица -> сотрудники -> отделы.
    Поэтому я смог сделать только сортировку по вашему образцу и то она сортирует не как строки, а как id, т.к. в таблице с сотрудниками id’шники из таблицы отделов, в которой находятся названия отделов.

  6. Иван

    У меня во втором варианте на 3 шаге не выводилось поле поиска, пока ‘country_name’ не заменил на
    [‘attribute’ => ‘countryName’,
    ‘value’ => ‘country.name’],

      1. Денис

        Подскажите пожалуйста. Если сделать как у Вас в статье
        return $this->country->country_name;
        то я получаю ошибку:
        PHP Notice – yii\base\ErrorException
        Trying to get property of non-object
        Я сделал так и все заработало:
        return $this->country[‘country_name’];
        Подскажите, с чем это связано?

        1. nix Автор записи

          Выложите код модели целиком куда-нибудь.

          Добавил в шапке обновление. Может быть поможет код приложения целиком?

  7. Yaroslav

    Фильтрация по полному имени в данном примере у меня не работает.
    Чтобы фильтрация нормально работала переписал
    // Фильтр по полному имени
    $query->andWhere(‘first_name LIKE «%’ . $this->fullName . ‘%» ‘ .
    ‘OR last_name LIKE «%’ . $this->fullName . ‘%»‘
    );

    на вот это
    $query->andFilterWhere([‘like’, ‘concat_ws(» «,name,surname,patronymic)’, $this->fullName]);

    1. Dmitry

      Да, вариант который приведен в статье, работает только если искать либо first_name, либо last_name, но не оба вместе. Ваш вариант лучше. Только там что-то с кавычками в разделителе concat_ws. Нужно заменить на обычные » «

  8. Радислав

    Кто сможет подсказать как сделать фильтр по действительно вычисляемому полю, к примеру есть записи в 2-х таблицах связь один ко многим. Так вот множество записей обрабатываются на пыхе, (обработка сложная поэтому на sql реализовать можно но получится страницы 2 формата а4) так вот вся эта обработка засунута в геттер модели, и возвращает число, вопрос как фильтровать по этому полю?
    Не прибегая к ArrayDataProvider или к фильтрации на фронте через дататаблю…

    1. nix Автор записи

      Третий вариант — добавить поле для хранения нужного значения и обновлять его при изменении зависимости.

  9. Радислав

    Возможно не сюда, но все же….

    как через GridView выводить динамическое количество колонок, а именно количество колонок будет будет зависеть от фиксированные данные к примеру 5 колонок + по колонке на каждый день периода (2016.07.25 — 2016.08.31 = 6 колонок).

    Данные хранятся в свойстве модели в массиве.
    Собственно вопрос можно ли как нибудь развернуть это свойство в стандартном GridView, если да то как?

    1. Варианты которые я нашел, отрисовать табличку руками.
    2. Написать свой GridView на основе существующего (со своим пасьянсом и куртизанками).
    3. Отказаться от GridView и забирать выхлоп в dataTable (все ровно она потом обрабатывает табличку, вариант плох в виду плохого знания JS)
    4. Найти другой GridView…

    4 сегодня вечерком займусь есть 2 наметки…

  10. Валенсия

    // ‘id_category2’,
    [
    ‘attribute’=>’category2’,
    ‘value’ =>’category2.name’,
    ‘filter’ => Html::activeDropDownList($searchModel, ‘id_category2’, ArrayHelper::map(Category2::find()->all(), ‘id’, ‘name’), [ ‘class’ =>’form-control’,’prompt’ => ‘<<>>’]),
    ],
    если этот код в ставить в GridView, то можно получить фильтр с выпадающим списком

  11. Ilia

    Мне кажется, или подобный код уязвим для SQL-инъекций ?

    $query->andWhere(‘first_name LIKE «%’ . $this->fullName . ‘%» ‘ .
    ‘OR last_name LIKE «%’ . $this->fullName . ‘%»‘
    );

  12. Andrew

    Отличная статья!
    Подскажите, пожалуйста, как сделать фильтр, который будет проверять наличие или отсутствие данных в определенном поле таблицы?
    Т.е. именно фильтровать записи по условиям:
    – поле field пустое (null, », etc)
    – поле field НЕ пустое
    Как такое можно реализовать в методе search?
    Какие условия добавить в запрос?

  13. annamavka

    Во втором варианте в моделе Person нужно было добавить еще переменную:

    public $countryName

    и в виджете GridView атрибут и значение:

    ‘contryName’ => [
    ‘attribute’ => ‘countryName’,
    ‘value’ => function($data) {
    return $data->country->name;
    },

    1. el

      Спасибо, что написали это здесь! Это решило проблему с ошибкой attribute read-only.

  14. Redfox

    Ребят я только одного не понимаю, как вы передаете $query вначале, потом его правите и отдаете ActiveDataProvider… Может все таки нужно передавать ссылкой?
    $dataProvider = new ActiveDataProvider([
    ‘query’ => $query,
    ]);

    ‘query’ => &$query,

    1. yiiness

      $query это экземпляр класса ActiveQuery, а экземпляры класса всегда передаются по ссылке.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *