GridView и DetailView в Yii2: рабочие приёмы для таблиц
Как настроить колонки, фильтры и связанные данные в GridView без N+1-запросов и как показать одну запись через DetailView. Разбор частых ошибок с ActiveDataProvider.

Вы выводите список заказов через GridView, вешаете в колонку имя клиента из связанной таблицы — и на странице из 20 строк профайлер показывает 21 запрос к базе. При переходе на вторую страницу сортировка по этой же колонке падает с ошибкой. Всё это лечится настройкой провайдера и колонок, а не переписыванием на голый HTML. Разберём, как готовить GridView и DetailView в Yii2 (актуально для 2.0.x), чтобы таблицы работали быстро, а фильтры и связи не ломались.
Материал для тех, кто уже поднял приложение на Yii2, сгенерировал CRUD через Gii и упёрся в реальные данные: связанные модели, фильтры, форматирование. Один угол — практика вывода списков и карточек. Кастомные виджеты, DataColumn с нуля и Kartik-расширения оставляем за скобками.
Минимальный GridView и что в нём не так по умолчанию
Gii генерирует примерно такой код представления:
<?php
use yii\grid\GridView;
echo GridView::widget([
'dataProvider' => $dataProvider,
'filterModel' => $searchModel,
'columns' => [
'id',
'customer_id',
'total',
'created_at',
['class' => 'yii\grid\ActionColumn'],
],
]);
Работает, но выводит customer_id как число и created_at как unix-таймстамп. Первое, что нужно сделать, — заменить сырые атрибуты на осмысленные колонки. Дальше начинается то, ради чего вы читаете эту статью: связанные данные и фильтры.
DataColumn: формат, значение, заголовок
Каждый элемент массива columns — это yii\grid\DataColumn, если не указан другой класс. Ключевые свойства:
attribute— атрибут модели, по которому идёт сортировка и фильтр;value— замыканиеfunction ($model) { ... }или строка-путь к данным для отображения;format— как форматировать:'text','html','datetime',['date', 'php:d.m.Y'],'currency';label— заголовок колонки, если не берётся изattributeLabels()модели.
Форматирование через format использует компонент formatter приложения — там же задаются локаль, часовой пояс и валюта. Не форматируйте дату вручную в замыкании, если это делает форматтер: часовой пояс проще один раз настроить в конфиге.
['class' => 'yii\grid\GridView'];
'columns' => [
'id',
[
'attribute' => 'customer.name', // данные из связи
'label' => 'Клиент',
],
[
'attribute' => 'total',
'format' => 'currency', // символ валюты из formatter
],
[
'attribute' => 'created_at',
'format' => ['datetime', 'php:d.m.Y H:i'],
],
],
Связанные данные без N+1
Запись 'attribute' => 'customer.name' заставляет Yii дёргать связь getCustomer() для каждой строки. На странице в 20 строк это 20 отдельных SELECT к таблице клиентов плюс основной запрос — классический N+1.
Лечится на уровне провайдера: жадная загрузка через with() в запросе, который вы отдаёте в ActiveDataProvider.
use yii\data\ActiveDataProvider;
$query = Order::find()->with('customer'); // один JOIN-подобный запрос по связи
$dataProvider = new ActiveDataProvider([
'query' => $query,
'pagination' => ['pageSize' => 20],
]);
После этого связанные клиенты грузятся одним дополнительным запросом на всю страницу, а не по одному на строку. Проверить легко: в Debug Toolbar на вкладке Database счётчик запросов перестаёт расти вместе с числом строк.
Правило простое: если в колонке есть точка (customer.name,customer.city.title), убедитесь, что соответствующая связь попала вwith(). Точка в атрибуте — сигнал проверить провайдер.
Сортировка и фильтр по связанному полю
Сортировка по customer.name из коробки не заработает: провайдер не знает, что это за колонка в SQL. Нужно объявить её в SearchModel и настроить sort и filter. В сгенерированном Gii поисковом классе добавьте виртуальный атрибут и JOIN:
public $customerName; // виртуальный атрибут для фильтра
public function rules()
{
return [
[['customerName'], 'safe'],
// ...остальные правила
];
}
public function search($params)
{
$query = Order::find();
$dataProvider = new ActiveDataProvider(['query' => $query]);
// связываем виртуальный атрибут с реальной колонкой связи
$dataProvider->sort->attributes['customerName'] = [
'asc' => ['customer.name' => SORT_ASC],
'desc' => ['customer.name' => SORT_DESC],
];
$this->load($params);
if (!$this->validate()) {
return $dataProvider;
}
$query->joinWith('customer'); // JOIN для сортировки и фильтра
$query->andFilterWhere(['like', 'customer.name', $this->customerName]);
return $dataProvider;
}
В представлении колонка ссылается на виртуальный атрибут:
[
'attribute' => 'customerName',
'value' => 'customer.name',
'label' => 'Клиент',
],
Разница между with() и joinWith(): первый догружает связь отдельным запросом, второй добавляет JOIN и позволяет фильтровать и сортировать по полям связанной таблицы в SQL. Для сортировки по связи нужен именно joinWith().
DetailView: одна запись, вертикальная таблица
Там, где GridView показывает список, DetailView показывает одну модель построчно — атрибут слева, значение справа. Типичное применение — страница просмотра записи (view).
<?php
use yii\widgets\DetailView;
echo DetailView::widget([
'model' => $model,
'attributes' => [
'id',
'customer.name:text:Клиент', // атрибут:формат:метка
'total:currency',
'created_at:datetime',
[
'attribute' => 'status',
'value' => function ($model) {
return $model->status == 1 ? 'Оплачен' : 'Ожидает';
},
],
],
]);
Короткий синтаксис атрибут:формат:метка экономит строки, когда не нужна логика. Как только нужно замыкание — переходите на массив.
У DetailView нет пагинации и провайдера: он берёт одну модель. Связь customer.name тут тоже вызовет отдельный запрос, но это один запрос на одну карточку — проблемы N+1 нет, оптимизировать нечего.
Сравнение: когда что
| Параметр | GridView | DetailView |
|---|---|---|
| Что показывает | список записей | одну запись |
| Источник данных | dataProvider | model |
| Фильтры и сортировка | да, через filterModel/sort | нет |
| Пагинация | да | нет |
| Риск N+1 | высокий на связях | отсутствует |
| Namespace | yii\grid | yii\widgets |
Что ломается чаще всего
- Setting unknown property — опечатка в свойстве колонки (например,
lableвместоlabel) или свойство не того класса. Yii бросает исключение с именем свойства — читайте текст ошибки, там указано, чего он не знает. - Getting unknown property: ...::customer — связь не объявлена в модели или названа иначе. Проверьте метод
getCustomer()в модели: имя связи в атрибуте должно совпадать с именем метода без префиксаgetи в нижнем camelCase. - Сортировка по связи молча не работает — забыли
joinWith()или не объявили атрибут в$dataProvider->sort->attributes. Клик по заголовку меняет URL, но порядок строк остаётся. - Число запросов растёт со строками — нет
with(). Смотрите Debug Toolbar, вкладка Database. - HTML в ячейке экранируется — по умолчанию
format => 'text'экранирует теги. Для готового HTML укажите'format' => 'html', но только для данных, которым доверяете, иначе получите XSS.
Что настроить дальше
Начните с включения Debug Toolbar (yii2-debug) в dev-окружении — без него N+1 не видно. Дальше вынесите повторяющуюся логику колонок в статические методы модели, чтобы формат клиента был одинаковым во всех гридах. Если фильтров и оформления не хватает — смотрите в сторону расширения kartik-v/yii2-grid, но сперва выжмите штатный GridView: большинство задач он закрывает. Мерить стоит две вещи — число SQL-запросов на страницу списка и время рендера представления; обе видны в профайлере.
Конспект и шпаргалка: Лекция, глава учебника или запись — в конспект, карточки и таблицы
Спросить за 12 ₽Источники: Yii2 Guide — Data Widgets (GridView, DetailView), Yii2 Guide — Active Record: Eager and Lazy Loading
Частые вопросы
У меня Yii2 версии 2.0.x, а примеры сортировки по связи не работают — почему?
В чём разница между with() и joinWith()?
GridView выводит HTML-теги как текст, а мне нужно их отрисовать. Как быть?
Можно ли использовать DetailView для списка записей?
Профайлер показывает лишний запрос даже после with() — это нормально?
Почему появляется ошибка Getting unknown property при обращении к связи в колонке?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.