Сервис на Yii2: Модуль администрирования и GridView

После добавления личного кабинета пользователя и редактора профиля в прошлых частях сегодня начнём делать модуль администратора. А именно, создадим модуль и CRUD для управления пользователями с помощью генератора Gii и доработаем его вид и маршрутизацию под свои требования.

Часть 1: Установка и настройка приложения
Часть 2: Настройка IDE и модульная структура
Часть 3: Перенос пользователей в БД
Часть 4: Доработка шаблона и локализация
Часть 5: Просмотр и редактирование профиля

Исходники проекта на GitHub

Сейчас мы поместим контроллеры по управлению в отдельный модуль, а не в тот, к которому они относятся. С архитектурной точки зрения это не очень хорошо. В одной из последующих частей мы сделаем более осознанную модульную структуру, а пока оставим всё так.

Сгенерируем модуль 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">
 
    <?php $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>
 
    <?php 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,
            'sort' => [
                'defaultOrder' => ['id' => SORT_DESC],
            ]
        ]);
 
        ...
    }
}

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

Но это уже сюжет одного из следующих рассказов, которые вы получите в моей рассылке или увидите в очередном вебинаре...

Следующая часть:

Первый рефакторинг: Перенос переводов и консольных команд в модули

Другие статьи

Самая объёмная тема среди предложенных завершена. Выкладываю исправленную и дополненную запись вебинара-скринкаста о тестировании c PHPUnit и Codeception. Добавлены и доработаны примеры кода, пункты про аннотации, фикстуры, анализ покрытия, Faker, про установку всего через Composer и другие нюансы.

Недавно в обратную связь поступил вопрос: куда пойти работать неопытному молодому программисту? А именно, продолжить искать заказы на фрилансе или всё-таки устроиться удалённо в крупную студию или другую компанию и работать там?

Время идёт, уроки становятся круче. Провели очередной вебинар по мотивам статьи о поведениях. В перерыве поговорили о жизни, о программировании, о блогах. А какая тема дальше? Поразмышлял об этом в эфире. В любом случае сразу вам сообщю в рассылке по вебинарам.

Давным-давно была опубликована статья про события в Yii. В дополнение на этот раз попробовали выполнить несколько примеров в прямом эфире. Начали с событий в JavaScript, пощёлкали по кнопкам, потом перешли в PHP и изучили принципы объявления, навешивания обработчиков и запуска события в Yii2 Framework.

Комментарии

 

Andrey

Супер Гуд!

Ответить

 

Vlad

Большое спасибо! Как всегда Вы на высоте! Ждем продолжения.

Ответить

 

Дмитрий

Спасибо Дима! за отличный материал, желаю огромных успехов

Ответить

 

nexus

Классно бесспорно! Для новичков конечно сложновато, но подача информации само то. Минимум воды максимум смысла. Огромное спасибо! Желаю дальнейших успехов во благо Ваших читателей. Ждем вебинар по RBAC)))

Ответить

 

Spirit Absolute

Поиск по дате не работает. У меня пользователь в базе создан 9 июля(1436427994), в datepicker в первом поле выбираю 11 число, а прилетает в метод search 14 число(1436427994). При этом все равно он мне отображает пользователя за 9 число. Видимо ошибка в параметрах подключения виджета, а так же здесь /modules/admin/models/UserSearch.php ("->andFilterWhere(['>=', 'created_at',") не могу пока разобраться.

Ответить

 

Sergey Bogdanov

Поиск по дате не работает из-за ошибки при валидации этой даты. В модели UserSearch, в правилах валидации, нужно указать, что это php формат даты:

[['date_from', 'date_to'], 'date', 'format' => 'php:Y-m-d']

Или изменить формат даты:

[['date_from', 'date_to'], 'date', 'format' => 'yyyy-MM-dd']

Ссылка на документацию: DateValidator

Ответить

 

Дмитрий Елисеев

Исправил. Спасибо!

Ответить

 

Александр Соломаха

Спасибо за уроки! Жду RBAC :)

Ответить

 

Александр Александров

$class = 'label-success'; Надо поменять на $class = 'success'; и т.д. в варианте определения в самом гриде

Ответить

 

Valentin

Наверное так правильнее:

namespace app\components\grid;
 
use yii\grid\ActionColumn as BaseActionColumn;
 
class ActionColumn extends BaseActionColumn
{
    public $contentOptions = [
        'style' => 'white-space: nowrap; text-align: center; letter-spacing: 0.1em; max-width: 7em;',
    ];
}
Ответить

 

Дмитрий Елисеев

Можно и так.

Ответить

 

Дмитрий Куткин

Если обернуть GridView в Pjax, выбор даты начитает глючить, а именно, после первого выбора даты перестает появляться DatePicker при клике в поле ввода. Как это можно побороть?

Ответить

 

Дмитрий Елисеев

Можно посмотреть JS-код навешивания DatePicker и запускать его заново после обновления Pjax.

Ответить

 

des

а если модуль создавать не в месте по умолчанию (для адвансед это фронтенд), а в вендор (что логичнее), то ничего не работает и отдается 404 ошибка

Ответить

 

Дмитрий Елисеев

В vendor всё должно загружаться только через репозитории. А общие модули создавайте в common/modules.

Ответить

 

Эдуард

Не скажу что внимательно статью прочитал, но никак не дождусь ответа на самый главный мой вопрос в yii2)) Как запрещать обращаться к контроллерам не авторизованным пользователям, и более сложный вопрос - устанавливать правила доступа в зависимости от ролей. Если писать ответ долго, то где можно почитать об этом на русском?

Ответить

 

Andrey

return Html::a(Html::encode($model->username, ['view', 'id' => $model->id]);

скобочка после username пропущена

Ответить

 

Дмитрий Елисеев

Спасибо! Исправил.

Ответить

 

Ильдар Камалов

Да, жду RBAC от Вас!

Ответить

 

Владимир

Спасибо, отличная статья, легко читать.

Ответить

 

Andrewkha

Дмитрий, добрый день!
Вопрос такой (возможно, пробелы в знании основ PHP)

Итак, в классе SetColumn мы использеум такую конструкцию:

$name = call_user_func($this->name, $model, $key, $index, $this);

Т.е., если я правильно понимаю, вызваем функцию $this->name (в нашем случае для вывода статусов пользователя это statusName, что, с вою очередь, вызывает геттер getStatusName). Здесь, вроде бы, все понятно. Вопрос в том, что в функции call_user_func праметры, начиная со второго, это то, что передается в вызываемую функцию. Хотя в нашем случа User->getStatusName() не принимает никаких параметров...

Зачем тогда все эти параметры в call_user_func?

Ответить

 

Дмитрий Елисеев

Вы не заметили полную конструкцию:

if (is_string($this->name)) {
    $name = ArrayHelper::getValue($model, $this->name);
} else {
    $name = call_user_func($this->name, $model, $key, $index, $this);
}

Мы указываем 'name' => 'statusName'. Это является строкой, поэтому срабатывает первый вариант ArrayHelper::getValue вместо call_user_func.

Ответить

 

Andrewkha

Точно! спасибо!

Ответить

 

Andrewkha

Вот если бы в представлении было бы что-то типа

'name' => function ($model, $key, $index) {} , тогда вызвался бы call_user_func. Тогда еще один вопрос. Зачем туда передается $this?

Ответить

 

Дмитрий Елисеев

$this попадает в $widget:

'name' => function ($model, $key, $index, $widget) {}
Ответить

 

Виталий

Здравствуйте.
У меня есть поле контент, которое я заполняю с помощью визуального редактора.
В итоге во время просмотра данных видно HTML теги.
Подскажите, как от этого избавиться.

у Nav::widget это 'encodeLabels' => false,
.
есть что-то похожее в GridView?

Ответить

 

Дмитрий Елисеев

Для фильтрации через HTML Purifier:

'content:html',

или

[
    'attribute' => 'content',
    'format' => 'html',
],

Для полного отключения – формат raw:

'content:raw',

Работает для GridView и DetailView.

Ответить

 

Виталий

Вы уж извините, но я не понял. (Только приступил к изучению)
у меня есть вывод виджета:

<?= GridView::widget([
    'dataProvider' => $dataProvider,
    'filterModel' => $searchModel,
    'columns' => [
        'id',
        'created_at',
        'updated_at',
        'enabled',
        'title',
        'content',
    ],
]); ?>

просветите как мне поступить для отображения без HTML тегов

Ответить

 

Дмитрий Елисеев

Если именно без тегов, то функцией strip_tags:

'columns' => [
    'id',
    'title',
    [
        'attribute' => 'content',
        'value' => function ($model) {
            return StringHelper::truncate(strip_tags($model->content), 100);
        },
    ],
],
Ответить

 

Виталий

низкий поклон вам)

Ответить

 

Andrewkha

Дмитрий,

скажите, пожалуйста, в чем принципиально различие атрибутов 'content' и 'value' у DataColumn?

Ответить

 

Дмитрий Елисеев

Сторим код в DataColumn:

class DataColumn extends Column
{
    protected function renderDataCellContent($model, $key, $index)
    {
        if ($this->content === null) {
            return $this->grid->formatter->format($this->getDataCellValue($model, $key, $index), $this->format);
        } else {
            return parent::renderDataCellContent($model, $key, $index);
        }
    }
}

и в Column:

class Column extends Object
{
    protected function renderDataCellContent($model, $key, $index)
    {
        if ($this->content !== null) {
            return call_user_func($this->content, $model, $key, $index, $this);
        } else {
            return $this->grid->emptyCell;
        }
    }
}

В value можно передать либо имя атрибута модели, либо функцию. DataColumn сам извлечёт значение атрибута и выведет в указанном формате.

А в content можно передать только функцию и её результат выведется как есть.

Ответить

 

Дмитрий Елисеев

Убрал наследование от User в модели UserSearch.

Ответить

 

Дмитрий

Дим когда ожидать продолжения???

Ответить

 

Web design Dubai

Дмитрий, жду видео от вас про DI и Service Locator, планируете?

Ответить

 

Дмитрий Елисеев

Планирую.

Ответить

 

Дмитрий Елисеев

Исправил сценарии в модели User.

Ответить

 

Денис

Добрый день. Может ли фильтр и сортировку сделать через POST(урл оставался прежним)? Если можно то каким образом?

Ответить

 

Дмитрий Елисеев

Можно просто обернуть в Pjax с enablePushState = false.

Ответить

 

Ярослав

Хорошие уроки. Дмитрий жду с нетерпением продолжения. Интересует RBAC, связи таблиц - фильтрация полей по связанным таблицам.

Ответить

 

slo_nik

Добрый день.
Благодарю за статью, как всегда всё доступно расписали.
Но у меня всё таки возникла проблема с обновлением записей в БД.

Ваш пример:

    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]);
        }
    }

При обновлении вообще ничего не происходи, страница просто перезагружается.

Я немного изменил Ваш пример

    public function actionUpdate($id)
    {
        $model = $this->findModel($id);

        $model->scenario = User::SCENARIO_ADMIN_UPDATE;

        if ($model->load(Yii::$app->request->post()) && $model->validate()) {

            if(!$model->save()){

               print_r($model->getErrors());

            }
            else{
              return $this->redirect(['view', 'id' => $model->id]);
            }

        } else {
            return $this->render('update', ['model' => $model]);
        }
    }

В этом случае срабатывает только

 if(!$model->save()){

               print_r($model->getErrors());

 }

Но никаких ошибок нет, массив пустой.

При этом действия "index", "delete", "view", "create" отлично выполняются.

Проверил построчно свой код, сравнил с Вашими примерами, но так ничего и не исправил, не могу понять где у меня ошибка.

Подскажите, пожалуйста, где я накосячить смог?

Ответить

 

Дмитрий Елисеев

А beforeSave(...) в модели переопределяли?

Ответить

 

slo_nik

Вот полностью код модели \app\modules\admin\models

namespace app\modules\admin\models;

use Yii;
use yii\helpers\ArrayHelper;
use app\modules\user\models\Users;

class User extends Users
{

  const SCENARIO_ADMIN_CREATE = 'adminCreate';
  const SCENARIO_ADMIN_UPDATE = 'adminUpdate';

  public $newPassword;
  public $repeatNewPassword;

  public function rules()
  {

     return ArrayHelper::merge(parent::rules(), [

        [['newPassword', 'repeatNewPassword'], 'required', 'on' => self::SCENARIO_ADMIN_CREATE],
        ['newPassword', 'string', 'min' => 6],
        ['repeatNewPassword', 'compare', 'compareAttribute' => 'newPassword']

     ]);

  }

  public function scenarios()
  {

     $scenarios = parent::scenarios();
     
     $scenarios[self::SCENARIO_ADMIN_CREATE] = ['username', 'email', 'phone', 'country', 'city', 'newPassword', 'repeatNewPassword'];

     $scenarios[self::SCENARIO_ADMIN_UPDATE] = ['username', 'email', 'phone', 'country', 'city', 'newPassword', 'repeatNewPassword'];
     return $scenarios;
  }
  public function attributeLabels()
  {
     return ArrayHelper::merge(parent::attributeLabels(), [
        'newPassword' => 'Новый пароль',
        'repeatNewPassword' => 'Повторить новый пароль'         
     ]);
  }
  public function beforeSave($insert)
  {
     if(parent::beforeSave($insert)){
        if(!empty($this->newPassword)){
           $this->setPassword($this->newPassword);
        }
        return true;
     }
     return false;
  }
}
Ответить

 

slo_nik

Дело в том, что действие "update" не работает даже если я использую модель Users от которой наследуется User

Ответить

 

Дмитрий Елисеев

Значит исследуйте ту модель.

Ответить

 

slo_nik

Родительская модель взята с Ваших статей, проверил тоже построчно - результат "0".
Не могу понять в чём дело...

Ответить

 

slo_nik

Проблема решилась. Виной тому моя невнимательность))) Запутался в скобках)))
return true была расположена внутри условия if(){}, поэтому и не работало обновление
Благодарю за подсказку.

Ответить

 

Роман

Добрый день. А возможно ли в GridView сделать так, чтобы колонки были по одним данным, а фильтр по другим. Например табличка с данными по отзывам(отзыв, дата,положительный или отрицательный), а фильтр по пользователям этих отзывов(пол, возраст,...) ?

Ответить

 

Дмитрий Елисеев

Да. Добавляете любые поля, делаете joinWith связей и фильтруете по всем табицам как угодно:

class VoteSearch extends Model
{
    public $id;
    public $age;

    public function rules()
    {
        return [
            ['id', 'age', 'safe'],
        ]
    }
    
    public function search($params) 
    {
        $query = Vote::find()
            ->from(['vote' => Vote::tableName()])
            ->joinWith(['user' => function($query) {
                $query->from(['user' => User::tableName()]);
            }]);

        ...

        $query->andFilterWhere([
            'vote.id' => $this->id,
            'user.age' => $this->age,
        ]);
    }
}


И в представлении _search.php для формы поиска эти поля вывести.

Ответить

 

Юрий

Добрый день. А возможно ли в GridView сделать группировку полей c одинаковыми id. Т.е. например есть ответы на вопросы с разными датами, а нужно эти ответы сгруппировать по id вопроса и как то выделить другими цветами или раскрывающий список сделать.
Спасибо за отличные уроки!!!

Ответить

 

Юрий

Добрый вечер!!!

Дмитрий спасибо за отличную работу...
Я только начал разбираться с Yii2
Вы пишите:
"Также мы добавили поле controller. Это мы подсмотрели в \yii\grid\ActionColumn. Если нужно сослаться на действие view другого контроллера, то его нужно будет просто указать в этом поле."

дальше код

<?= GridView::widget([
    'dataProvider' => $dataProvider,
    'filterModel' => $searchModel,
    'columns' => [
        'id',
        ...
        [
            'class' => LinkColumn::className(),
            'attribute' => 'username',
        ],
        ...
    ],
]); ?>

не могу понять как обращаться к свойствам LinkColumn, а точнее к контроллеру?
Спасибо за ответ.

Ответить

 

Юрий

Все разобрался...
ответ скрывался в "Это мы подсмотрели в \yii\grid\ActionColumn." (подсмотрел как у него работает),
может кому понадобатся

[
                'class' => LinkColumn::className(),
                'controller' => 'name',
                'attribute' => 'fullName'
            ],

Ответить

 

Юрий

и еще нужно не забыть изменить $key, а то идентификатор модели будет той на которой находится.

$dataProvider->key = 'newID';

мало ли кому пригодится ))

Ответить

 

Spirit Absolute

У меня вопрос насчёт LinkColumn. Подскажи как сделать так, чтобы ссылка вела не на view, а на update?

Ответить

 

Дмитрий Елисеев
'url' => function ($data) {
    return Url::to(['update', 'id' => $data->id]);
},
Ответить

 

Андрей

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

В нужной колонке, в параметре фильтр пишу

'filter' => Html::activeDropDownList($searchModel, 'status', Accounts::getStatusList(), ['class' => 'form-control', 'multiple' => true]),

Фильтр отображается, возможноть выбирать несколько вариантов есть, но в поисковую модель передается всегда массив только с одним значением. Независимо от того, сколько вариантов я выбрал. И соответственно после перезагрузки страницы, выделен только один вариант, а не все которые я выбрал .Т.е. в урл даже не передается несколько значений..
Как быть?

Ответить

 

Дмитрий Елисеев

В GridView делать multiple не пробовал, так что не подскажу.

Ответить

 

Виталий Еропкин

Здравствуйте Дмитрий. Хочу спросить про вот такую ошибку на Хостинге. На локалке стоит PHP 7, на хостинге PHP 5.5.31.
Выдаёт предупреждение на эту строку

http://pravpod.16mb.com/Test/public/admin/users/index

На локалке PHP 7, предупреждений нет. Все предупреждения включены.
Эту строку возможно переписать под PHP 5.5 ?

Ответить

 

Дмитрий Елисеев

Там логин и пароль просит. Можете строку сюда скопировать?

Ответить

 

Виталий Еропкин

Там много информации на странице и показывает на строку. Вдруг надо всю станицу с ошибкой смотреть, я пароль и логин не прячу)) это не секретная информация
Логин - superuser
Пароль - root

Ответить

 

Дмитрий Елисеев
$roles = Yii::$app->authManager->getRolesByUser($this->id_user);
return array_shift($roles);
Ответить

 

Виталий Еропкин

т.е. первый элемент массива вернуть? Спасибо большое. Сегодня испытаю.

Ответить

 

Виталий Еропкин

Огромное спасибо за помощь. Я почти так делал, но не получилось))

Ответить

 

Виталий Еропкин

И вопрос по GridView.
Можно ли в строке 'username' равной 'superuser', не показывать ссылку {delete}? Что бы даже админ не смог сам себя удалить случайно. Или в данном случае это не реально и оставить так?

<?= GridView::widget([
            'dataProvider' => $dataProvider,
            'columns' => [
                ['class' => 'yii\grid\SerialColumn'],
                'username',
                [
                    'label' => 'Группа',
                    'value' => 'role.description',
                ],
                [
                    'class' => 'yii\grid\ActionColumn',
                    'template' => '{view} {update} {delete}',
                    'buttons' => [
                        'delete' => function ($url, $model, $key) {
                            return Html::a('<span class="glyphicon glyphicon-trash"></span>', $url, [
                                'title' => Yii::t('yii', 'Заблокировать'),
                                'data-confirm' => Yii::t('yii', 'Вы действительно хотите заблокировать пользователя?'),
                                'data-method' => 'post',
                            ]);
                        },
                    ],
                ],
            ],
        ]); ?>
Ответить

 

Дмитрий Елисеев

Ну либо так, либо оставить с кнопкой и только в actionDelete проверку сделать.

Ответить

 

Андрей Порсев

Добрый день.
Дмитрий, подскажите, а есть ли возможность с помощью GridView сделать таблицу, чтобы данные выводились из БД в текстовые поля, где их можно было бы сразу все отредактировать, а потом просто нажать, к примеру, одну кнопку "Обновить" и все данные обновились в БД. Если есть, то где можно посмотреть пример. Или для этого есть специальные виджеты и админпанели, если есть, то подскажите какие.
Заранее благодарен.

Ответить

 

Андрей Порсев

Данную таблицу я рассматривал, но я не смог найти то что меня интересует. Мне нужна возможность водить данные в таблицу, как к примеру в Excel переходя по ячейкам с помощью Tab, а потом все данные по нажатию кнопки занести в базу. А в этой таблице надо все равно щелкать мышкой на каждую позицию для ее изменения в всплывающем окне, а это замедляет ввод данных в таблицу.

Ответить

 

Андрей Порсев

Дмитрий, спасибо за ссылку. Документация помогла, но есть одно, но. Не срабатывает обновление. И я не могу понять в чем дело. Все сделал вроде как описано. Таблица выводится со значения нормально.
В контроллере

public function actionUpdate() {
   $models = VizitkiOfset::find()->indexBy('id')->all();
   if (Model::loadMultiple($models, Yii::$app->request->post()) && Model::validateMultiple($models)) {
            foreach ($models as $model) {
                $model->save(false);
            }
            return $this->redirect('index');
    }
    return $this->render('update', ['models' => $models]);
}


Вот сама форма где я вывожу $models. :

<?php $form = ActiveForm::begin(); ?>
	<table>
		 <?php foreach ($models as $index => $model) { ?>
		 <tr>
			 <td><?= $form->field($model, "[$index]id")?></td>
			 <td><?= $form->field($model, "[$index]naimenovanie") ?></td>
			 <td><?= $form->field($model, "[$index]bumaga") ?></td>
			 <td><?= $form->field($model, "[$index]cvetnost") ?></td>
			 <td><?= $form->field($model, "[$index]pokrytie") ?></td>
			 <td><?= $form->field($model, "[$index]t1") ?></td>
			 <td><?= $form->field($model, "[$index]t2") ?></td>
			 <td><?= $form->field($model, "[$index]t3") ?></td>
			 <td><?= $form->field($model, "[$index]t4") ?></td>
			 <td><?= $form->field($model, "[$index]t5") ?></td>
			 <td><?= $form->field($model, "[$index]t6") ?></td>
			 <td><?= $form->field($model, "[$index]t7") ?></td>
			 <td><?= $form->field($model, "[$index]t8") ?></td>
		</tr>
		<?php } ?>
	</table>
	<div class="form-group">
        <?= Html::submitButton('Обновить', ['class' => 'btn btn-primary']) ?>
    </div>
    <?php ActiveForm::end();?>

Добавление ->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()) вот результат той строчки из массива которая должна была обновиться:

Array ( [1] => common\models\VizitkiOfset Object ( [_attributes:yii\db\BaseActiveRecord:private] => Array ( [id] => 1 [naimenovanie] => [bumaga] => Мелованная бумага 250 г/м. [cvetnost] => 4+0, 4+4 [pokrytie] => Тираж [t1] => 1000 [t2] => 2000 [t3] => 3000 [t4] => 4000 [t5] => 5000 [t6] => 10000 [t7] => 15000 [t8] => 20000 ) [_oldAttributes:yii\db\BaseActiveRecord:private] => Array ( [id] => 1 [naimenovanie] => [bumaga] => Мелованная бумага 250 г/м. [cvetnost] => 4+0, 4+4 [pokrytie] => Тираж [t1] => 1000 [t2] => 2000 [t3] => 3000 [t4] => 4000 [t5] => 5000 [t6] => 10000 [t7] => 15000 [t8] => 20000 ) [_related:yii\db\BaseActiveRecord:private] => Array ( ) [_errors:yii\base\Model:private] => Array ( ) [_validators:yii\base\Model:private] => ArrayObject Object ( [storage:ArrayObject:private] => Array ( ) ) [_scenario:yii\base\Model:private] => default [_events:yii\base\Component:private] => Array ( ) [_behaviors:yii\base\Component:private] => Array ( ) ) [2] => common\models\VizitkiOfset Object....

и т.д.

как я понял в loadMultiple не выполняется замена значений переданных в Post.

Подскажите пожалуйста в чем может скрываться проблема.

Ответить

 

Дмитрий Елисеев

А в rules() модели VizitkiOfset эти поля вроде t1 есть?

Ответить

 

Андрей Порсев

Да в rules перечислены все выводимые поля. Но я заметил такую же проблему с таблицей Users которая создавалась автоматически. У нее такие же проблемы что и с VizitkiOfset. Хотя делал все операции через Gii. Так вот и там и там, записи новые добавить можно, но вот обновить не получается. Может ли быть это связано с использованием AdminLTE. Или все же где-то произошла ошибка при использовании Gii.

Ответить

 

Дмитрий Елисеев

Тогда не знаю.

Ответить

 

Андрей Порсев

Хорошо, буду копать дальше. Спасибо за помощь и терпение.

Ответить

 

Сергей

Делаю

php composer.phar require kartik-v/yii2-widget-datepicker:"*" kartik-v/yii2-field-range:"*""

появились папки vendor/kartik-v.....

Добавляю виджет

[
    'filter' => DatePicker::widget([
        'model' => $searchModel,
        'attribute' => 'date_from'...

и получаю ошибку "Class 'DatePicker' not found"

Может еще где то нужно прописать этот datapicker в пространстве имен и т.д. ?(не очень понимаю эти компосеры)

Ответить

 

Сергей

Нашел проблему.

нужно добавить в project\modules\admin\views\users\index.php

use kartik\date\DatePicker

Тогда нормально вызывается DatePicker::widget !!! Фильтрация даты это очень важный вопрос. Спасибо.

Ответить

 

Сергей Беловенцев

Вопрос а вот это

        [
            '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',
        ],


Как то без символических ссылок сделать можно ?

Ответить

 

Дмитрий Елисеев

А где здесь символические ссылки?

Ответить

 

Сергей Беловенцев

При использовании этого кода возникла ошибка symlink(): Cannot create symlink, error code(1314)
со ссылкой в dedug на конец этой строки 'pluginOptions' => ['format' => 'yyyy-mm-dd']),. Вот я и решил что символические ссылки. А как тогда понять эту ошибку ?

Ответить

 

Дмитрий Елисеев

Понимать так, что символические ссылки не работают Windows.

Ответить

 

Сергей Беловенцев

Согласен, но получается, что вот в этом коде есть символическая ссылка( ибо если его закомментировать то все работает). Вот я и спросил, а можно ли без символических ссылок? А если в коде нет символических ссылок то почему вываливается такая ошибка ?

Ответить

 

Дмитрий Елисеев

Просто не ставьте 'linkAssets' = true в конфигурации компонента assetManager.

Ответить

 

Сергей

Добрый день!
Нашел вашу статью про админку, и делаю по ней грид.
Дошел до момента с DatePicker, скачал компонент, но довольно странное решение в нем реализовано. Когда устанавливаешь первую дату, тут же проставляется и вторая дата, равная первой. Смотрел документацию, нигде не нашел, как это отключить. Может вы подскажите?
Пока буду делать костыль из двух датапикеров.

Ответить

 

Дмитрий Елисеев

Да, можно и самому вручную два склеить.

Ответить

 

Дмитрий

Добрый день.
Воспользовавшись Вашим примером, написал следующие функции:

public function getGenreName()
    {
        return ArrayHelper::getValue(self::getGenre(), $this->track->track_genre);
    }

    public static function getGenre()
    {
        $return = Genre::find()
            ->indexBy('id')
            ->all();
        return ArrayHelper::map($return,'id3', 'name');
    }


Однако заметил в логах очень много запросов типа:

SELECT * FROM "genre"


Возможно стоит закешировать запрос. Попробовал так, как описано тут: https://yiiframework.com.ua/ru/doc/guide/2/caching-data/ но к сожалению ничего не получилось.Не могли бы Вы подсказать как правильно выполнить кеширование в этом случае.
Спасибо

Ответить

 

Дмитрий Елисеев

Сделайте hasOne связь и используйте её с жадной загрузкой.

Ответить

 

Иван Зайцев

Спасибо за отличные уроки! Очень полезно и познавательно)
У меня вопрос не по теме урока: не могу зайти в личный кабинет: ошибка 404.

Ответить

 

dindilin

Дмитрий, подскажите, а как можно перенести вот такую повторяющуюся простыню из вьюхи, в отдельный компонент и уже потом просто указывать на него

['class' => 'yii\grid\ActionColumn',
'template' => '{view} {update} {delete}',
'buttons' => [
    'view' => function ($url, $model, $key) {
        return Html::a('<span class="fa fa-eye"></span>', $url, [
            'class' => 'btn btn-success btn-sm',
        ]);
    },
    'update' => function ($url, $model, $key) {
        return Html::a('<span class="fa fa-pencil-square-o"></span>', $url, [
            'class' => 'btn btn-primary btn-sm',
        ]);
    },
    'delete' => function ($url, $model, $key) {
        return Html::a('<span class="fa fa-trash"></span>', $url, [
            'class' => 'btn btn-danger btn-sm',
        ]);
    },
],
],
Ответить

 

Дмитрий Елисеев

Наследуем:

class ButtonColumn extends ActionColumn
{
    public $template = '{view} {update} {delete}';

    public function init()
    {
        if (!isset($this->buttons['view'])) {
            $this->buttons['view'] = function ($url, $model, $key) { ... },
        }
        if (!isset($this->buttons['update'])) {
            $this->buttons['update'] => function ($url, $model, $key) { ... },
        if (!isset($this->buttons['delete'])) {
            $this->buttons['delete'] => function ($url, $model, $key) { ... },
        }
        parent::init();
    }
}

и используем:

['class' => 'app\grid\ButtonColumn']
Ответить

 

Макс

Ошибочка небольшая. attributeLabels из модели UserSearch не работает в данном случае.

Ответить

 

Дмитрий Елисеев

Если делать отдельную форму поиска в _search.php над гридом (который обычно генерируется в Gii), то там всё будет.

Ответить

 

slo_nik

Доброй ночи.
Подскажите, пожалуйста, как сделать вывод статуса в DetaiView как в GridView?

Ответить

 

Дмитрий Елисеев

Вручную:

[
    'attribute' => 'status',
    'value' => Html::tag('span', $model->getStatusName(), ...),
    'format' => 'raw',
],
Ответить

 

slo_nik

Это понятно.
Я не могу понять, как сделать так, чтобы class для span подставлялся в зависимости от status пользователя.

Ответить

 

Дмитрий Елисеев
ArrayHelper::getValue([0 => 'default', 1 => 'primary', ...], $model->status)
Ответить

 

slo_nik

Действительно, всё оказалось проще, чем я думал(
Благодарю за подсказку.

Ответить

 

Сергей

Спасибо, Дим) Хороший блог и полезные статьи.

Ответить

 

Роман

Создал модуль админа. Создал каталог для моделей. Создал CRUD через gii. Прописал модуль в config/common. Не пойму, почему при переходе в /admin/users/index - Not Found (#404) ? Когда должна страница с пользователями выводиться....?

Ответить

 

Роман

Опять поспешил. В контроллере символ не тот указал.

Ответить

 

Shvarz

В итоге мы можем зайти по адресу /admin/users/index и увидеть список пользователей сайта:

Не работает, не видит страницы(ошибка 404), хотя все файлы создал.....

Ответить

 

Shvarz

Какой символ , Роман?

Ответить

 

Shvarz

Тоже там ошибся ,нашел...ппц...путаница какая-то....

Ответить

 

Роман

Добрый вечер всем.

Вообщем впал в ступор, уже ничего не соображаю.

Отнаследовался от модели User, удалил ненужный метод scenarios, прописал явно все поля как на скрине:

    public $id;
    public $username;
    public $email;
    public $status; 

Продублировал метод attributeLabels.

Удалил ненужные данные фильтрации из метода $query->andFilterWhere:

'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
->andFilterWhere(['like', 'auth_key', $this->auth_key])
->andFilterWhere(['like', 'email_confirm_token', $this->email_confirm_token])
->andFilterWhere(['like', 'password_hash', $this->password_hash])
->andFilterWhere(['like', 'password_reset_token', $this->password_reset_token])

В итоге, в форме вывода пользователей, в строке поиска пропали поля ввода 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, что напрягает процессор и замедляет запрос.

Ответить

 

Алексей

Что-то не получается по примеру сделать фильтрацию

В модели:

public $from_date;
public $to_date;

[['from_date', 'to_date'], 'date', 'format' => 'php:Y-m-d'],

   ->andFilterWhere(['>=', 'sort_date', $this->from_date ? strtotime($this->from_date . ' 00:00:00') : null])
   ->andFilterWhere(['<=', 'sort_date', $this->to_date ? strtotime($this->to_date . ' 23:59:59') : null]);

Поля вводятся вручную

<?= $form->field($model, 'from_date')->widget(\yii\widgets\MaskedInput::className(), [
    'mask' => '9999-99-99',
]) ?>
    
<?= $form->field($model, 'to_date')->widget(\yii\widgets\MaskedInput::className(), [
    'mask' => '9999-99-99',
]) ?>

В итоге на странице

index.php?r=article%2Fdefault%2Findex&ArticleSearch%5Bfrom_date%5D=2012-01-01&ArticleSearch%5Bto_date%5D=2013-01-01

Все выводится как обычно, как будто нет никаких фильтров. Подскажите, что не так делаю?

Ответить

 

Дмитрий Елисеев

Посмотрите, какой генерируется SQL-запрос. И заполняются ли эти поля до andFilterWhere.

Ответить

Оставить комментарий

Войти | Завести аккаунт


(никто не увидит)



Можно использовать теги <p> <ul> <li> <b> <i> <a> <pre>