Search code examples
phpdynamicyii2rules

Yii2 add rule based on condition


I am building a website using PHP Yii2 Framework and dektrium\yii2-user extension for user authentication.

I want to ask the user for a captcha if the number of unsuccessful logins is over three, but by default the extension doesn't support this.

Now, I have overridden the extension's User and LoginForm model, and added the fields and checks required. However, I cannot figure out how to add a rule to make the captcha required only from the fourth attempt.

Is it possible to add rules dynamically? I have shown a simplified code view below and my comments where I need help. I will write the functions, just need help with the commented part.

<?php

namespace app\models\dektrium\user;


class LoginForm extends \dektrium\user\models\LoginForm
{
    public $captcha;
    public $need_captcha;

    public function rules() {
        $rules = parent::rules();

        //This is how you'd normally add a rule, but this will require it for every login
        //The following rule should be added from the login()
        $rules[] = ['captcha', 'captcha', 'message' => 'Too many attempts. Captcha required.'];
        $rules[] = ['need_captcha', 'boolean'];

        return $rules;
    }

    public function login() {
        $success = false;
        $requireCaptcha = false;

        if ($this->validate() && $this->user) {
            if ($this->user->login_attempts > 3) {
                //add rule here to require captcha

                $requireCaptcha = true;
            }

            $success = !$requireCaptcha && $this->validateCaptcha() && $this->validateLogin();

            if ($success) {
                $this->user->updateAttributes(['last_login_at' => time()]);
            }
        }

        return $success;
    }
}

?>

EDIT:

If there is an 'optional' parameter converse to 'required', that'd suffice to. I can check for the captcha within my login().

EDIT 2:

I tried to use scenarios as follows, but the model is not loading the captcha value in the controller action, before doing the validation.

<?php

namespace app\models\dektrium\user;


class LoginForm extends \dektrium\user\models\LoginForm
{
    public $captcha;
    public $need_captcha;
    public $login_count;

    public function rules() {
        $rules = parent::rules();

        $rules[] = ['captcha', 'captcha', 'message' => 'Too many attempts. Captcha required.', 'on' => ['required_captcha']];
        $rules[] = ['need_captcha', 'boolean'];
        $rules[] = ['login_count', 'integer'];

        return $rules;
    }

    public function login() {
        $this->user = $this->finder->findUserByUsernameOrEmail(trim($this->login));

        if($this->user && $this->user->login_count > 3) {
            $this->scenario = 'required_captcha';
            $this->need_captcha = true;
        }

        $success = parent::login();

        if ($success) {
            $this->user->login_count = 0;
            $this->user->save();
        } else {
            $this->login_count++;

            if ($this->user) {
                $this->user->login_count++;
                $this->user->save();
            }
        }

        if ($this->login_count > 3) {
            $this->scenario = 'required_captcha';
            $this->need_captcha = true;
        }

        return $success;
    }
}

Solution

  • So here's how I ended up doing this...

    I used a scenario based rule and in the controller action, I set the scenario when the condition for which I needed captcha was true. In reality, the controller was also of any extension, so I had to do some supported controller mapping and set the scenario via an event.

    My first attempts at doing this failed since I was setting the scenario during the validation function, but should have probably set it prior to doing so, where it worked.

    <?php
    
    namespace app\models\dektrium\user;
    
    
    class LoginForm extends \dektrium\user\models\LoginForm
    {
        public $captcha;
        public $need_captcha;
        public $login_count;
    
        public function rules() {
            $rules = parent::rules();
    
            $rules[] = ['captcha', 'required', 'on' => ['use_captcha']];
            $rules[] = ['captcha', 'captcha', 'on' => ['use_captcha']];
            $rules[] = ['need_captcha', 'boolean'];
            $rules[] = ['login_count', 'integer'];
    
            return $rules;
        }
    
        public function login() {
            $success = true;
            $this->user = $this->finder->findUserByUsernameOrEmail(trim($this->login));
    
            if(!$this->need_captcha && $this->user && $this->user->login_count > 3) {
                $this->need_captcha = true;
                $success = false;
            }
    
            $success = $success && parent::login();
    
            if ($success) {
                $this->user->login_count = 0;
                $this->user->save();
            } else {
                $this->login_count++;
    
                if ($this->user) {
                    $this->user->login_count++;
                    $this->user->save();
                }
    
                if ($this->login_count > 2)
                    $this->need_captcha = true;
            }
    
            return $success;
        }
    }