T
T
Timur2014-02-17 00:01:17
Yii
Timur, 2014-02-17 00:01:17

Yii: rules and a lot of scenarios in which the devil will break his leg?

I've been working on yii for more than a year, but today I finally realized that I don't understand something, and I'm doing something wrong.
Situation example: There is a User model.
Each user (User) can change personal information, email and password in separate actions, respectively. Changing the password and email requires entering the user's current password, while changing personal information does not. There is also an administrator who can change any user information without entering the current password. And there are moderators who can only change personal information and email (without entering the current password), but they cannot change the user's password.
Usually, for each action, I use a separate script, for which I define the rules in the model. But in the situation described above, complete hell is obtained. An example is even scary to give:

class User extends CActiveRecord {

  public $currentPassword;
  public $retypePassword;

public function rules()
  {
    return array(
      array('login''required''on'=>'registration, updateByAdmin'),
      array('login''length''max' => 80'on'=>'registration, updateByAdmin'),
      array('login''unique''on'=>'registration, updateByAdmin'),
      array('login''match''pattern' => '~^[\da-zа-яёА-ЯЁ\.\[email protected]_\+]+$~i''on'=>'registration, updateByAdmin'),

      array('phone''length''max' => 12),
      array('icq''length''max' => 9),

      array('email''required''on'=>'registration, updateByAdmin, updateByModerator, changeEmail'),
      array('email''length''max' => 200'on'=>'registration, updateByAdmin, updateByModerator, changeEmail'),
      array('email''unique''on'=>'registration, updateByAdmin,  updateByModerator, changeEmail'),
      array('email''email''on'=>'registration, updateByAdmin,  updateByModerator, changeEmail'),

      array('password''required''on'=>'registration, updateByAdmin, changePassword'),
      array('currentPassword''required''on'=>'changePassword, changeEmail'),
      array('currentPassword''passwordValidator''on'=>'changePassword, changeEmail'),
      array('retypePassword''required''on'=>'registration, updateByAdmin, changePassword'),
      array('retypePassword''compare''compareAttribute' => 'password''on'=>'registration, updateByAdmin, changePassword,')
    );
  }
}

but it's even scarier to imagine what will happen if we add ajax validation and captcha to some actions. The problem with captcha and ajax has been known for a long time, of course it can be solved by adding CCaptchaAction, but if we solve it with scripts, we will get real hell (the example is exaggerated):
      array('email''required''on'=>'registration, updateByAdmin, updateByModerator, changeEmail, ajaxRegistration, ajaxUpdateByAdmin, ajaxUpdateByModerator, ajaxChangeEmail'),
 
and so on for each rule
. So, how to be in situations where one model can be edited from many different actions, and when you need to save only certain fields using $model->attributes = $_POST['User']? I really want to do this straight from the controller actions, removing on and except from rules () almost everywhere.
The use of $model->save(true,['password','login',...]) is frustrating because the values ​​of the fields assigned in beforeSave() are not saved.
You can again remove on and except, write in rules() for the desired scenario an unsafe validator for fields that do not need to be changed, but then there will be a problem with validating model fields that are not in the view (for example, when changing the password, array ('login ', 'required')).
Currently I am doing the following: Adding a method to the model
public function setAttributesByNames($values,$names)
  {
    if(!is_array($values)) return;
    if(!is_array($names)) $names = explode(',',$names);
    foreach($names as $name) if(isset($values[$name])) $this->$name = $values[$name];
  }

Or by removing on and except I use constructions of the form
$model->setAttributesByNames($_POST['User'],array('login,email'));
if ($model->validate(array('login,email'))) $model->save(false);
and everything works fine, only now I think that I misunderstood something.
How are you doing?

Answer the question

In order to leave comments, you need to log in

6 answer(s)
T
Timur, 2014-02-17
@XAKEPEHOK

New solution: separate scripts from validation rules. Solved all my problems at once
Expanded ActiveRecord

class ActiveRecord extends CActiveRecord {
  protected $modelRules = [];

  ...

  /**
   * @return array общие (базовые для модели) правила валидации, описанные в формате
   * array(
   *   'login' => array(
   *     ['required'],
   *     ['length', 'max' => 200]
   *   ),
   *   'firstName' => array(
   *     ['required'],
   *     ['length', 'max' => 200]
   *   ),
   * )
   */
  public function baseRules()
  {
    return array();
  }

  /**
   * @return array список сценариев с установленными правилами валидации для каждого сценария.
   * Правила валидации берутся из массива @see baseRules()
   * 'createUser' => ['login','firstName']
   * Существует возможность задать индивидуальные правила валидации для отдельного поля в заданном сценарии. Например:
   * 'createUser' => array(
   *   'login',
   *   'firstName' => array(
   *     '*', //можем унаследовать правила из @see baseRules()
   *     ['!required'], //можем удалить валидатор "required", указанный в @see baseRules() при наследовании правил
   *     ['in', 'range' => array('Alex','Jack','Sam','Jane')], //и добавляем новое правило
   *   ),
   * )
   */
  public function scenarioRules()
  {
    return array();
  }

  /**
   * Формирует валидатор в соотстветствии с требованиями Yii
   * @param $field string поле модели
   * @param $validator array массив с параметрами валидатора
   * @param $scenario string сценарий
   */
  protected function addBaseRule($field,$validator,$scenario)
  {
    $validatorName = [];
    preg_match('/^(!)?([^!]+)/',$validator[0],$validatorName);
    $validator[0] = $validatorName[2];
    $ruleKey = $field.'_'.$validator[0].'_'.$scenario;

    if (empty($validatorName[1])) {
      if (!empty($scenario)) $validator['on'] = $scenario;
      $validator[1] = $validator[0];
      $validator[0] = $field;
      $this->modelRules[$ruleKey] = $validator;
    } else unset($this->modelRules[$ruleKey]);
  }

  /**
   * Формирует валидатор в соотстветствии с требованиями Yii из массива @see baseRules()
   * @param $field string поле модели
   * @param $scenario string сценарий
   */
  protected function addBaseRules($field,$scenario)
  {
    $baseRules = $this->baseRules();
    if (isset($baseRules[$field])) foreach($baseRules[$field] as $validator) $this->addBaseRule($field,$validator,$scenario);
  }

  public function rules()
  {
    $this->modelRules = [];
    $scenarioRules = $this->scenarioRules();
    if (empty($scenarioRules)) $scenarioRules = [''];
    foreach ($scenarioRules as $scenario => $rules) {
      //Если сценариев нет, то устанавливаем общие для всех правила
      if ($scenario == 0 && empty($rules)) $rules = array_keys($this->baseRules());
      foreach ($rules as $field => $rule) {
        //Если в сценариях заданы правила
        if (is_array($rule)) {
          //Добавляем родительские правила, если есть «*»
          if (in_array('*',$rule)) $this->addBaseRules($field,$scenario);
          foreach ($rule as $validator) {
            //Проверяем, что правило не является маской. Т.е. «*»
            if (is_array($validator)) $this->addBaseRule($field,$validator,$scenario);
          }
        } else $this->addBaseRules($rule,$scenario);
      }
    }
    foreach ($this->modelRules as &$rule) uksort($rule,function($a,$b){
      if (is_numeric($a) && is_numeric($b) && $a>$b) return 1;
      return is_numeric($a) ? -1 : 1;
    });
    return $this->modelRules;
  }

As a result, I got such a User model
class User extends CActiveRecord {

  public $currentPassword;
  public $retypePassword;

  pulbic function baseRules()
  {
    return array(
      'login' => array(
        array('required')
        array('length', 'max' => 80),
        array('unique'),
        array('match', 'pattern' => '~^[\da-zа-яёА-ЯЁ\.\[email protected]_\+]+$~i'),
      ),
      'email' => array(
        array('length', 'max' => 200),
        array('unique'),
        array('email'),
        array('required'),
      ),
      'password' => array(
        array('required'),
      ),
      'currentPassword' => array(
        array('required'),
        array('passwordValidator'),
      ),
      'retypePassword' => array(
        array('required'),
        array('compare','compareAttribute' => 'password'),
      ),
      'phone' => array(
        array('length', 'max' => 12),
      ),
      'icq' => array(
        array('length', 'max' => 9),
      ),
    );
  }

  pulbic function scenarios()
  {
    return array(
      'registration' => array('login','email','icq','phone'),
      'updateByAdmin' => array(
        'login',
        'email',
        'icq',
        'phone',
        'password' => array(
          array('default'),
        )
      ),
      'updateByModerator' => array('email','icq','phone'),
      'changeEmail' => array('email','currentPassword'),
      'changePassword' => array('password','retypePassword','currentPassword'),
    );
  }
}

Extended AR generates a typical Yii rules array using data from the scenarios() and baseRules() methods

Y
Yuri Morozov, 2014-02-17
@metamorph

It is not necessary to fence the entire garden in one model.
For example, all gestures with an email/password (change, reminder, etc.) fit perfectly into the usual model form.
I haven't looked in detail, but in some places it seems that you are prescribing some fields for ALL scenarios. It's not obligatory.
In general, the problem of scripting hell is well solved in Yii2 by separating the actual validation rules from the validation scripts. On the other hand, yii2 application templates unequivocally hint that it's easier to split such things into separate forms anyway :)

E
Egor Mokeev, 2014-02-20
@zetamen

You can arrange the rules by scenarios, in your example it would look like this:

public function rules()
{
  return array(
    array('phone', 'length', 'max' => 12),
    array('icq', 'length', 'max' => 9),
    array('email', 'email'),
    array('login', 'length', 'max' => 80),
    array('email', 'length', 'max' => 200),
    //registration
    array('login, email, password, retypePassword', 'required', 'on'=>'registration'),
    array('login, email', 'unique', 'on'=>'registration'),
    array('login', 'match', 'pattern' => '~^[\da-zа-яёА-ЯЁ\.\[email protected]_\+]+$~i', 'on'=>'registration'),
    array('retypePassword', 'compare', 'compareAttribute' => 'password', 'on'=>'registration')
    //updateByAdmin
    array('login, email, password, retypePassword', 'required', 'on'=>'updateByAdmin'),
    array('login, email', 'unique', 'on'=>'updateByAdmin'),
    array('login', 'match', 'pattern' => '~^[\da-zа-яёА-ЯЁ\.\[email protected]_\+]+$~i', 'on'=>'updateByAdmin'),
    array('retypePassword', 'compare', 'compareAttribute' => 'password', 'on'=>'updateByAdmin')
    //updateByModerator
    array('email', 'required', 'on'=>'updateByAdmin'),
    array('login', 'match', 'pattern' => '~^[\da-zа-яёА-ЯЁ\.\[email protected]_\+]+$~i', 'on'=>'updateByAdmin'),
    array('email', 'unique', 'on'=>'updateByAdmin'),
    //changePassword
    array('password, currentPassword, retypePassword', 'required', 'on'=>'changePassword'),
    array('currentPassword', 'passwordValidator', 'on'=>'changePassword'),
    array('retypePassword', 'compare', 'compareAttribute' => 'password', 'on'=>'changePassword')
    //changeEmail
    array('email, currentPassword', 'required', 'on'=>'changeEmail'),
    array('email', 'unique', 'on'=>'changeEmail'),
    array('currentPassword', 'passwordValidator', 'on'=>'changeEmail'),
  );
}

What is the point of all this? The code, in comparison with your example, has become more, but its readability has also increased.

I
Ivan Karabadzhak, 2014-02-17
@Jakeroid

I use normal model validation for this kind of thing, not ActiveRecord. For example, for the administrator AdministratorUserEditModel. It contains all the rules for the administrator. Same for moderator and regular user. It turns out that you take out unnecessary checks in other classes and, as a result, clog the model code less.

_
_ _, 2014-02-17
@AMar4enko

In addition to on, there is also except, to exclude single scenarios
PS Well, in general, there is definitely no point in dragging this crap directly into ActiveRecord, just littering. I would take out this validation in behaviors, and in the behaviors method I would skip unnecessary ones based on the necessary scenarios.

R
Rowdy Ro, 2014-02-17
@rowdyro

In your case, you can try to separate the logic into behaviors, or simply make a base class of the User model (in which you write rules and behavior for all users) and inherit from it like AdminUser extends User, Moderator extends User, etc.
You can even make a factory, for example, according to the required model was instantiated to a certain field in the database.

Didn't find what you were looking for?

Ask your question

Ask a Question

731 491 924 answers to any question