Answer the question
In order to leave comments, you need to log in
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,')
);
}
}
array('email', 'required', 'on'=>'registration, updateByAdmin, updateByModerator, changeEmail, ajaxRegistration, ajaxUpdateByAdmin, ajaxUpdateByModerator, ajaxChangeEmail'),
and so on for each rule 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];
}
$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. Answer the question
In order to leave comments, you need to log in
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;
}
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'),
);
}
}
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 :)
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'),
);
}
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.
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.
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 questionAsk a Question
731 491 924 answers to any question