skip to Main Content

I’m using the SettingsForm model from Dektrium in my Yii web app for user profile settings. In this model, there’s a requirement to input the current password (current_password) whenever a user wants to update their profile. However, I want to make current_password required only if the user intends to change their password (inserting a new value in the "new_password" field; otherwise, it shouldn’t be mandatory for other profile changes.
How can I achieve that without altering the library model SettingsForm?

Here’s a snippet of the relevant parts of my code:

 /**
     * SettingsForm gets user's username, email and password and changes them.
     *
     * @property User $user
     *
     * @author Dmitry Erofeev <[email protected]>
     */
    class SettingsForm extends Model
    {
       /** @var string */
    public $new_password;

    /** @var string */
    public $current_password;

    /** @inheritdoc */
    public function rules()
    {
        return [
            'newPasswordLength' => ['new_password', 'string', 'max' => 72, 'min' => 6],
            'currentPasswordRequired' => ['current_password', 'required'],
            'currentPasswordValidate' => ['current_password', function ($attr) {
                if (!Password::validate($this->$attr, $this->user->password_hash)) {
                    $this->addError($attr, Yii::t('user', 'Current password is not valid'));
                }
            }],
        ];
    }

This is the action in the controller

public function actionAccount()
{
    /** @var SettingsForm $model */
    $model = Yii::createObject(SettingsForm::className());
    $profile = Profile::find()->where(['user_id' => Yii::$app->user->identity->getId()])->one();
    $event = $this->getFormEvent($model);

    $this->performAjaxValidation($model);

    $old_attachs = [];
    $files_preview = [];
    if ($profile->gravatar_id) {
        $old_attachs = $profile->getImageUrl();
        $files_preview = $profile->getImagePreview();
    }

    $this->trigger(self::EVENT_BEFORE_ACCOUNT_UPDATE, $event);
    if ($model->load(Yii::$app->request->post()) && $model->save()) {
        Yii::$app->session->setFlash('success', Yii::t('user', 'Your account details have been updated'));
        $this->trigger(self::EVENT_AFTER_ACCOUNT_UPDATE, $event);
        if ($profile->load(Yii::$app->request->post())) $profile->save();
        $profile->avatarFile = UploadedFile::getInstance($profile, 'avatarFile');
        $profile->upload();

        return $this->refresh();
    }

    return $this->render('@app/views/my-settings/account', [
        'model' => $model,
        'profile' => $profile,
        'old_attachs' => $old_attachs,
        'files_preview' => $files_preview,
    ]);
}

And finally the fields inside the view are

<?php echo $form->field($model, 'new_password')->passwordInput() ?>

<?php echo $form->field($model, 'current_password')->passwordInput() ?>

2

Answers


  1. It can be made using conditional validation Yii docs:

    ['old_password', 'required', 'when' => function($model) {
        return $model->new_password != '';
    }]
    
    Login or Signup to reply.
  2. I can think of two ways to achieve that without directly modifying the 3rd party code.

    1. Extend the SettingsForm Model – the pretty way

    You can extend the library’s SettingsForm model, override its rules() method and tell DI container to use your model instead of library’s one.

    Your model can look like this:

    namespace appmodels;
    
    use dektriumusermodelsSettingsForm as SettingsFormBase;
    
    class SettingsForm extends SettingsFormBase
    {
        public function rules()
        {
            $rules = parent::rules();
            // add when to currentPasswordRequired rule
            $rules['currentPasswordRequired']['when'] = fn() => !empty($this->new_password);
            // we also want to skip currentPasswordValidate when it's empty
            $rules['currentPasswordValidate']['skipOnEmpty'] = true;
            return $rules;
        }
    }
    

    Now we need to tell DI container to use our model when it’s looking for dektriumusermodelsSettingsForm. We can do that in config files. For example in config/web.php:

    $config = [
        'container' => [
            'definitions' => [
                dektriumusermodelsSettingsForm::class => appmodelsSettingsForm::class,
                // ... other definitions ...
            ],
        ],
        // ... other configurations ...
    ];
    

    Now whenever dektriumusermodelsSettingsForm instance is created using Yii::createObject(SettingsForm::className()); we will get instance of our appmodelsSettingsForm instead and our modified rules will be used for validation.

    2. Modify the existing rules on the instace – the ugly way

    Other option is to modify existing rules when the instance is already created. It’s a bit hacky and ugly but it might have it’s own use if you can’t use the first approach.

    First we will prepare method that will find and modify rules in the controller.

    private function modifySettingsFormRules(SettingsForm $model): void
    {
        foreach ($model->getValidators() as $validator) {
            // we are looking for RequiredValidator that checks current_password attribute
            if (
                $validator instanceof yiivalidatorsRequiredValidator
                && in_array('current_password', $validator->attributes)
            ) {
                $validator->when = fn ($model) => !empty($model->new_password);
                continue;
            }
            // we also want to modify currentPasswordValidate rule
            // which is instance of InlineValidator
            if (
                $validator instanceof yiivalidatorsInlineValidator
                && in_array('current_password', $validator->attributes)
            ) {
               $validator->skipOnEmpty = true;
            }
        }
    }
    

    When we have method to modify rules prepared we can call it in our controller’s action:

    public function actionAccount()
    {
        /** @var SettingsForm $model */
        $model = Yii::createObject(SettingsForm::className());
        $this->modifySettingsFormRules($model);
        // ... rest of action ...
    }
    

    Note:
    Namespaces and files used in my answer matches basic app template. If your structure is different modify them to match your structure.

    Note 2:
    Property when only affects server side validation. If you want to affect client side validation (before the form is submitted) you also need to use whenClient property.

    Note 3:
    Package dektrium/yii2-user has been abandoned 4 years ago. It might be a good idea to look for a replacement if possible.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search