Сортировка и фильтр 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, а экземпляры класса всегда передаются по ссылке.