Новое решение: отделить сценарии от правил валидации. Решил разом все свои проблемы
Расширил 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;
}
В итоге получил такую модель User
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а-яёА-ЯЁ\.\-@_\+]+$~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'),
);
}
}
Расширенный AR формирует типичный для Yii массив rules, используя данные из методов scenarios() и baseRules()