Сортировка и фильтр 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. В итоге у нас должны получиться следующие классы:
- Person: Модель для таблицы tbl_person;
- PersonSearch: Класс поиска и фильтра для модели Person;
- Country: Модель для таблицы tbl_country;
- 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'], ] ]);
Готово! Мы получили дополнительный столбец для поля, ссылающегося на связанную запись в этой же таблице с возможностью с работающим фильтром и сортировкой по этому полю.
В первом варианте используется addCondition(), который описывается только в варианте 3. В оригинальном английском тексте такой же косяк.
И никто не исправляет! Ты посмотри!
// Фильтр по стране
$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]);
Попробуйте в фильтре этого поля, вбить значение, выполнить поиск, потом значение стереть, и выполнить поиск.
А вообще Спасибо автору за полезную информацию!
Спасибо тебе хороший человек, очень помогло!
А как сделать чтоб вместо
$query->joinWith([‘country’ => function ($q) {
$q->where(‘tbl_country.country_name LIKE «%’ . $this->countryName . ‘%»‘);
}]);
Сделать left join так как в моем случае там может быть null?
Что за классы PersonSearch, CountrySearch. По имени понятно, что для поиска. Где их размещать? И как вызывать?
Классы расширяют модель, содержат метод search, возвращающий экземпляр ActiveDataProvider. Пример из статьи YII2: РАЗБИРАЕМСЯ С GRIDVIEW.
Спасибо большое! Статью очень помогла)))
Подскажите пожалуйста, нигде найти не могу ответ. У меня таблица также берет из другой данные ФИО и точно также её обрабатываю. Проблема в том, что мне нужно фильтровать данные по отделу. А он находится через ещё одну таблицу:
основная таблица -> сотрудники -> отделы.
Поэтому я смог сделать только сортировку по вашему образцу и то она сортирует не как строки, а как id, т.к. в таблице с сотрудниками id’шники из таблицы отделов, в которой находятся названия отделов.
Перечитайте второй вариант, на его основе можно сделать!
У меня во втором варианте на 3 шаге не выводилось поле поиска, пока ‘country_name’ не заменил на
[‘attribute’ => ‘countryName’,
‘value’ => ‘country.name’],
в первом шаге метод getCountryName() объявлен корректно?
Так:
public function getCountryName () {
return $this->country->name;
}
Попробуйте так:
return $this->country[‘country_name’];
Подскажите пожалуйста. Если сделать как у Вас в статье
return $this->country->country_name;
то я получаю ошибку:
PHP Notice – yii\base\ErrorException
Trying to get property of non-object
Я сделал так и все заработало:
return $this->country[‘country_name’];
Подскажите, с чем это связано?
Выложите код модели целиком куда-нибудь.
Добавил в шапке обновление. Может быть поможет код приложения целиком?
Извините за задержку.
Модель простейшая. Вот код:
http://pastie.org/private/os7ug48uqhor5pc8mu06kq
Речь о втором способе.
Фильтрация по полному имени в данном примере у меня не работает.
Чтобы фильтрация нормально работала переписал
// Фильтр по полному имени
$query->andWhere(‘first_name LIKE «%’ . $this->fullName . ‘%» ‘ .
‘OR last_name LIKE «%’ . $this->fullName . ‘%»‘
);
на вот это
$query->andFilterWhere([‘like’, ‘concat_ws(» «,name,surname,patronymic)’, $this->fullName]);
Да, вариант который приведен в статье, работает только если искать либо first_name, либо last_name, но не оба вместе. Ваш вариант лучше. Только там что-то с кавычками в разделителе concat_ws. Нужно заменить на обычные » «
И у меня что-то с кавычками…
Кто сможет подсказать как сделать фильтр по действительно вычисляемому полю, к примеру есть записи в 2-х таблицах связь один ко многим. Так вот множество записей обрабатываются на пыхе, (обработка сложная поэтому на sql реализовать можно но получится страницы 2 формата а4) так вот вся эта обработка засунута в геттер модели, и возвращает число, вопрос как фильтровать по этому полю?
Не прибегая к ArrayDataProvider или к фильтрации на фронте через дататаблю…
Третий вариант — добавить поле для хранения нужного значения и обновлять его при изменении зависимости.
Возможно не сюда, но все же….
как через GridView выводить динамическое количество колонок, а именно количество колонок будет будет зависеть от фиксированные данные к примеру 5 колонок + по колонке на каждый день периода (2016.07.25 — 2016.08.31 = 6 колонок).
Данные хранятся в свойстве модели в массиве.
Собственно вопрос можно ли как нибудь развернуть это свойство в стандартном GridView, если да то как?
1. Варианты которые я нашел, отрисовать табличку руками.
2. Написать свой GridView на основе существующего (со своим пасьянсом и куртизанками).
3. Отказаться от GridView и забирать выхлоп в dataTable (все ровно она потом обрабатывает табличку, вариант плох в виду плохого знания JS)
4. Найти другой GridView…
4 сегодня вечерком займусь есть 2 наметки…
// ‘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, то можно получить фильтр с выпадающим списком
Можно упростить до: ‘filter’ => ArrayHelper::map(Category2::find()->all(), ‘id’, ‘name’),
Мне кажется, или подобный код уязвим для SQL-инъекций ?
$query->andWhere(‘first_name LIKE «%’ . $this->fullName . ‘%» ‘ .
‘OR last_name LIKE «%’ . $this->fullName . ‘%»‘
);
Отличная статья!
Подскажите, пожалуйста, как сделать фильтр, который будет проверять наличие или отсутствие данных в определенном поле таблицы?
Т.е. именно фильтровать записи по условиям:
– поле field пустое (null, », etc)
– поле field НЕ пустое
Как такое можно реализовать в методе search?
Какие условия добавить в запрос?
Во втором варианте в моделе Person нужно было добавить еще переменную:
public $countryName
и в виджете GridView атрибут и значение:
‘contryName’ => [
‘attribute’ => ‘countryName’,
‘value’ => function($data) {
return $data->country->name;
},
Спасибо, что написали это здесь! Это решило проблему с ошибкой attribute read-only.
Ребят я только одного не понимаю, как вы передаете $query вначале, потом его правите и отдаете ActiveDataProvider… Может все таки нужно передавать ссылкой?
$dataProvider = new ActiveDataProvider([
‘query’ => $query,
]);
‘query’ => &$query,
$query это экземпляр класса ActiveQuery, а экземпляры класса всегда передаются по ссылке.