Сервис на Yii2: Просмотр и редактирование профиля

Профиль пользователя

В прошлой части мы исправляли и переводили интерфейс нашего приложения. Сегодня мы, как анонсировалось ранее, добавим страницу просмотра профиля пользователя и выведем формы для редактирования информации о пользователе и для смены пароля.

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

Для удобства изучения сходники проекта теперь доступны на GitHub.

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

Итак, приступим. В модуле user на данный момент имеется только контроллер DefaultController.

Дадим пользователю возможность просматривать и редактировать свой профиль. Для этого добавим новый контроллер ProfileController, доступный только для авторизованных пользователей. Сделаем его пока с единственным действием index. В этом действии мы получаем модель текущего пользователя и передаём в представление:

namespace app\modules\user\controllers;
 
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' => ['@'],
                    ],
                ],
            ],
        ];
    }
 
    public function actionIndex()
    {
        return $this->render('index', [
            'model' => $this->findModel(),
        ]);
    }
 
    /**
     * @return User the loaded model
     */
    private function findModel()
    {
        return User::findOne(Yii::$app->user->identity->getId());
    }
}

Вспомним, что наша модель User реализовывает интерфейс IdentityInterface и её класс указан в качестве identityClass для компонента Yii::$app->user:

$config = [
    ...
    'components' => [
        'user' => [
            'identityClass' => 'app\modules\user\models\User',
            'enableAutoLogin' => true,
        ],
        ...
    ],
];

и при авторизации в качестве Yii::$app->user->identity используется именно экземпляр модели User текущего пользователя. Это значит, что в качестве $model мы могли бы использовать прямо этот экземпляр Yii::$app->user->identity. Но безопаснее в ходе выполнения действия контроллера не давать доступа к самому экземпляру Yii::$app->user->identity. Поэтому мы ещё раз загрузили экземпляр пользователя из базы данных:

return User::findOne(Yii::$app->user->identity->getId());

При этом не возникнет проблем с пустым identity, так как с помощью поведения AccessControl мы разрешаем доступ ко всем действиям этого контроллера только авторизованным пользователям.

В представлении modules/user/views/profile/index.php будем просто выводить информацию о пользователе с помощью виджета DetailView:

<?php
<?php
 
use yii\helpers\Html;
use yii\widgets\DetailView;
 
/* @var $this yii\web\View */
/* @var $model app\modules\user\models\User */
 
$this->title = Yii::t('app', 'TITLE_PROFILE');
$this->params['breadcrumbs'][] = $this->title;
?>
<div class="user-profile">
 
    <h1><?= Html::encode($this->title) ?></h1>
 
    <?= DetailView::widget([
        'model' => $model,
        'attributes' => [
            'username',
            'email',
        ],
    ]) ?>
 
</div>

Теперь мы можем попробовать отобразить профиль. Для удобства добавим в меню пункт Профиль:

echo Nav::widget([
    'options' => ['class' => 'navbar-nav navbar-right'],
    '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_PROFILE'), 'url' => ['/user/profile/index']] :
            false,
        !Yii::$app->user->isGuest ?
            ['label' => Yii::t('app', 'NAV_LOGOUT'),
                'url' => ['/user/default/logout'],
                'linkOptions' => ['data-method' => 'post']] :
            false,
    ]),
]);

и не забудем добавлять переводы по мере необходимости в файлы в созданных на прошлом уроке папках messages/en и messages/ru.

С нашими правилами маршрутизации эта страница будет доступна по адресу /user/profile. Мы могли бы не создавать отдельный контроллер ProfileController и поместить этот функционал прямо в DefaultController::actionIndex, чтобы профиль отображался по адресу /user. Но для нашего удобства всё-таки вынесли. Иначе DefaultController окажется слишком большим.

Давайте ещё сделаем так, чтобы с пока нерабочего (так как действия actionIndex в DefaultController пока нет) адреса /user нас автоматически перекидывало на /user/profile. Просто добавим действие и поместим в него перенаправление на соседний контроллер:

namespace app\modules\user\controllers;
 
...
 
class DefaultController extends Controller
{
    ...
 
    public function actionIndex()
    {
        return $this->redirect(['profile/index'], 301);
    }
}

Теперь при ручном переходе по адресу /user не вывалится ошибка, а сработает редирект на профиль:

Страница просмотра готова. Добавим теперь страницу редактирования.

Редактирование: сценарии и формы

Итак, наша текущая модель пользователя хранит правила валидации для имени пользователя, адреса электронной почты и статуса:

class User extends ActiveRecord implements IdentityInterface
{
    ...
 
    public function rules()
    {
        return [
            ['username', 'required'],
            ['username', 'match', 'pattern' => '#^[\w_-]+$#i'],
            ['username', 'unique', 'targetClass' => self::className(), 'message' => Yii::t('app', 'ERROR_USERNAME_EXISTS')],
            ['username', 'string', 'min' => 2, 'max' => 255],
 
            ['email', 'required'],
            ['email', 'email'],
            ['email', 'unique', 'targetClass' => self::className(), 'message' => Yii::t('app', 'ERROR_EMAIL_EXISTS')],
            ['email', 'string', 'max' => 255],
 
            ['status', 'integer'],
            ['status', 'default', 'value' => self::STATUS_ACTIVE],
            ['status', 'in', 'range' => array_keys(self::getStatusesArray())],
        ];
    }
 
    ...
}

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

class ProfileController extends Controller
{
    ...
 
    public function actionUpdate()
    {
        $model = $this->findModel();
 
        if ($model->load(Yii::$app->request->post()) && $model->save()) {
            return $this->redirect(['index']);
        } else {
            return $this->render('update', [
                'model' => $model,
            ]);
        }
    }
 
    ...
}

и сделав для него представление с формой:

<?php
...
 
<?php $form = ActiveForm::begin([]); ?>
 
<?= $form->field($model, 'email')->input('email', ['maxlength' => true]) ?>
 
<div class="form-group">
    <?= Html::submitButton(Yii::t('app', 'BUTTON_SAVE'), ['class' => 'btn btn-primary']) ?>
</div>
 
<?php ActiveForm::end(); ?>
 
...

Но есть нюанс:

Из формы будут приниматься все перечисленные в rules() поля. И если пользователь с помощью инструментов разработчика в браузере добавит в форму поле User[status] или User[username], то может изменить недоступные ему поля. Но нам нужно сохранить эти поля для редактирования в панели администратора.

Для решения этой проблемы можно пойти двумя путями. Их мы и рассмотрим.

Сценарии модели

Любая модель в Yii2 позволяет задавать ей несколько сценариев использования, для которых можно задавать разные правила валидации и для смены поведения.

Нам нужно ввести два сценария: один для администратора (со всеми правилами) и второй для пользователя в его профиле. По умолчанию в модели все правила рассматриваются для сценария default, хранящегося в константе Model::SCENARIO_DEFAULT. Так что нам остаётся добавить один сценарий profile. Обозначим его также константой User::SCENARIO_PROFILE:

class User extends ActiveRecord implements IdentityInterface
{
    const SCENARIO_PROFILE = 'profile';
 
    ...
}

Теперь нам нужно определить, какие правила будут работать у администратора, а какие на странице профиля пользователя. Можно пойти путём Yii1, введя в специфические правила указания применимости on к сценарию либо исключения except:

class User extends ActiveRecord implements IdentityInterface
{
    const SCENARIO_PROFILE = 'profile';
 
    ...
 
    public function rules()
    {
        return [
            ...
 
            ['email', 'required', 'except' => self::SCENARIO_PROFILE],
            ['email', 'email', 'except' => self::SCENARIO_PROFILE],
            ['email', 'unique', 'targetClass' => self::className(), 'except' => self::SCENARIO_PROFILE, 'message' => Yii::t('app', 'ERROR_EMAIL_EXISTS')],
            ['email', 'string', 'max' => 255, 'except' => self::SCENARIO_PROFILE],
 
            ...
        ];
    }
}

И фреймворк для сканирования атрибутов запустит метод scenarios() модели, который обойдёт все правила и построит список сценариев и полей для них.

Но это не очень удобно с большим количеством правил. Намного удобнее оставить правила как есть и самому переопределить метод scenarios(), где вручную перечислить нужные для каждого сценария поля:

class User extends ActiveRecord implements IdentityInterface
{
    const SCENARIO_PROFILE = 'profile';
 
    ...
 
    public function scenarios()
    {
        return [
            self::SCENARIO_DEFAULT => ['username', 'email', 'status'],
            self::SCENARIO_PROFILE => ['email'],
        ];
    }
}

или аналогично:

class User extends ActiveRecord implements IdentityInterface
{
    const SCENARIO_PROFILE = 'profile';
 
    ...
 
    public function scenarios()
    {
        return ArrayHelper::merge(parent::scenarios(), [
            self::SCENARIO_PROFILE => ['email'],
        ]);
    }
}

Этими строками мы разрешим пользователю редактировать только адрес электронной почты. А все остальные поля разрешим менять в будущем только администратору.

Теперь в действии редактирования профиля не забудем указать модели нужный нам сценарий:

namespace app\modules\user\controllers;
 
use app\modules\user\models\User;
use yii\filters\AccessControl;
use yii\web\Controller;
use Yii;
use yii\widgets\ActiveForm;
 
class ProfileController extends Controller
{
    ...
 
    public function actionUpdate()
    {
        $model = $this->findModel();
        $model->scenario = User::SCENARIO_PROFILE;
 
        if ($model->load(Yii::$app->request->post()) && $model->save()) {
            return $this->redirect(['index']);
        } else {
            return $this->render('update', [
                'model' => $model,
            ]);
        }
    }
 
    ...
}

И добавим само представление modules/user/views/profile/update.php с формой:

<?php
<?php
use yii\bootstrap\ActiveForm;
use yii\helpers\Html;
 
/* @var $this yii\web\View */
/* @var $model app\modules\user\models\User */
 
$this->title = Yii::t('app', 'TITLE_UPDATE');
$this->params['breadcrumbs'][] = ['label' => Yii::t('app', 'TITLE_PROFILE'), 'url' => ['index']];
$this->params['breadcrumbs'][] = $this->title;
?>
<div class="user-profile-update">
 
    <h1><?= Html::encode($this->title) ?></h1>
 
    <div class="user-form">
 
        <?php $form = ActiveForm::begin(); ?>
 
        <?= $form->field($model, 'email')->textInput(['maxlength' => true]) ?>
 
        <div class="form-group">
            <?= Html::submitButton(Yii::t('app', 'BUTTON_SAVE'), ['class' => 'btn btn-primary']) ?>
        </div>
 
        <?php ActiveForm::end(); ?>
 
    </div>
 
</div>

Пока у нас здесь всего одно поле:

Но в будущем можем реализовать и другие настройки профиля.

Теперь рассмотрим другой подход.

Модели форм

Использовать сценарии удобно, но это немного захламляет модель. Если мы вспомним наши страницы логина, регистрации, восстановления пароля, то для них почему-то добавлены отдельные модели LoginForm, SignupForm и PasswordResetForm.

Но ведь, по сути, это относится к модели пользователя. Их бы можно было реализовать с помощью единственной модели User со сценариями SCENARIO_LOGIN, SCENARIO_SIGNUP и подобными. Но кроме различия правил валидации в этих моделях есть свои специфические методы login(), signup() и прочие, которые нужны только для одного действия.

Если бы мы собрали все эти сценарии использования в одной модели User, то там оказалось бы слишком много вспомогательного кода для реализации всех случаев жизни. И, в итоге, наша модель из простой записи ActiveRecord превратилась бы в большой комбайн с десятками и сотнями методов и вспомогательных полей.

Именно поэтому ради упрощения кода для реализации специфичных форм желательно создавать отдельные модели.

Это, во первых, позволяет получить несколько простых и «чистых» классов.

Во-вторых, повышает безопасность, так как при описании сценариев можно нечаянно ошибиться и открыть какое-нибудь служебное поле, а при использовании отдельного класса пользователь не имеет прямого доступа к редактированию исходной модели User.

И, в третьих, внутри такой отдельной модели удобно добавлять любой код, любые public поля, оперировать любыми другими сущностями. Например, подписать пользователя на рассылку, если он поставил галочку в поле subscribe. Помещать это всё в одну модель User было бы слишком громоздко.

Так что добавим новую форму для редактирования данных пользователя с полем $email:

namespace app\modules\user\models;
 
use yii\base\Model;
use yii\db\ActiveQuery;
use Yii;
 
class ProfileUpdateForm extends Model
{
    public $email;
 
    /**
     * @var User
     */
    private $_user;
 
    public function __construct(User $user, $config = [])
    {
        $this->_user = $user;
        $this->email = $user->email;
        parent::__construct($config);
    }
 
    public function rules()
    {
        return [
            ['email', 'required'],
            ['email', 'email'],
            [
                'email',
                'unique',
                'targetClass' => User::className(),
                'message' => Yii::t('app', 'ERROR_EMAIL_EXISTS'),
                'filter' => ['<>', 'id', $this->_user->id],
            ],
            ['email', 'string', 'max' => 255],
        ];
    }
 
    public function update()
    {
        if ($this->validate()) {
            $user = $this->_user;
            $user->email = $this->email;
            return $user->save();
        } else {
            return false;
        }
    }
}

У нас нет в базе отдельной таблицы profile, так что мы будем использовать только модель User. Мы переопределили конструктор так, чтобы получать эту модель. Поменяем теперь контроллер:

public function actionUpdate()
{
    $user = $this->findModel();
    $model = new ProfileUpdateForm($user);
 
    if ($model->load(Yii::$app->request->post()) && $model->update()) {
        return $this->redirect(['index']);
    } else {
        return $this->render('update', [
            'model' => $model,
        ]);
    }
}

Всё. Сценарий SCENARIO_PROFILE нам здесь больше не нужен.

Страница редактирования профиля готова.

Страница смены пароля

Чтобы не загромождать модель User, сделаем смену пароля отдельной от редактирования профиля формой.

Для формы ввода создадим модель PasswordChangeForm и добавим три поля для нового и текущего паролей и правила валидации для них. В эту модель через конструктор мы будем передавать модель пользователя, чей пароль нужно будет изменить. Для валидации текущего пароля создадим отдельный метод-валидатор currentPassword, который будет проверять правильность пароля, вызывая метод validatePassword переданной ему модели пользователя:

namespace app\modules\user\models;
 
use yii\base\Model;
use Yii;
 
/**
 * Password reset form
 */
class PasswordChangeForm extends Model
{
    public $currentPassword;
    public $newPassword;
    public $newPasswordRepeat;
 
    /**
     * @var User
     */
    private $_user;
 
    /**
     * @param User $user
     * @param array $config
     */
    public function __construct(User $user, $config = [])
    {
        $this->_user = $user;
        parent::__construct($config);
    }
 
    public function rules()
    {
        return [
            [['currentPassword', 'newPassword', 'newPasswordRepeat'], 'required'],
            ['currentPassword', 'currentPassword'],
            ['newPassword', 'string', 'min' => 6],
            ['newPasswordRepeat', 'compare', 'compareAttribute' => 'newPassword'],
        ];
    }
 
    public function attributeLabels()
    {
        return [
            'newPassword' => Yii::t('app', 'USER_NEW_PASSWORD'),
            'newPasswordRepeat' => Yii::t('app', 'USER_REPEAT_PASSWORD'),
            'currentPassword' => Yii::t('app', 'USER_CURRENT_PASSWORD'),
        ];
    }
 
    /**
     * @param string $attribute
     * @param array $params
     */
    public function currentPassword($attribute, $params)
    {
        if (!$this->hasErrors()) {
            if (!$this->_user->validatePassword($this->$attribute)) {
                $this->addError($attribute, Yii::t('app', 'ERROR_WRONG_CURRENT_PASSWORD'));
            }
        }
    }
 
    /**
     * @return boolean
     */
    public function changePassword()
    {
        if ($this->validate()) {
            $user = $this->_user;
            $user->setPassword($this->newPassword);
            return $user->save();
        } else {
            return false;
        }
    }
}

Добавим действие смены пароля в наш контроллер:

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 actionPasswordChange()
    {
        $user = $this->findModel();
        $model = new PasswordChangeForm($user);
 
        if ($model->load(Yii::$app->request->post()) && $model->changePassword()) {
            return $this->redirect(['index']);
        } else {
            return $this->render('passwordChange', [
                'model' => $model,
            ]);
        }
    }
 
    ...
}

и добавим представление modules/user/views/profile/passwordChange.php:

<?php
<?php
 
use yii\bootstrap\ActiveForm;
use yii\helpers\Html;
 
/* @var $this yii\web\View */
/* @var $model app\modules\user\models\ChangePasswordForm */
 
$this->title = Yii::t('app', 'TITLE_PASSWORD_CHANGE');
$this->params['breadcrumbs'][] = ['label' => Yii::t('app', 'TITLE_PROFILE'), 'url' => ['index']];
$this->params['breadcrumbs'][] = $this->title;
?>
<div class="user-profile-password-change">
 
    <h1><?= Html::encode($this->title) ?></h1>
 
    <div class="user-form">
 
        <?php $form = ActiveForm::begin(); ?>
 
        <?= $form->field($model, 'currentPassword')->passwordInput(['maxlength' => true]) ?>
        <?= $form->field($model, 'newPassword')->passwordInput(['maxlength' => true]) ?>
        <?= $form->field($model, 'newPasswordRepeat')->passwordInput(['maxlength' => true]) ?>
 
        <div class="form-group">
            <?= Html::submitButton(Yii::t('app', 'BUTTON_SAVE'), ['class' => 'btn btn-primary']) ?>
        </div>
 
        <?php ActiveForm::end(); ?>
 
    </div>
 
</div>

Страница смены пароля готова:

Теперь в соседнее представление просмотра профиля index.php добавим ссылки на эти действия:

<?php
<?php
use yii\helpers\Html;
use yii\widgets\DetailView;
 
/* @var $this yii\web\View */
/* @var $model app\modules\user\models\User */
 
$this->title = Yii::t('app', 'TITLE_PROFILE');
$this->params['breadcrumbs'][] = $this->title;
?>
<div class="user-profile">
 
    <h1><?= Html::encode($this->title) ?></h1>
 
    <p>
        <?= Html::a(Yii::t('app', 'BUTTON_UPDATE'), ['update'], ['class' => 'btn btn-primary']) ?>
        <?= Html::a(Yii::t('app', 'LINK_PASSWORD_CHANGE'), ['password-change'], ['class' => 'btn btn-primary']) ?>
    </p>
 
    <?= DetailView::widget([
        'model' => $model,
        'attributes' => [
            'username',
            'email',
        ],
    ]) ?>
 
</div>

В приложении теперь появилась страница просмотра профиля:

страница редактирования данных (пока у нас всего одно поле):

и страница смены пароля:

Вот и всё. А дальше:

Часть 6: Модуль администрирования и GridView

Подписывайтесь на рассылку (форма в сайдбаре), чтобы не пропустить продолжение и получать полезные фишки, и оставляйте свои предложения или замечания в комментариях. И оставляйте предложения по темам будущих вебинаров здесь. Спасибо!

Комментарии

 

Евгений Левачев

Спасибо, хорошо пишешь., красавчик :)

Ответить

 

Дмитрий

Спасибо за статью. Продолжай в том же духе... По чаще бы статьи выходили. ;)

Ответить

 

Евгений Левачев

думаю логично было бы далее сделать модуль admin и вынести в него управление пользователями. Так же не помешало бы разобрать тему про RBAC

Ответить

 

Дмитрий

Полностью поддерживаю....

Ответить

 

Юрий

Спасибо огромное!

Ответить

 

SSDD

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

Ответить

 

Роман

Здравствуйте, Дмитрий! А в какую папку будет разумнее кидать css, javascript'ы, следуя вашему шаблону?

Ответить

 

Alex

в папку web/css, или создай свою папку themes с с темой + css, js

Ответить

 

Сашка

В моделе указано:

['status', 'default', 'value' => self::STATUS_ACTIVE],

почему не self::STATUS_WAIT ?

Ответить

 

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

Здесь без разницы.

Ответить

 

EVG

Вопрос, а что передается в переменной params вот тут: validatePassword($attribute, $params) и зачем она там нужна, вроде ж она нигде не используется?

Ответить

 

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

Это сигнатура написания встроенных валидаторов. При вызове с параметрами

['password', 'validatePassword', 'param1' => 'value1', 'param2' => 'value2']

как раз всё остальное попадёт в $params:

[
    'param1' => 'value1',
    'param2' => 'value2',
]
Ответить

 

EVG

по этой функции понятно, спасибо. Еще в документации покурил по Yii1, там как раз код закомментирован в схожей функции.
А вот теперь задача, помогите разобраться. Решил на основе вашего примера реализовать смену пароля в модели User, а не в новой модели.
Кнотроллер:

public function actionUpdate()
    {
        $model = $this->findModel();
        $model->scenario = User::SCENARIO_PROFILE;

        if ($model->load(Yii::$app->request->post()) && $model->profileupdate()) {
            return $this->redirect(['index']);
        } else {
            return $this->render('update', [
                'model' => $model,
            ]);
        }
    }


Модель:

class User extends \yii\db\ActiveRecord implements IdentityInterface
{
...

    public $currentPassword;
    public $newPassword;
    public $newPasswordRepeat;

...
    public function rules()
    {
        return [
...
            /*[['currentPassword', 'newPassword', 'newPasswordRepeat'], 'required'],*/
            ['currentPassword', 'required'],
            ['currentPassword', 'changeValidatePassword'],
            ['newPassword', 'string', 'min' => 6],
            ['newPasswordRepeat', 'compare', 'compareAttribute' => 'newPassword'],
        ];
    }
...
public function validatePassword($password)
    {
        return Yii::$app->security->validatePassword($password, $this->password_hash);
    }
...
/**
     * @param string $attribute
     * @param array $params
     */
    public function changeValidatePassword($attribute, $params)
    {
        if (!$this->hasErrors()) {
            if (!$this->validatePassword($this->$attribute)) {
                $this->addError($attribute, Yii::t('app', 'ERROR_WRONG_CURRENT_PASSWORD'));
            }
        }
    }

    /**
     * @return boolean
     */
    public function profileupdate()
    {
        if ($this->validate() && strlen($this->newPassword) > 0) {
            echo 'len='.strlen($this->newPassword);
            $this->setPassword($this->newPassword);
            return $this->save();
        } else {
            if ($this->validate() && strlen($this->newPassword) == 0){
                echo 'len='.strlen($this->newPassword);
                return $this->save();
            } else {
                return false;
            }
        }
    }


тут я думал, что пароль вводить при редактировании профиля нужно будет по-любому, а вот нового пароля может и не быть, поэтому убрал из правил обязательное условие заполнения полей нового пароля и перед сохранением проверяю есть ли что там чтобы понять нужно ли обновлять пароль.
Если новый пароль не заполнять, форма работает, поля обновляет, текущий пароль валидируется. А вот если новый пароль забить, то форма не проходит проверку текущего пароля (changeValidatePassword). Она ж должна тупо захешировать текущий и сравнить с таблицей. В поле пароль не меняется, проверил. Уже всю голову сломал, где я дурак?

Ответить

 

EVG

отвечаю сам себе.
при валидации пароля хеш читается от нового, сначала срабатывает setPassword, а потом сравниваются хеши

Ответить

 

Grey

Добрый день очень понравились курсы, хочу ими воспользоваться для работы над дипломным проектом.

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

Можно ли сделать небольшой список с классами моделей и контроллеров, которые вы делали на прошлых занятиях и вкратце их функционал?

Ответить

 

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

А Вы когда что-то делаете сразу конспектируйте, что именно делали. Тогда на следующем уроке сразу всё вспомните.

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

А все классы и методы действуют именно так, как они и называются.

Ответить

 

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

опечатка:

...Иначе DefaultDontroller окажется слишком большим.

Ответить

 

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

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

Ответить

 

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

Добавил модель ProfileUpdateForm.

Ответить

 

igor

Здравствуйте! Подскажите в чем может быть проблема? Делаю по вашему руководству. Возникает ошибка:

Argument 1 passed to frontend\models\PasswordChangeForm::__construct() must be an instance of frontend\models\User, none given, called in
Ответить

 

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

Значит пользователя не передаёте в new PasswordChangeForm($user).

Ответить

 

igor

Спасибо, уже разобрался!

Ответить

 

Mike Artemiev

Не понимаю зачем ради защиты от обновления лишних полей модели User, городить сценарии или отдельную модель формы, если в контроллере в actionUpdate вместо загрузки всех $_POST'ов

$model->load(Yii::$app->request->post()

можно загружать только нужный

$model->email = Yii::$app->request->post('email')

Или это слишком хардкодно?

Ответить

 

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

Да, весьма хардкорно:

if (isset($_POST['User'])) {
    $model->email = ArrayHelper::getValue($_POST['User'], 'email');
    if ($model->save()) {
        ...
    }
}

Особенно если полей много.

Ответить

 

Иван Зайцев

В разделе "Страница смены пароля" написано: "Для валидации текущего пароля создадим отдельный метод-валидатор currentPassword". Но метод-валидатор называется не currentPassword, а validatePassword

Ответить

 

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

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

Ответить

 

Новичек

Добрый вечер Дмитрий. Подскажите пожалуйста как добавить поле для загрузки изображения. Рядом с редактированием email. Не загружаются оттуда картинки. Как я ни пытался. А так хотелось бы что бы в профиле у пользователя изображение тоже было

модель

class ProfileUpdateForm extends Model
{
    public $email;
    public $username;
    public $usersurname;
    public $data;
    public $age;
    public $seats;
    public $from;
    public $date;
    public $time;
    public $img;
    public $file;

    /**
     * @var User
     */
    private $_user;

    public function __construct(User $user, $config = [])
    {
        $this->_user = $user;
        parent::__construct($config);
    }

    public function init()
    {
        $this->email = $this->_user->email;
        $this->username = $this->_user->username;
        $this->usersurname = $this->_user->usersurname;
        $this->img = $this->_user->img;
//        $this-> file = $this->_user-> file;
        parent::init();
    }

    public function rules()
    {
        return [
            ['email', 'required'],
            ['email', 'email'],
            [
                'email',
                'unique',
                'targetClass' => User::className(),
                'message' => Yii::t('app', 'ERROR_EMAIL_EXISTS'),
                'filter' => ['<>', 'id', $this->_user->id],
            ],
            [['email','usersurname', 'username', 'img'], 'string', 'max' => 255],
            [['img'], 'file', 'extensions' => 'png, jpg, gif'],
            ['age', 'integer'],
        ];
    }

    public function update()
    {
        if ($this->validate()) {
            $user = $this->_user;
            $user->email = $this->email;
            $user->username = $this->username;
            $user-> age = $this -> age;
            $user-> img = $this -> img;
            return $user->save();
        } else {
            return false;
        }
    }

}

view

   
<div class="user-form">

            <?php $form = ActiveForm::begin(['options' => ['enctype' => 'multi-part/form-data']]); ?>

            <?= $form->field($model, 'email')->textInput(['maxlength' => true]) ?>
            <?= $form->field($model, 'username')->textInput(['maxlength' => true]) ?>
            <?= $form->field($model, 'age')->textInput() ?>
            <?= $form->field($model, 'img')->widget(FileInput::classname(), [
                'options' => ['accept'=>'image/*'],
                'pluginOptions'=>[
                    'uploadUrl' => Url::to(['/uploads']),
                    'allowedFileExtensions'=>['jpg', 'gif', 'png'],
                    'showUpload' => true,
                    'initialPreview' => [
    //                    $model-> img ? Html::img($model-> img) : null, // checks the models to display the preview
                    ],
                    'overwriteInitial' => false,
                ],
            ]); ?>

            <div class="form-group">
                <?= Html::submitButton(Yii::t('app', 'BUTTON_SAVE'), ['class' => 'btn siteColor']) ?>
            </div>

            <?php ActiveForm::end(); ?>

        </div>

вываливается ошибка SyntaxError: Unexpected token < in JSON at position 0.

Ответить

 

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

Добавьте экшен 'upload' в actions() контроллера.

Ответить

 

Новичек

Действие происходит в update.php соответственно в контроллере в public function actionUpdate и если добавить actionUpload в контроллер, то это будет означать вью форму upload а не update. Ошибка возникает при сохранение самого файла.
в контроллере пробовал так

public function actionUpdate()
{
    $user = $this->findModel();
    $model = new ProfileUpdateForm($user);
    if ($model->load(Yii::$app->request->post()) && $model->update()) {
        $imageName = rand(1000,100000);
        $model->file = UploadedFile::getInstance($model, 'file');

        $model->img =''.$imageName.'.'.$model->file->extension; //тут возникает ошибка Trying to get property of non-object
        $model->file->saveAs('uploads/'.$imageName.'.'.$model->file->extension);
        $model->file = null;
        $model->save();
        return $this->redirect(['index']);
    } else {
        return $this->render('update', [
            'model' => $model,
        ]);
    }
}

тут я эту проблему расскрыл полностью.

Буду очень признателен за помощь.

Ответить

 

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

Переместите этот код в ProfileUpdateForm.

Ответить

 

Ильдар

Здравствуйте Дмитрий. Есть небольшая проблема. Русский текст не записывается в БД. Хотя в конфиге стоит

'language' => 'ru-RU'

и если заполняю поля на русском срабатывает условие else из контроллера то есть

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

но если пишу на английском то все нормально сохраняется. Не подскажете как это можно исправить?

Ответить

 

Ильдар

Оказалось из-за этого условия

[['username', 'usersurname'], 'match', 'pattern' => '#^[\w_-]+$#i'],

Ответить

 

Алексей – zlaksudbi.ru

И чем это лучше заменить? Или можно вообще убрать это условие?

Ответить

 

Ильдар

Я убрал и у меня заработало, может это и неправильно но других идей не было

Ответить

 

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

Добавить модификатор юникода в регулярное выражение.

Ответить

 

Святослав

Спасибо, за проделаную работу. Очень приятно почитать с утра

Ответить

 

Arman

1. А что если просто наследовать новые формы от главной модели?
Например, форма регистрации наследуется от модели User, переназначает/дополняет метод rules(), реализовывает свои доп. методы и в конце нам нужно только вызвать $this->save(), если конечено в экшне указали, например, $form->signup();

2. Зачем файла называть в верблюжьем формате? Намного удобнее password-change.php

3. Хорошо бы еще подтвердить, что новая почта моя, одна опечатка и прощай акк.

Ответить

 

Алексей

Я сделал в качестве логина для входа почту. Теперь нужно сделать возможность для пользователя ее смены на другую. Как это правильно делать? Мне нужно еще добавлять поля в базу для токена новой почты? Нужно ли отправлять токен на старую почту?

Ответить

 

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

Поле для новой почты и поле для токена.

Ответить

 

Алексей

Когда на почту приходит уведомление о комментариях с этого сайта, вместо названия всегда написано www-data.

Ответить

 

Alex All

Спасибо за статью, хотелось бы узнать как сделать чтобы для смены пароля отправлялась ссылка на e mail и только по приходу с нее можно было его изменить

Ответить

 

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

Так уже сделано восстановление пароля.

Ответить

 

Юрий

Как хранить и редактировать дату рождения?

Ответить

 

Алекс

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

    public function actionUpdate($id)
    {
        $model = $this->findModel($id);
        $profile = new ProfileForm($model);
        $model->scenario = ProfileForm::SCENARIO_DEFAULT;

А саму форму сделал вот так

namespace my\rbac\common\models;

use yii\base\Model;
use common\models\User;
 
/**
 * Password reset form
 */
class ProfileForm extends Model
{

    public $currentPassword;
    public $newPassword;
    public $newPasswordRepeat;
    public $password;
    public $username;
    public $email;
    public $role;
    /**
     * @var User
     */
    private $_user;
 
    const SCENARIO_PROFILE = 'profile';   

    /**
     * @param User $user
     * @param array $config
     */
    public function __construct(User $user, $config = [])
    {
        $this->_user = $user;
        $this->username = $user->username;
        $this->email = $user->email;
        $this->role = $user->role;
        parent::__construct($config);
    }
 
    public function scenarios()
    {
        return [
            self::SCENARIO_DEFAULT => ['password', 'username', 'role', 'email'],
            self::SCENARIO_PROFILE => ['currentPassword', 'newPassword', 'newPasswordRepeat', 'email'],
        ];
    }
    
    public function rules()
    {
        return [
            [['currentPassword', 'newPassword', 'newPasswordRepeat', 'username'], 'required'],
            ['currentPassword', 'currentPassword'],
            [['newPassword', 'password'], 'string', 'min' => 6],
            ['newPasswordRepeat', 'compare', 'compareAttribute' => 'newPassword'],

            ['username', 'match', 'pattern' => '#^[\w_-]+$#i'],
            ['username', 'unique', 'targetClass' => '\common\models\User', 'message' => 'ERROR_USERNAME_EXISTS','filter' => ['<>', 'id', $this->_user->id]],
            ['username', 'string', 'min' => 2, 'max' => 255],
 
            ['email', 'required'],
            ['email', 'email'],
            ['email', 'unique', 'targetClass' => '\common\models\User', 'message' => 'ERROR_EMAIL_EXISTS','filter' => ['<>', 'id', $this->_user->id]],
            ['email', 'string', 'max' => 255],
            
            ['role', 'string', 'max' => 64],
        ];
    }
 
    public function attributeLabels()
    {
        return [
            'newPassword' => 'New Password',
            'currentPassword' => 'Current Password',
            'newPasswordRepeat' => 'New Password Repeat',
        ];
    }
    
    /**
     * @param string $attribute
     * @param array $params
     */
    public function currentPassword($attribute, $params)
    {
        if (!$this->hasErrors()) {
            if (!$this->_user->validatePassword($this->$attribute)) {
                $this->addError($attribute, 'ERROR_WRONG_CURRENT_PASSWORD');
            }
        }
    }
 
    /**
     * @return boolean
     */
    
    public function updateUser()
    {
        if ($this->validate()) {
            $user = $this->_user;
            $user->username = $this->username;
            $user->email = $this->email;
            $user->role = $this->role;
            $user->setPassword($this->password);
            return $user->save();
        } else {
            return false;
        }
    }
}

И вроде все работает? старые данные выводятся при редактировании и новые добавляться. Меня смущает два момента. 1) Валидация на уникальность НЕ срабатывает, точнее срабатывает но в этом месте

 public function updateUser()
    {
        if ($this->validate()) { 

И я получается нечего не вижу что по сути не было сохранения.
и второй момент Для того чтобы передать данные форму я в конструкторе указал

        $this->username = $user->username;
        $this->email = $user->email;
        $this->role = $user->role;

А сами эти переменные публичные, так как приватные не будут работать в форме

            <?= $form->field($profile, 'username')->textInput(['maxlength' => true]) ?>
            <?= $form->field($profile, 'email')->textInput(['maxlength' => true]) ?>
            <?= $form->field($profile, 'role')->dropDownList(yii\helpers\ArrayHelper::map($roles, 'name', 'name')); ?>
            <?= $form->field($profile, 'password')->textInput(['maxlength' => true, 'id'=>'random']); ?>

Скажите я все правильно сделал или это какаято муть и компот)))? спасибо

Ответить

 

Прооксиус Сваагали

Спасибо, полезный материал

Ответить

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

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


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





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