Сервис на Yii2: Модуль администрирования и GridView
После добавления личного кабинета пользователя и редактора профиля в прошлых частях сегодня начнём делать модуль администратора. А именно, создадим модуль и CRUD для управления пользователями с помощью генератора Gii и доработаем его вид и маршрутизацию под свои требования.
Часть 1: Установка и настройка приложения
Часть 2: Настройка IDE и модульная структура
Часть 3: Перенос пользователей в БД
Часть 4: Доработка шаблона и локализация
Часть 5: Просмотр и редактирование профиля
Сейчас мы поместим контроллеры по управлению в отдельный модуль, а не в тот, к которому они относятся. С архитектурной точки зрения это не очень хорошо. В одной из последующих частей мы сделаем более осознанную модульную структуру, а пока оставим всё так.
Сгенерируем модуль admin
также, как мы собирали модуль user
:
и подключим его в конфигурации приложения config/common.php
:
'modules' => [ 'admin' => [ 'class' => 'app\modules\admin\Module', ], 'main' => [ 'class' => 'app\modules\main\Module', ], 'user' => [ 'class' => 'app\modules\user\Module', ], ],
Создадим папку models и сгенерируем CRUD. Эта аббревиатура обозначает комплект действий Create, Read (Index и View), Update и Delete. Не забудьте поставить галочку использования с i18n, чтобы представления сгенерировались с интернационализацией по Yii::t
:
В итоге мы можем зайти по адресу /admin/users/index
и увидеть список пользователей сайта:
Интернационализация
В свежесгенерированном коде представлений меняем надписи по умолчанию на свои, например вместо:
$this->title = $model->id; $this->params['breadcrumbs'][] = ['label' => Yii::t('app', 'Users'), 'url' => ['index']]; $this->params['breadcrumbs'][] = $this->title;
вписываем свои идентификаторы фраз:
$this->title = $model->username; $this->params['breadcrumbs'][] = ['label' => Yii::t('app', 'ADMIN_USERS'), 'url' => ['index']]; $this->params['breadcrumbs'][] = $this->title;
и при этом не забываем добавлять их переводы в наши языковые файлы в папке messages
.
Навигация
Для нового контроллера добавим пункты в меню и немного скомпонуем вместе ссылки на профиль и на действие выхода в выпадающие списки:
echo Nav::widget([ 'options' => ['class' => 'navbar-nav navbar-right'], 'activateParents' => true, 'items' => array_filter([ ['label' => Yii::t('app', 'NAV_HOME'), 'url' => ['/main/default/index']], ['label' => Yii::t('app', 'NAW_CONTACT'), 'url' => ['/main/contact/index']], Yii::$app->user->isGuest ? ['label' => Yii::t('app', 'NAV_SIGNUP'), 'url' => ['/user/default/signup']] : false, Yii::$app->user->isGuest ? ['label' => Yii::t('app', 'NAV_LOGIN'), 'url' => ['/user/default/login']] : false, !Yii::$app->user->isGuest ? ['label' => Yii::t('app', 'NAV_ADMIN'), 'items' => [ ['label' => Yii::t('app', 'NAV_ADMIN'), 'url' => ['/admin/default/index']], ['label' => Yii::t('app', 'ADMIN_USERS'), 'url' => ['/admin/users/index']], ]] : false, !Yii::$app->user->isGuest ? ['label' => Yii::t('app', 'NAV_PROFILE'), 'items' => [ ['label' => Yii::t('app', 'NAV_PROFILE'), 'url' => ['/user/profile/index']], ['label' => Yii::t('app', 'NAV_LOGOUT'), 'url' => ['/user/default/logout'], 'linkOptions' => ['data-method' => 'post']] ]] : false, ]), ]);
Аналогично добавим ссылку на главную страницу панели управления в хлебные крошки каждого представления как в view.php
:
$this->title = $model->username; $this->params['breadcrumbs'][] = ['label' => Yii::t('app', 'ADMIN'), 'url' => ['default/index']]; $this->params['breadcrumbs'][] = ['label' => Yii::t('app', 'ADMIN_USERS'), 'url' => ['index']]; $this->params['breadcrumbs'][] = $this->title;
В представлении modules/admin/views/default/index.php
удалим текстовую заглушку и тоже вставим ссылку:
<?php use yii\helpers\Html; /* @var $this yii\web\View */ /* @var $model \app\modules\user\models\User */ $this->title = Yii::t('app', 'ADMIN'); $this->params['breadcrumbs'][] = $this->title; <div class="admin-default-index"> <h1> Html::encode($this->title) </h1> <p> Html::a(Yii::t('app', 'ADMIN_USERS'), ['users/index'], ['class' => 'btn btn-primary']) </p> </div>
И постепенно перейдём к функционалу.
Модель и сценарии
Чем работа с моделью пользователя у администратора в панели управления отличается от управления ею же посетителем в своём личном кабинете? Только тем, что администратор при добавлении пользователя должен сам указать пароль. И при редактировании он этот пароль может изменить.
Это нужно только в модуле администрирования, поэтому не будем засорять лишним функционалом исходную модель User
из модуля user
. Вместо этого отнаследуемся от оригинальной модели и добавим пару сценариев:
namespace app\modules\admin\models; use yii\helpers\ArrayHelper; use Yii; class User extends \app\modules\user\models\User { const SCENARIO_ADMIN_CREATE = 'adminCreate'; const SCENARIO_ADMIN_UPDATE = 'adminUpdate'; public $newPassword; public $newPasswordRepeat; public function rules() { return ArrayHelper::merge(parent::rules(), [ [['newPassword', 'newPasswordRepeat'], 'required', 'on' => self::SCENARIO_ADMIN_CREATE], ['newPassword', 'string', 'min' => 6], ['newPasswordRepeat', 'compare', 'compareAttribute' => 'newPassword'], ]); } public function scenarios() { $scenarios = parent::scenarios(); $scenarios[self::SCENARIO_ADMIN_CREATE] = ['username', 'email', 'status', 'newPassword', 'newPasswordRepeat']; $scenarios[self::SCENARIO_ADMIN_UPDATE] = ['username', 'email', 'status', 'newPassword', 'newPasswordRepeat']; return $scenarios; } public function attributeLabels() { return ArrayHelper::merge(parent::attributeLabels(), [ 'newPassword' => Yii::t('app', 'USER_NEW_PASSWORD'), 'newPasswordRepeat' => Yii::t('app', 'USER_REPEAT_PASSWORD'), ]); } public function beforeSave($insert) { if (parent::beforeSave($insert)) { if (!empty($this->newPassword)) { $this->setPassword($this->newPassword); } return true; } return false; } }
Теперь в модуле admin
вместо оригинала:
use app\modules\user\models\User;
будем использовать эту модель:
use app\modules\admin\models\User;
и устанавливать сценарии в действиях контроллера:
namespace app\modules\admin\controllers; use Yii; use app\modules\admin\models\User; use app\modules\admin\models\UserSearch; use yii\web\Controller; use yii\web\NotFoundHttpException; use yii\filters\VerbFilter; /** * UsersController implements the CRUD actions for User model. */ class UsersController extends Controller { ... public function actionCreate() { $model = new User(); $model->scenario = User::SCENARIO_ADMIN_CREATE; $model->status = User::STATUS_ACTIVE; if ($model->load(Yii::$app->request->post()) && $model->save()) { return $this->redirect(['view', 'id' => $model->id]); } else { return $this->render('create', [ 'model' => $model, ]); } } public function actionUpdate($id) { $model = $this->findModel($id); $model->scenario = User::SCENARIO_ADMIN_UPDATE; if ($model->load(Yii::$app->request->post()) && $model->save()) { return $this->redirect(['view', 'id' => $model->id]); } else { return $this->render('update', [ 'model' => $model, ]); } } }
Отображение пользователя
В modules/admin/views/users/view.php
выведем статус пользователя названием, а не числом. Достаточно просто передать значение в value
нужного атрибута DetailView
:
<?= DetailView::widget([ 'model' => $model, 'attributes' => [ 'id', 'username', 'email:email', 'created_at:datetime', 'updated_at:datetime', [ 'attribute' => 'status', 'value' => $model->getStatusName(), ], ], ])
В modules/admin/views/users/update.php
мы недавно поменяли только хлебные крошки:
<?php use yii\helpers\Html; /* @var $this yii\web\View */ /* @var $model app\modules\admin\models\User */ $this->title = $model->username; $this->params['breadcrumbs'][] = ['label' => Yii::t('app', 'ADMIN'), 'url' => ['default/index']]; $this->params['breadcrumbs'][] = ['label' => Yii::t('app', 'ADMIN_USERS'), 'url' => ['index']]; $this->params['breadcrumbs'][] = ['label' => $model->username, 'url' => ['view', 'id' => $model->id]]; $this->params['breadcrumbs'][] = Yii::t('app', 'TITLE_UPDATE'); <div class="user-update"> <h1> Html::encode($this->title) </h1> $this->render('_form', [ 'model' => $model, ]) </div>
В modules/admin/views/users/_form.php
добавляем поля для нового пароля и выпадающий список с вариантами для статуса:
<?php use app\modules\admin\models\User; use yii\helpers\Html; use yii\widgets\ActiveForm; /* @var $this yii\web\View */ /* @var $model app\modules\admin\models\User */ /* @var $form yii\widgets\ActiveForm */ <div class="user-form"> $form = ActiveForm::begin(); $form->field($model, 'username')->textInput(['maxlength' => true]) $form->field($model, 'email')->textInput(['maxlength' => true]) $form->field($model, 'newPassword')->passwordInput(['maxlength' => true]) $form->field($model, 'newPasswordRepeat')->passwordInput(['maxlength' => true]) $form->field($model, 'status')->dropDownList(User::getStatusesArray()) <div class="form-group"> Html::submitButton( $model->isNewRecord ? Yii::t('app', 'BUTTON_CREATE') : Yii::t('app', 'BUTTON_CREATE'), ['class' => $model->isNewRecord ? 'btn btn-success' : 'btn btn-primary'] ) </div> ActiveForm::end(); </div>
Вот, собственно, и всё с отображением и редактированием пользователя. Перейдём к выводу списка пользователей в гриде GridView
.
Поисковая модель
По умолчанию Gii генерирует поисковую модель UserSearch наследуемой от User, чтобы использовать список атрибутов и значения attributeLabels из исходной модели. Удобно. Но! При этом помимо этих двух вещей в наш класс наследуется ешё стопятьсот методов, событий, связей, поведений и прочего, что есть в модели User
, но вообще нам не нужно. Да и приходится перекрывать валидацию и сбрасывать сценарии. Да и, к тому же, в поисковой модели могут быть вообще другие поля.
Логичнее для поиска использовать отдельную модель, никак не связанную с основной. Для этого уберём наследование от ActiveRecord-модели User
, удалим ненужный метод scenarios
, пропишем явно все поля, нужные для поиска, и продублируем метод attributeLabels
:
namespace app\modules\admin\models; use Yii; use yii\base\Model; use yii\data\ActiveDataProvider; class UserSearch extends Model { public $id; public $username; public $email; public $status; public function rules() { return [ [['id', 'status'], 'integer'], [['username', 'email'], 'safe'], ]; } public function attributeLabels() { return [ 'id' => 'ID', 'created_at' => Yii::t('app', 'USER_CREATED'), 'updated_at' => Yii::t('app', 'USER_UPDATED'), 'username' => Yii::t('app', 'USER_USERNAME'), 'email' => Yii::t('app', 'USER_EMAIL'), 'status' => Yii::t('app', 'USER_STATUS'), ]; } public function search($params) { $query = User::find(); $dataProvider = new ActiveDataProvider([ 'query' => $query, ]); $this->load($params); if (!$this->validate()) { $query->where('0=1'); return $dataProvider; } $query->andFilterWhere([ 'id' => $this->id, 'status' => $this->status, ]); $query ->andFilterWhere(['like', 'username', $this->username]) ->andFilterWhere(['like', 'email', $this->email]); return $dataProvider; } }
Эта модель для формы поиска стала значительно легче, так как полностью избавилась от всего наследия ActiveRecord
.
Теперь перейдём к улучшению интерфейса вывода.
Сортировка по умолчанию
По умолчанию сортировка при запросе записей из базы идёт хаотично в том порядке, в котором они в саму базу записывались либо обновлялись.
Добавим в поисковую модель сортировку по убыванию поля id
, чтобы новые пользователи были сверху:
class UserSearch extends Model { ... public function search($params) { $query = User::find(); $dataProvider = new ActiveDataProvider([ 'query' => $query, 'sort' => [ 'defaultOrder' => ['id' => SORT_DESC], ], ]); ... } }
Кнопки управления
Сейчас в таблице списка пользователей GridView
в modules/admin/views/users/index.php
последней колонкой выводятся кнопки управления записью:
['class' => 'yii\grid\ActionColumn'],
Но на большом экране эта колонка слишком широкая, а на маленьком кнопки не вмещаются и располагаются вертикально:
Мы можем изменить стиль ячейки, запретив перенос строк, выровняв по центру, увеличив интервалы между иконками и указав максимально допустимую ширину колонки:
[ 'class' => 'yii\grid\ActionColumn', 'contentOptions' => ['style' => 'white-space: nowrap; text-align: center; letter-spacing: 0.1em; max-width: 7em;'], ],
Для удобства для переопределения этих правил мы могли бы создать отдельный класс колонки:
namespace app\components\grid; use yii\grid\ActionColumn; class ActionColumn extends ActionColumn { public $contentOptions = [ 'style' => 'white-space: nowrap; text-align: center; letter-spacing: 0.1em; max-width: 7em;', ]; }
и использовать его вместо стандартного:
use app\components\grid\ActionColumn; ... ['class' => ActionColumn::className()],
Но хранить стили в самих компонентах мы не будем, так как будет неудобно исправлять внешний вид и менять темы оформления. Для стилей больше предназначены CSS-файлы. Поэтому для нашей колонки укажем только CSS-класс ячейки:
namespace app\components\grid; class ActionColumn extends \yii\grid\ActionColumn { public $contentOptions = [ 'class' => 'action-column', ]; }
А сами стили для этого класса добавим в web/css/site.css
:
.grid-view td.action-column { white-space: nowrap; text-align: center; letter-spacing: 0.1em; max-width: 7em; }
И, как уже говорили выше, подключим эту колонку:
use app\components\grid\ActionColumn; ... ['class' => ActionColumn::className()],
Так мы избавились от одного из неудобств. Идём дальше.
Вывод статуса
Сейчас колонка статуса выводится как есть:
<?= GridView::widget([ 'dataProvider' => $dataProvider, 'filterModel' => $searchModel, 'columns' => [ 'id', 'created_at:datetime', 'username', 'email:email', 'status', ['class' => ActionColumn::className()], ], ]);
Но в самой модели статус хранится числом и простое текстовое поле поиска рассчитано на ввода числа. Это неудобно.
Мы можем для фильтра и значения использовать наши методы из модели User
:
<?= GridView::widget([ 'dataProvider' => $dataProvider, 'filterModel' => $searchModel, 'columns' => [ 'id', 'created_at:datetime', 'username', 'email:email', [ 'filter' => User::getStatusesArray(), 'attribute' => 'status', 'value' => function ($model, $key, $index, $column) { /** @var User $model */ return $model->getStatusName()) } ], ['class' => ActionColumn::className()], ], ]);
Но здесь можно схитрить. На одном из вебинаров мы говорили про геттеры, а наш метод $model->getStatusName()
тоже представляет собой геттер, который можно вызывать как поле $model->statusName
.
Заглянем в класс колонки DataColumn
и посмотрим, как он вычисляет значение для каждой ячейки:
namespace yii\grid; class DataColumn extends Column { ... public function getDataCellValue($model, $key, $index) { if ($this->value !== null) { if (is_string($this->value)) { return ArrayHelper::getValue($model, $this->value); } else { return call_user_func($this->value, $model, $key, $index, $this); } } elseif ($this->attribute !== null) { return ArrayHelper::getValue($model, $this->attribute); } return null; } ... }
Видим, что если передано что-то в value
и если это строка, то это рассматривается как имя атрибута для ArrayHelper::getValue
. А если там не строка, а что-то ещё, то он пытается вызвать это переданное как функцию.
Поэтому вместо использования анонимной функции мы можем указать для value
имя любого поля. Вот и передадим имя нашего псевдополя statusName
:
<?= GridView::widget([ 'dataProvider' => $dataProvider, 'filterModel' => $searchModel, 'columns' => [ 'id', 'created_at:datetime', 'username', 'email:email', [ 'filter' => User::getStatusesArray(), 'attribute' => 'status', 'value' => 'statusName', ], ['class' => ActionColumn::className()], ], ]);
и вместо чисел в колонке будет выводится название статуса, а фильтр вместо простого поля ввода станет выпадающим списком.
Всё получилось. Можно пока отдохнуть.
А можно ли вывод статусов как-нибудь улучшить? А давайте выведем разные статусы в плашках разного цвета? Можно внутри той анонимной функции названия статусов обернуть название в тег <span>
с различным цветом. Но среди стилей CSS-фреймворка Twitter Bootstrap уже имеются классы label
для этих целей и мы можем вывести, например, так:
<span class="label label-success">Активен</span> <span class="label label-warning">Ожидает подтверждения</span> <span class="label label-default">Заблокирован</span>
Давайте обернём результат в <span>
и укажем колонке статуса формат raw
(чтобы отключить обработку вернувшихся от нас тегов функцией htmlspecialchars
):
<?= GridView::widget([ 'dataProvider' => $dataProvider, 'filterModel' => $searchModel, 'columns' => [ 'id', 'created_at:datetime', 'username', 'email:email', [ 'filter' => User::getStatusesArray(), 'attribute' => 'status', 'format' => 'raw', 'value' => function ($model, $key, $index, $column) { /** @var User $model */ /** @var \yii\grid\DataColumn $column */ $value = $model->{$column->attribute}; switch ($value) { case User::STATUS_ACTIVE: $class = 'success'; break; case User::STATUS_WAIT: $class = 'warning'; break; case User::STATUS_BLOCKED: default: $class = 'default'; }; $html = Html::tag('span', Html::encode($model->getStatusName()), ['class' => 'label label-' . $class]); return $value === null ? $column->grid->emptyCell : $html; } ], ['class' => ActionColumn::className()], ], ]);
И получим красивое разноцветное отображение статуса:
Можно этот функционал вынести в класс-колонку UserStatusColumn
, отнаследовавшись от yii\grid\DataColumn
(который используется по умолчанию для вывода колонки) и переопределив метод renderDataCellContent
, отвечающий за вывод самой ячейки:
namespace app\modules\admin\components; use app\modules\admin\models\User; use yii\grid\DataColumn; use yii\helpers\Html; class UserStatusColumn extends DataColumn { protected function renderDataCellContent($model, $key, $index) { /** @var User $model */ $value = $this->getDataCellValue($model, $key, $index); switch ($value) { case User::STATUS_ACTIVE: $class = 'success'; break; case User::STATUS_WAIT: $class = 'warning'; break; case User::STATUS_BLOCKED: default: $class = 'default'; }; $html = Html::tag('span', Html::encode($model->getStatusName()), ['class' => 'label label-' . $class]); return $value === null ? $this->grid->emptyCell : $html; } }
и использовать его:
<?= GridView::widget([ 'dataProvider' => $dataProvider, 'filterModel' => $searchModel, 'columns' => [ 'id', 'created_at:datetime', 'username', 'email:email', [ 'class' => UserStatusColumn::className(), 'filter' => User::getStatusesArray(), 'attribute' => 'status', ], ['class' => ActionColumn::className()], ], ]);
Но статусы и подобные поля нам придётся выводить часто в разных таблицах, а это нам хотелось бы делать единообразно. В этом специфическом компоненте вроде бы атрибут передан и его можно менять, но сами значения и имя метода getStatusName
оказались жёстко вписаны. Надо бы их вынести за пределы методов класса.
Поэтому пойдём дальше и сделаем универсальную колонку для статусов и прочих полей с наборами вариантов. Для этого полностью отвяжем компонент от модели пользователя. Для этого помимо основного поля attribute
добавим ещё одно настраиваемое поле name
типа callable
для получения наименования статуса:
namespace app\components\grid; use yii\grid\DataColumn; use yii\helpers\ArrayHelper; use yii\helpers\Html; class SetColumn extends DataColumn { /** * @var callable */ public $name; /** * Array of status classes * ``` * [ * User::STATUS_ACTIVE => 'success', * User::STATUS_WAIT => 'warning', * User::STATUS_BLOCKED => 'default', * ] * ``` * @var array */ public $cssCLasses = []; protected function renderDataCellContent($model, $key, $index) { $value = $this->getDataCellValue($model, $key, $index); $name = $this->getStatusName($model, $key, $index, $value); $class = ArrayHelper::getValue($this->cssCLasses, $value, 'default'); $html = Html::tag('span', Html::encode($name), ['class' => 'label label-' . $class]); return $value === null ? $this->grid->emptyCell : $html; } /** * @param mixed $model * @param mixed $key * @param integer $index * @param mixed $value * @return string */ private function getStatusName($model, $key, $index, $value) { if ($this->name !== null) { if (is_string($this->name)) { $name = ArrayHelper::getValue($model, $this->name); } else { $name = call_user_func($this->name, $model, $key, $index, $this); } } else { $name = null; } return $name === null ? $value : $name; } }
в которое передадим имя свойства (нашего геттера) для получения из него наименования текущего статуса
<?= GridView::widget([ 'dataProvider' => $dataProvider, 'filterModel' => $searchModel, 'columns' => [ 'id', 'created_at:datetime', 'username', 'email:email', [ 'class' => SetColumn::className(), 'filter' => User::getStatusesArray(), 'attribute' => 'status', 'name' => 'statusName', 'cssCLasses' => [ User::STATUS_ACTIVE => 'success', User::STATUS_WAIT => 'warning', User::STATUS_BLOCKED => 'default', ], ], ['class' => ActionColumn::className()], ], ]);
Такую колонку SetColumn
теперь можно использовать для вывода любых списков.
Поиск по дате
Теперь модифицируем UserSearch
. А именно уберём поиск по паролю и хешам и добавим реализацию поиска по двум значением даты вместо одного.
Даты создания и обновления записи у нас хранятся числами, чтобы не было мороки с часовыми поясами в базе. Так что от стандартного поискового поля у колонки с created_at
толку никакого. Yii2 не позволяет в поисковое поле вбивать операторы больше-меньше, да и мы сами не будем вбивать дату числом в UnixTime. Так что нам нужно для удобства осуществить поиск от одной даты до другой.
Добавим для этого в поисковую модель два поля $date_from
и $date_to
:
namespace app\modules\admin\models; use Yii; use yii\base\Model; use yii\data\ActiveDataProvider; /** * UserSearch represents the model behind the search form about `app\modules\admin\models\User`. */ class UserSearch extends Model { public $id; public $username; public $email; public $status; public $date_from; public $date_to; public function rules() { return [ [['id', 'status'], 'integer'], [['username', 'email'], 'safe'], [['date_from', 'date_to'], 'date', 'format' => 'php:Y-m-d'], ]; } ... }
Нас устроит поиск только по дням (без часов и минут), но в базу записываются значения UnixTime с точностью до секунды.
Мы бы могли скомпоновать нижний и верхний пределы времени с секундами. Если мы ищем в интервале от одного дня до другого, то при работе с секундами нужно искать от 0 часов 0 минут первого дня до 23 часов 59 минут 59 секунд другого:
$dateFrom = $this->date_from ? $this->date_from . ' 00:00:00' : null; $dateTo = $this->date_to ? $this->date_to . ' 23:59:59' : null;
и воспользоваться конвертером СУБД MySQL для поиска пользователей с датами регистрации из этого интервала:
$query ->andFilterWhere(['>=', new Expression('CONVERT(DATETIME, [[created_at]], 20)'), $dateFrom]) ->andFilterWhere(['<=', new Expression('CONVERT(DATETIME, [[created_at]], 20)'), $dateTo]);
или отброисть секунды и вычислять и сравнивать даты только в формате yyyy-mm-dd
без секунд.
Но это опасные пути, так как:
- Конвертирование в разных SQL-продуктах выполняется разными функциями. Это ломает кроссплатформенность в плане поддержки различных баз.
- Поиск по вычисляемому полю загружает процессор вычислениями и не использует индексы. Следовательно, сильно падает скорость работы.
Вместо этого намного эффективнее преобразовать наши даты в числа функцией strtotime
и воспользоваться простыми неравенствами без вычислений на стороне SQL:
$query ->andFilterWhere(['>=', 'created_at', $this->date_from ? strtotime($this->date_from . ' 00:00:00') : null]) ->andFilterWhere(['<=', 'created_at', $this->date_to ? strtotime($this->date_to . ' 23:59:59') : null]);
Этот вариант и оставим.
В итоге, после небольших манипуляций наша поисковая модель станет такой:
namespace app\modules\admin\models; use Yii; use yii\base\Model; use yii\data\ActiveDataProvider; /** * UserSearch represents the model behind the search form about `app\modules\admin\models\User`. */ class UserSearch extends Model { public $id; public $username; public $email; public $status; public $date_from; public $date_to; public function rules() { return [ [['id', 'status'], 'integer'], [['username', 'email'], 'safe'], [['date_from', 'date_to'], 'date', 'format' => 'php:Y-m-d'], ]; } public function attributeLabels() { return [ 'id' => 'ID', 'created_at' => Yii::t('app', 'USER_CREATED'), 'updated_at' => Yii::t('app', 'USER_UPDATED'), 'username' => Yii::t('app', 'USER_USERNAME'), 'email' => Yii::t('app', 'USER_EMAIL'), 'status' => Yii::t('app', 'USER_STATUS'), 'date_from' => Yii::t('app', 'USER_DATE_FROM'), 'date_to' => Yii::t('app', 'USER_DATE_TO'), ]; } /** * Creates data provider instance with search query applied * * @param array $params * * @return ActiveDataProvider */ public function search($params) { $query = User::find(); $dataProvider = new ActiveDataProvider([ 'query' => $query, 'sort' => [ 'defaultOrder' => ['id' => SORT_DESC], ] ]); $this->load($params); if (!$this->validate()) { $query->where('0=1'); return $dataProvider; } $query->andFilterWhere([ 'id' => $this->id, 'status' => $this->status, ]); $query ->andFilterWhere(['like', 'username', $this->username]) ->andFilterWhere(['like', 'email', $this->email]) ->andFilterWhere(['>=', 'created_at', $this->date_from ? strtotime($this->date_from . ' 00:00:00') : null]) ->andFilterWhere(['<=', 'created_at', $this->date_to ? strtotime($this->date_to . ' 23:59:59') : null]); return $dataProvider; } }
Теперь нужно как-нибудь вывести форму поиска в представлении с GridView
.
Можно поместить эти два поля в свежесгенерированную Gii форму в представлении _search.php
, но было бы интереснее вставить фильтр в саму шапку таблицы к другим фильтрам.
Обычное поле filter
для двух дат не подойдёт. Сгенерируем свой HTML-код фильтра и передадим в filter
:
<?= GridView::widget([ 'dataProvider' => $dataProvider, 'filterModel' => $searchModel, 'columns' => [ 'id', [ 'filter' => Html::tag( 'div', Html::tag('div', Html::activeTextInput($searchModel, 'date_from', ['class' => 'form-control']), ['class' => 'col-xs-6']) . Html::tag('div', Html::activeTextInput($searchModel, 'date_to', ['class' => 'form-control']), ['class' => 'col-xs-6']), ['class' => 'row'] ), 'attribute' => 'created_at', 'format' => 'datetime', ], 'username', 'email:email', ... ['class' => ActionColumn::className()], ], ]);
Да, так тоже можно :)
Вбивать даты приходится вручную, но всё работает.
Сделаем удобнее. В закромах Bootstrap имеется интересный плагин DatePicker:
Попробуем его прикрутить.
Для Yii2 народными умельцами для него уже сделано несколько виджетов-обёрток. Особого внимания заслуживает набор продвинутых виджетов полей ввода от Krajee. Из всего набора нам лучше всего подойдёт вариант Date Range Markup с двойным полем. Но для работы в режиме двойного поля DatePicker::TYPE_RANGE
ему требуется ещё и компонент yii2-field-range
. Так что установим в свой проект оба пакета:
composer require kartik-v/yii2-widget-datepicker:"*" kartik-v/yii2-field-range:"*"
Теперь вместо склейки из наших двух полей передадим в качестве фильтра этот виджет:
<?= GridView::widget([ 'dataProvider' => $dataProvider, 'filterModel' => $searchModel, 'columns' => [ 'id', [ 'filter' => DatePicker::widget([ 'model' => $searchModel, 'attribute' => 'date_from', 'attribute2' => 'date_to', 'type' => DatePicker::TYPE_RANGE, 'separator' => '-', 'pluginOptions' => ['format' => 'yyyy-mm-dd'] ]), 'attribute' => 'created_at', 'format' => 'datetime', ], 'username', 'email:email', ... ['class' => ActionColumn::className()], ], ]);
Проверяем... Всё работает:
Имя пользователя ссылкой
Для просмотра пользователя нам приходится целиться в маленькую иконку просмотра. Удобнее бы было само имя сделать ссылкой на страницу просмотра.
Сделаем это как уже умеем:
[ 'attribute' => 'username', 'format' => 'raw', 'value' => function ($model, $key, $index, $column) { /** @var User $model */ return Html::a(Html::encode($model->username), ['view', 'id' => $model->id]); } ],
Хотя... Вынесем это тоже в отдельную мега-колонку LinkColumn
:
namespace app\components\grid; use Closure; use yii\grid\DataColumn; use yii\helpers\Html; use yii\helpers\Url; class LinkColumn extends DataColumn { /** * @var callable */ public $url; /** * @var bool */ public $targetBlank = false; /** * @var string */ public $controller; /** * @inheritdoc */ public $format = 'raw'; protected function renderDataCellContent($model, $key, $index) { $value = $this->getDataCellValue($model, $key, $index); $text = $this->grid->formatter->format($value, $this->format); $url = $this->createUrl($model, $key, $index); $options = $this->targetBlank ? ['target' => '_blank'] : []; return $value === null ? $this->grid->emptyCell : Html::a($text, $url, $options); } public function createUrl($model, $key, $index) { if ($this->url instanceof Closure) { return call_user_func($this->url, $model, $key, $index); } else { $params = is_array($key) ? $key : ['id' => (string) $key]; $params[0] = $this->controller ? $this->controller . '/view' : 'view'; return Url::toRoute($params); } } }
Здесь мы в методе renderDataCellContent
формируем и возвращаем ссылку. По умолчанию в методе createUrl
мы формируем адрес на действие view
текущего контроллера с использованием значения простого или составного первичного ключа модели. А если в поле link
передана анонимная функция, то запускаем её.
Также мы добавили поле controller
. Это мы подсмотрели в \yii\grid\ActionColumn
. Если нужно сослаться на действие view
другого контроллера, то его нужно будет просто указать в этом поле.
Ссылка на действие view
текущего контроллера нас вполне устраивает, поэтому будем использовать эту колонку с настройками по умолчанию.
<?= GridView::widget([ 'dataProvider' => $dataProvider, 'filterModel' => $searchModel, 'columns' => [ 'id', ... [ 'class' => LinkColumn::className(), 'attribute' => 'username', ], ... ], ]);
В итоге у нас получился маленький, но весьма навороченный грид:
<?= GridView::widget([ 'dataProvider' => $dataProvider, 'filterModel' => $searchModel, 'columns' => [ 'id', [ 'filter' => DatePicker::widget([ 'model' => $searchModel, 'attribute' => 'date_from', 'attribute2' => 'date_to', 'type' => DatePicker::TYPE_RANGE, 'separator' => '-', 'pluginOptions' => ['format' => 'yyyy-mm-dd'] ]), 'attribute' => 'created_at', 'format' => 'datetime', ], [ 'class' => LinkColumn::className(), 'attribute' => 'username', ], 'email:email', [ 'class' => SetColumn::className(), 'filter' => User::getStatusesArray(), 'attribute' => 'status', 'name' => 'statusName', 'cssCLasses' => [ User::STATUS_ACTIVE => 'success', User::STATUS_WAIT => 'warning', User::STATUS_BLOCKED => 'default', ], ], ['class' => ActionColumn::className()], ], ]);
И ещё пара слов про философию человекопонятныйх адресов.
ЧПУ и здравый смысл
Сейчас адреса просмотра, редактирования и удаления пользователя имеют вид:
/admin /admin/users /admin/users/create /admin/users/1 /admin/users/update/1 /admin/users/delete/1
Это красиво, но семантически (по смыслу) не отображает суть происходящего. Действительно, действия update
и delete
«вклиниваются» в середину адреса, оставляя идентификатор всё время позади. С update
и delete
всё понятно, но если взять какие-то другие названия, например notes
, то всё становится неочевидным:
/admin /admin/users /admin/users/create /admin/users/1 /admin/users/update/1 /admin/users/notes/1 /admin/users/notes/create/1 /admin/users/notes/1/3 /admin/users/notes/update/1/3
Как вы думаете, что представляет из себя шестой адрес с notes/1
? Что это за единица? Это вывод первой заметки (users/notes/view/<id>
) или это заметки первого пользователя (users/notes/<user_id>
)?
А в последний адресах вообще указаны два идентификатора и вовсе непонятно что это. У нас такие адреса не используются, но скоро будут.
Это чисто эстетическое неудобство можно простить, но есть и функциональная проблема: если из второй строки убрать единицу, то мы попадём на несуществующий адрес /admin/users/update
. Аналогично если перепутаем идентификаторы местами и сотрём не тот, который нужно было... То есть сейчас это не до конца ЧеловекоПонятные Урлы.
А теперь сравните с этими:
/admin /admin/users /admin/users/create /admin/users/1 /admin/users/1/update /admin/users/1/delete /admin/users/1/notes /admin/users/1/notes/create /admin/users/1/notes/3 /admin/users/1/notes/3/update
Здесь уже каждый вложенный адрес собирается и читается слева направо и является логическим иерархическим продолжением родительского. Здесь можно спокойно вручную удалять любой фрагмент с «хвоста» и легко различать идентификаторы.
Пока у нас нет настолько вложенной структуры, поэтому изменим только существующие правила, содержащие идентификатор <id:\d+>
:
'rules' => [ '' => 'main/default/index', 'contact' => 'main/contact/index', '<_a:error>' => 'main/default/<_a>', '<_a:(login|logout|signup|confirm-email|request-password-reset|reset-password)>' => 'user/default/<_a>', '<_m:[\w\-]+>/<_c:[\w\-]+>/<id:\d+>' => '<_m>/<_c>/view', '<_m:[\w\-]+>/<_c:[\w\-]+>/<id:\d+>/<_a:[\w\-]+>' => '<_m>/<_c>/<_a>', '<_m:[\w\-]+>' => '<_m>/default/index', '<_m:[\w\-]+>/<_c:[\w\-]+>' => '<_m>/<_c>/index', ],
Эти конструкции преобразюет адреса в нашей панели управления в нужный нам вид:
/admin /admin/users /admin/users/create /admin/users/1 /admin/users/1/update /admin/users/1/delete
Теперь с такой маршрутизацией наш новый адрес:
http://site.ru/admin/users/1/update
полностью повторяет навигацию из хлебных крошек:
Главная / Панель управления / Пользователи / Admin / Редактирование
Это красиво и удобно.
Контроль доступа
Теперь надо бы хотя бы примитивно ограничить доступ к админке от незалогиненных пользователей. Серьёзный контроль доступа мы сделаем позже. Про подходы к его построению, быть может, поговорим на одном из следующих вебинаров и по его мотивам реализуем свой RBAC при создании модуля проектов.
А пока вспомним, как мы ограничили доступ к просмотру своего профиля. В ProfileController
мы добавили фильтрацию с помощью AccessControl
с доступом только залогиненным пользователям:
namespace app\modules\user\controllers; use app\modules\user\models\PasswordChangeForm; use app\modules\user\models\User; use yii\filters\AccessControl; use yii\web\Controller; use Yii; class ProfileController extends Controller { public function behaviors() { return [ 'access' => [ 'class' => AccessControl::className(), 'rules' => [ [ 'allow' => true, 'roles' => ['@'], ], ], ], ]; } ... }
Мы можем это же самое ограничение добавить в каждый контроллер модуля admin
. В отличие от модуля user
(с частичным закрытием только некоторых действий и контроллеров) нашу панель управления нужно закрыть полностью.
Но мало кто знает, что помимо контроллеров этот фильтр может работать и с самими классами модулей. Мы можем этим воспользоваться, добавив фильтр всего один раз к самому модулю:
namespace app\modules\admin; use yii\filters\AccessControl; class Module extends \yii\base\Module { public $controllerNamespace = 'app\modules\admin\controllers'; public function behaviors() { return [ 'access' => [ 'class' => AccessControl::className(), 'rules' => [ [ 'allow' => true, 'roles' => ['@'], ], ], ], ]; } }
Это сэкономит наши усилия, так как не придётся копировать этот код из контроллера в контроллер. Если теперь сделать RBAC с ролью или разрешением admin
, то достаточно поменять это здесь на
'rules' => [ [ 'allow' => true, 'roles' => ['admin'], ], ],
и заменить проверку на Yii::$app->user->can('admin')
в главном меню:
Yii::$app->user->can('admin') ? ['label' => Yii::t('app', 'NAV_ADMIN'), 'items' => [ ['label' => Yii::t('app', 'NAV_ADMIN'), 'url' => ['/admin/default/index']], ['label' => Yii::t('app', 'ADMIN_USERS'), 'url' => ['/admin/users/index']], ]] : false,
Но это уже сюжет одного из следующих рассказов, которые вы получите в моей рассылке или увидите в очередном вебинаре...
Следующая часть:
Первый рефакторинг: Перенос переводов и консольных команд в модули
Супер Гуд!
Большое спасибо! Как всегда Вы на высоте! Ждем продолжения.
Спасибо Дима! за отличный материал, желаю огромных успехов
Классно бесспорно! Для новичков конечно сложновато, но подача информации само то. Минимум воды максимум смысла. Огромное спасибо! Желаю дальнейших успехов во благо Ваших читателей. Ждем вебинар по RBAC)))
Поиск по дате не работает. У меня пользователь в базе создан 9 июля(1436427994), в datepicker в первом поле выбираю 11 число, а прилетает в метод search 14 число(1436427994). При этом все равно он мне отображает пользователя за 9 число. Видимо ошибка в параметрах подключения виджета, а так же здесь /modules/admin/models/UserSearch.php ("->andFilterWhere(['>=', 'created_at',") не могу пока разобраться.
Поиск по дате не работает из-за ошибки при валидации этой даты. В модели UserSearch, в правилах валидации, нужно указать, что это php формат даты:
Или изменить формат даты:
Ссылка на документацию: DateValidator
Исправил. Спасибо!
Спасибо за уроки! Жду RBAC :)
$class = 'label-success'; Надо поменять на $class = 'success'; и т.д. в варианте определения в самом гриде
Наверное так правильнее:
Можно и так.
Если обернуть GridView в Pjax, выбор даты начитает глючить, а именно, после первого выбора даты перестает появляться DatePicker при клике в поле ввода. Как это можно побороть?
Можно посмотреть JS-код навешивания DatePicker и запускать его заново после обновления Pjax.
а если модуль создавать не в месте по умолчанию (для адвансед это фронтенд), а в вендор (что логичнее), то ничего не работает и отдается 404 ошибка
В vendor всё должно загружаться только через репозитории. А общие модули создавайте в common/modules.
Не скажу что внимательно статью прочитал, но никак не дождусь ответа на самый главный мой вопрос в yii2)) Как запрещать обращаться к контроллерам не авторизованным пользователям, и более сложный вопрос - устанавливать правила доступа в зависимости от ролей. Если писать ответ долго, то где можно почитать об этом на русском?
В переводе официальной документации, например.
return Html::a(Html::encode($model->username, ['view', 'id' => $model->id]);
скобочка после username пропущена
Спасибо! Исправил.
Да, жду RBAC от Вас!
Спасибо, отличная статья, легко читать.
Дмитрий, добрый день!
Вопрос такой (возможно, пробелы в знании основ PHP)
Итак, в классе SetColumn мы использеум такую конструкцию:
$name = call_user_func($this->name, $model, $key, $index, $this);
Т.е., если я правильно понимаю, вызваем функцию $this->name (в нашем случае для вывода статусов пользователя это statusName, что, с вою очередь, вызывает геттер getStatusName). Здесь, вроде бы, все понятно. Вопрос в том, что в функции call_user_func праметры, начиная со второго, это то, что передается в вызываемую функцию. Хотя в нашем случа User->getStatusName() не принимает никаких параметров...
Зачем тогда все эти параметры в call_user_func?
Вы не заметили полную конструкцию:
Мы указываем 'name' => 'statusName'. Это является строкой, поэтому срабатывает первый вариант ArrayHelper::getValue вместо call_user_func.
Точно! спасибо!
Вот если бы в представлении было бы что-то типа
'name' => function ($model, $key, $index) {} , тогда вызвался бы call_user_func. Тогда еще один вопрос. Зачем туда передается $this?
$this попадает в $widget:
Здравствуйте.
У меня есть поле контент, которое я заполняю с помощью визуального редактора.
В итоге во время просмотра данных видно HTML теги.
Подскажите, как от этого избавиться.
у Nav::widget это 'encodeLabels' => false,
.
есть что-то похожее в GridView?
Для фильтрации через HTML Purifier:
или
Для полного отключения – формат raw:
Работает для GridView и DetailView.
Вы уж извините, но я не понял. (Только приступил к изучению)
у меня есть вывод виджета:
просветите как мне поступить для отображения без HTML тегов
Если именно без тегов, то функцией strip_tags:
низкий поклон вам)
Дмитрий,
скажите, пожалуйста, в чем принципиально различие атрибутов 'content' и 'value' у DataColumn?
Сторим код в DataColumn:
и в Column:
В value можно передать либо имя атрибута модели, либо функцию. DataColumn сам извлечёт значение атрибута и выведет в указанном формате.
А в content можно передать только функцию и её результат выведется как есть.
Убрал наследование от User в модели UserSearch.
Дим когда ожидать продолжения???
Дмитрий, жду видео от вас про DI и Service Locator, планируете?
Планирую.
Исправил сценарии в модели User.
Добрый день. Может ли фильтр и сортировку сделать через POST(урл оставался прежним)? Если можно то каким образом?
Можно просто обернуть в Pjax с enablePushState = false.
Хорошие уроки. Дмитрий жду с нетерпением продолжения. Интересует RBAC, связи таблиц - фильтрация полей по связанным таблицам.
Добрый день.
Благодарю за статью, как всегда всё доступно расписали.
Но у меня всё таки возникла проблема с обновлением записей в БД.
Ваш пример:
При обновлении вообще ничего не происходи, страница просто перезагружается.
Я немного изменил Ваш пример
В этом случае срабатывает только
Но никаких ошибок нет, массив пустой.
При этом действия "index", "delete", "view", "create" отлично выполняются.
Проверил построчно свой код, сравнил с Вашими примерами, но так ничего и не исправил, не могу понять где у меня ошибка.
Подскажите, пожалуйста, где я накосячить смог?
А beforeSave(...) в модели переопределяли?
Вот полностью код модели \app\modules\admin\models
Дело в том, что действие "update" не работает даже если я использую модель Users от которой наследуется User
Значит исследуйте ту модель.
Родительская модель взята с Ваших статей, проверил тоже построчно - результат "0".
Не могу понять в чём дело...
Проблема решилась. Виной тому моя невнимательность))) Запутался в скобках)))
return true была расположена внутри условия if(){}, поэтому и не работало обновление
Благодарю за подсказку.
Добрый день. А возможно ли в GridView сделать так, чтобы колонки были по одним данным, а фильтр по другим. Например табличка с данными по отзывам(отзыв, дата,положительный или отрицательный), а фильтр по пользователям этих отзывов(пол, возраст,...) ?
Да. Добавляете любые поля, делаете joinWith связей и фильтруете по всем табицам как угодно:
И в представлении _search.php для формы поиска эти поля вывести.
Добрый день. А возможно ли в GridView сделать группировку полей c одинаковыми id. Т.е. например есть ответы на вопросы с разными датами, а нужно эти ответы сгруппировать по id вопроса и как то выделить другими цветами или раскрывающий список сделать.
Спасибо за отличные уроки!!!
Добрый вечер!!!
Дмитрий спасибо за отличную работу...
Я только начал разбираться с Yii2
Вы пишите:
"Также мы добавили поле controller. Это мы подсмотрели в \yii\grid\ActionColumn. Если нужно сослаться на действие view другого контроллера, то его нужно будет просто указать в этом поле."
дальше код
не могу понять как обращаться к свойствам LinkColumn, а точнее к контроллеру?
Спасибо за ответ.
Все разобрался...
ответ скрывался в "Это мы подсмотрели в \yii\grid\ActionColumn." (подсмотрел как у него работает),
может кому понадобатся
и еще нужно не забыть изменить $key, а то идентификатор модели будет той на которой находится.
мало ли кому пригодится ))
У меня вопрос насчёт LinkColumn. Подскажи как сделать так, чтобы ссылка вела не на view, а на update?
Спасибо, за прекрасный материал. Дмитрий, подскажите пожалуйста, а если нужно сделать в GridView выпадающий фильтр, с возможностью выбирать несколько вариантов? Никак не получается реализовать.
В нужной колонке, в параметре фильтр пишу
Фильтр отображается, возможноть выбирать несколько вариантов есть, но в поисковую модель передается всегда массив только с одним значением. Независимо от того, сколько вариантов я выбрал. И соответственно после перезагрузки страницы, выделен только один вариант, а не все которые я выбрал .Т.е. в урл даже не передается несколько значений..
Как быть?
В GridView делать multiple не пробовал, так что не подскажу.
Здравствуйте Дмитрий. Хочу спросить про вот такую ошибку на Хостинге. На локалке стоит PHP 7, на хостинге PHP 5.5.31.
Выдаёт предупреждение на эту строку
http://pravpod.16mb.com/Test/public/admin/users/index
На локалке PHP 7, предупреждений нет. Все предупреждения включены.
Эту строку возможно переписать под PHP 5.5 ?
Там логин и пароль просит. Можете строку сюда скопировать?
Там много информации на странице и показывает на строку. Вдруг надо всю станицу с ошибкой смотреть, я пароль и логин не прячу)) это не секретная информация
Логин - superuser
Пароль - root
т.е. первый элемент массива вернуть? Спасибо большое. Сегодня испытаю.
Огромное спасибо за помощь. Я почти так делал, но не получилось))
И вопрос по GridView.
Можно ли в строке 'username' равной 'superuser', не показывать ссылку {delete}? Что бы даже админ не смог сам себя удалить случайно. Или в данном случае это не реально и оставить так?
Ну либо так, либо оставить с кнопкой и только в actionDelete проверку сделать.
Добрый день.
Дмитрий, подскажите, а есть ли возможность с помощью GridView сделать таблицу, чтобы данные выводились из БД в текстовые поля, где их можно было бы сразу все отредактировать, а потом просто нажать, к примеру, одну кнопку "Обновить" и все данные обновились в БД. Если есть, то где можно посмотреть пример. Или для этого есть специальные виджеты и админпанели, если есть, то подскажите какие.
Заранее благодарен.
http://demos.krajee.com/grid-demo
Данную таблицу я рассматривал, но я не смог найти то что меня интересует. Мне нужна возможность водить данные в таблицу, как к примеру в Excel переходя по ячейкам с помощью Tab, а потом все данные по нажатию кнопки занести в базу. А в этой таблице надо все равно щелкать мышкой на каждую позицию для ее изменения в всплывающем окне, а это замедляет ввод данных в таблицу.
https://github.com/yiisoft/yii2/blob/master/docs/guide-ru/input-tabular-input.md
Дмитрий, спасибо за ссылку. Документация помогла, но есть одно, но. Не срабатывает обновление. И я не могу понять в чем дело. Все сделал вроде как описано. Таблица выводится со значения нормально.
В контроллере
Вот сама форма где я вывожу $models. :
Добавление ->label($model->name) думаю ситуацию не поменяет.
Вношу изменения. Обновляю. Но данные остаются старые. Вывел массив передаваемый через Post, как видно тираж был 1000 стал 10001 и т.д., вроде значения обновились:
Array ( [_csrf] => Li0wdXp0emIeWnwXSC1NK0UbYBAWJzAaQhR9EjAVGzVdXVRAHQQNBQ== [VizitkiOfset] => Array ( [1] => Array ( [id] => 1 [naimenovanie] => [bumaga] => Мелованная бумага 250 г/м. [cvetnost] => 4+0, 4+4 [pokrytie] => Тираж [t1] => 10001 [t2] => 20001 [t3] => 30001 [t4] => 40001 [t5] => 50001 [t6] => 100001 [t7] => 150001 [t8] => 200001 ) [2] => Array ( [id] => 2....
и т.д. весь массив не стал переписывать
также вывел маcсив который прошел Model::loadMultiple($models, Yii::$app->request->post()) вот результат той строчки из массива которая должна была обновиться:
и т.д.
как я понял в loadMultiple не выполняется замена значений переданных в Post.
Подскажите пожалуйста в чем может скрываться проблема.
А в rules() модели VizitkiOfset эти поля вроде t1 есть?
Да в rules перечислены все выводимые поля. Но я заметил такую же проблему с таблицей Users которая создавалась автоматически. У нее такие же проблемы что и с VizitkiOfset. Хотя делал все операции через Gii. Так вот и там и там, записи новые добавить можно, но вот обновить не получается. Может ли быть это связано с использованием AdminLTE. Или все же где-то произошла ошибка при использовании Gii.
Тогда не знаю.
Хорошо, буду копать дальше. Спасибо за помощь и терпение.
Делаю
появились папки vendor/kartik-v.....
Добавляю виджет
и получаю ошибку "Class 'DatePicker' not found"
Может еще где то нужно прописать этот datapicker в пространстве имен и т.д. ?(не очень понимаю эти компосеры)
Нашел проблему.
нужно добавить в project\modules\admin\views\users\index.php
Тогда нормально вызывается DatePicker::widget !!! Фильтрация даты это очень важный вопрос. Спасибо.
Вопрос а вот это
Как то без символических ссылок сделать можно ?
А где здесь символические ссылки?
При использовании этого кода возникла ошибка symlink(): Cannot create symlink, error code(1314)
со ссылкой в dedug на конец этой строки 'pluginOptions' => ['format' => 'yyyy-mm-dd']),. Вот я и решил что символические ссылки. А как тогда понять эту ошибку ?
Понимать так, что символические ссылки не работают Windows.
Согласен, но получается, что вот в этом коде есть символическая ссылка( ибо если его закомментировать то все работает). Вот я и спросил, а можно ли без символических ссылок? А если в коде нет символических ссылок то почему вываливается такая ошибка ?
Просто не ставьте 'linkAssets' = true в конфигурации компонента assetManager.
Добрый день!
Нашел вашу статью про админку, и делаю по ней грид.
Дошел до момента с DatePicker, скачал компонент, но довольно странное решение в нем реализовано. Когда устанавливаешь первую дату, тут же проставляется и вторая дата, равная первой. Смотрел документацию, нигде не нашел, как это отключить. Может вы подскажите?
Пока буду делать костыль из двух датапикеров.
Да, можно и самому вручную два склеить.
Добрый день.
Воспользовавшись Вашим примером, написал следующие функции:
Однако заметил в логах очень много запросов типа:
Возможно стоит закешировать запрос. Попробовал так, как описано тут: https://yiiframework.com.ua/ru/doc/guide/2/caching-data/ но к сожалению ничего не получилось.Не могли бы Вы подсказать как правильно выполнить кеширование в этом случае.
Спасибо
Сделайте hasOne связь и используйте её с жадной загрузкой.
Спасибо за отличные уроки! Очень полезно и познавательно)
У меня вопрос не по теме урока: не могу зайти в личный кабинет: ошибка 404.
Дмитрий, подскажите, а как можно перенести вот такую повторяющуюся простыню из вьюхи, в отдельный компонент и уже потом просто указывать на него
Наследуем:
и используем:
Ошибочка небольшая. attributeLabels из модели UserSearch не работает в данном случае.
Если делать отдельную форму поиска в _search.php над гридом (который обычно генерируется в Gii), то там всё будет.
Доброй ночи.
Подскажите, пожалуйста, как сделать вывод статуса в DetaiView как в GridView?
Вручную:
Это понятно.
Я не могу понять, как сделать так, чтобы class для span подставлялся в зависимости от status пользователя.
Действительно, всё оказалось проще, чем я думал(
Благодарю за подсказку.
Спасибо, Дим) Хороший блог и полезные статьи.
Создал модуль админа. Создал каталог для моделей. Создал CRUD через gii. Прописал модуль в config/common. Не пойму, почему при переходе в /admin/users/index - Not Found (#404) ? Когда должна страница с пользователями выводиться....?
Опять поспешил. В контроллере символ не тот указал.
В итоге мы можем зайти по адресу /admin/users/index и увидеть список пользователей сайта:
Не работает, не видит страницы(ошибка 404), хотя все файлы создал.....
Какой символ , Роман?
Тоже там ошибся ,нашел...ппц...путаница какая-то....
Добрый вечер всем.
Вообщем впал в ступор, уже ничего не соображаю.
Отнаследовался от модели User, удалил ненужный метод scenarios, прописал явно все поля как на скрине:
Продублировал метод attributeLabels.
Удалил ненужные данные фильтрации из метода $query->andFilterWhere:
В итоге, в форме вывода пользователей, в строке поиска пропали поля ввода created_at и updated_at.
Нет именно полей ввода.
Почему такое может происходить? Где глянуть можно?
Вроде всё пересмотрел, а понять не могу.
Для фильтра поля должны быть прописаны в rules().
Здравствйте, Дмитрий, у пользователей сохраняю дни рождения в формате d.m.Y, поле varchar, не работает поиск например найти всех с датой от 1.1.1990 до 1.1.1999
Вопрос какой тип поля нужно указывать для формата d.m.Y чтоб работал поиск по типу вашему?
Пробовал datatime, но там другой формат, может можно как указать свой?
Лучше, всё-таки, даты хранить в date в Y-m-d, а на сайте выводить в d.m.Y. Иначе нужно использовать функцию STR_TO_DATE в MySQL, что напрягает процессор и замедляет запрос.
Что-то не получается по примеру сделать фильтрацию
В модели:
Поля вводятся вручную
В итоге на странице
Все выводится как обычно, как будто нет никаких фильтров. Подскажите, что не так делаю?
Посмотрите, какой генерируется SQL-запрос. И заполняются ли эти поля до andFilterWhere.
Создал модуль админа. Создал каталог для моделей. Создал CRUD через gii. Прописал модуль в config/common. Не пойму, почему при переходе в /admin/users/index - Not Found (#404) ? Когда должна страница с пользователями выводиться.
Подскажите какой символ нужно изменить, а то я не нахожу нечего лишнего???????????
У меня была ошибка в UserController вместо UsersController
Когда меняю
class UserSearch extends User
на
class UserSearch extends Model, то выходят ошибки
Есть мысли по этому поводу?
Нужно добавить все поля вручную:
Большое спасибо!
Ещё вопрос, тянется уже не помню с какого урока.
Не могу разлогиниться с сайта. "Страница не найдена".
localhost/index.php?r=user%2Fdefault%2Flogout - такой адрес у ссылка на логаут.
Значит где-то что-то не совпадает с правилом маршрутизации.
подскажите, зачем мы добавили сценарий ADMIN_UPDATE? без него разве не тоже самое получается?
http://prntscr.com/jf46sw
мдааа, столько времени потратить чтобы потом вот это увидеть... причем все делалось как в тексте всех статей.......как можно так учить........ причем ошибка, которую хер поймешь, из за убогого дебагера от yii
Здравствуйте Дмитрий? в поисках решения проблемы посетил ваш блог.
Не могу решить такую задачу, при регистрации пользователя на почту пользователя приходит письмо с подтверждением, где он должен перейти по ссылке чтобы подтвердить регистрацию.
Но ссылка приходит вот такая:
Не подскажите как убрать первый admin?
Заранее благодарен.
Исправить маршрут ссылки в письме.
Добрый день!
У вас для класса LinkColumn() среди параметров указано, что можно указать contoroller, чтобы сделать ссылку вообще на другой модуль, нужно ваш класс доработать или имя контроллера как-то правильно указать
Сейчас ФИО автора материала выводится корректно, а вот ссылка идёт не туда куда нужно
URL Manger такой:
Доброго времени суток. Столкнулся с проблемой при редактировании пользователя из админки. Пишет что пользователь и емейл уже зарегистрированы. Проблему решил двумя способами:
Как я понял ошибка в том, что в родительском классе, в правиле валидации указывался targetClass модели из модуля пользователя.
<?php
<span class="label label-success">Активен</span>
<span class="label label-warning">Ожидает подтверждения</span>
<span class="label label-default">Заблокирован</span>
Куда записывать этот код?