Yii: rules и очень много сценариев, в которых чёрт ногу сломит?

Работаю на yii уже больше года, но сегодня окончательно понял, что я чего-то не понимаю, и что-то делаю не так.
Пример ситуации: Есть модель User.
Каждый пользователь (User) может менять личную информацию, email и пароль в отдельных action'ах соответственно. Смена пароля и email требует ввода текущего пароля пользователя, в то время как смена личной информации этого не требует. Есть еще администратор, который может менять любую информацию пользователя без ввода текущего пароля. А еще есть модераторы, которые могут менять только личную инфу и email (без ввода текущего пароля), но пароль пользователя они изменить не могут.

Обычно, для каждого действия я использую отдельный сценарий, для которого определяю rules в модели. Но в описанной выше ситуации получается полный ад. Пример даже страшно приводить:

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а-яёА-ЯЁ\.\-@_\+]+$~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,')
    );
  }
}


но еще более страшно представить что будет, если мы добавим ajax-валидацию и каптчу на некоторые действия. Проблема с каптчей и ajax известна давно, её конечно можно решить допиливанием CCaptchaAction, но если решать сценариями, то мы получим настоящий ад (пример утрирован):
      array('email', 'required', 'on'=>'registration, updateByAdmin, updateByModerator, changeEmail, ajaxRegistration, ajaxUpdateByAdmin, ajaxUpdateByModerator, ajaxChangeEmail'),
 
и так для каждого правила

Так вот, как быть в таких ситуациях, когда одна модель может редактироваться из множества разных action'ов, и когда нужно сохранять только определенные поля используя $model->attributes = $_POST['User']? Очень хочется делать это прямиком из действий контроллера, практически везде убрав on и except из rules().

Использование $model->save(true,['password','login',...]) огорчает тем, что не сохраняются значения полей, присвоенных в beforeSave().

Можно опять же убрать on и except, прописать в rules() для нужного сценария валидатор unsafe для полей, которые не надо изменять, но тогда возникнет проблема с валидацией полей модели, которых нет во view (например, при смене пароля сработает array('login', 'required')).

В настоящее время я делаю следующим образом: Добавляю в модель метод
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];
  }

Или убрав on и except использую конструкции вида
$model->setAttributesByNames($_POST['User'],array('login,email'));
if ($model->validate(array('login,email'))) $model->save(false);
и все нормально работает, только вот я считаю, что я чего-то недопонимаю.
А как поступаете вы?
  • Вопрос задан
  • 16352 просмотра
Решения вопроса 1
XAKEPEHOK
@XAKEPEHOK Автор вопроса
Новое решение: отделить сценарии от правил валидации. Решил разом все свои проблемы

Расширил 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()
Ответ написан
Комментировать
Пригласить эксперта
Ответы на вопрос 5
metamorph
@metamorph
Совершенно необязательно городить весь огород в одной модели.

К примеру, все телодвижения с емейлом/паролем (смена, напоминалка итд) отлично укладываются в обычную модельную форму.

Детально не смотрел, но местами кажется, что Вы прописываете некоторые поля для ВСЕХ сценариев. Это необязательно.

Вообще, проблема ада сценариев отлично решена в Yii2 разделением собственно правил валидации и сценариев валидации. С другой стороны, шаблоны приложений yii2 недвусмысленно намекают, что по отдельным формам такие штуки все равно разбивать проще :)
Ответ написан
zetamen
@zetamen
В поисках нового
Можно скомпоновать правила по сценариям, в вашем примере это выглядело бы так:
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а-яёА-ЯЁ\.\-@_\+]+$~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а-яёА-ЯЁ\.\-@_\+]+$~i', 'on'=>'updateByAdmin'),
		array('retypePassword', 'compare', 'compareAttribute' => 'password', 'on'=>'updateByAdmin')
		//updateByModerator
		array('email', 'required', 'on'=>'updateByAdmin'),
		array('login', 'match', 'pattern' => '~^[\da-zа-яёА-ЯЁ\.\-@_\+]+$~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'),
	);
}


В чем фишка всего этого? Кода, по сравнению с вашим примером, стало больше, но и его читаемость возросла.
Ответ написан
Я использую для подобных дел валидацию обычной модели, а не ActiveRecord. Например, для администратора AdministratorUserEditModel. В ней все правила для администратора. Так же для модератора и обычного пользователя. Получается что вы выносите лишнее проверки в другие классы и в итоге меньше засоряете код модели.
Ответ написан
Комментировать
AMar4enko
@AMar4enko
Помимо on есть еще except, для исключения единичных сценариев
P.S. Ну а в целом, тащить эту лабуду напрямую в ActiveRecord смысла точно никакого нет, только сорить. Я бы вынес эту валидацию в behaviors, и в методе behaviors пропускал ненужные отталкиваясь от нужных сценариев.
Ответ написан
Комментировать
@rowdyro
В вашем случае можно попробывать разнести логику по поведениям, либо просто сделать базовый класс модели User (в котором прописываете правила и поведение для всех пользователей) и наследоваться от него типа AdminUser extends User, Moderator extends User и тд
Можете даже фабрику сделать, чтобы например по определенному полю в бд инстанцировалась нужная модель.
Ответ написан
Комментировать
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Войти через центр авторизации
Похожие вопросы