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

Общие вопросы по использованию фреймворка. Если не знаете как что-то сделать и это про Yii, вам сюда.
Ответить
Аватара пользователя
XAKEPEHOK
Сообщения: 38
Зарегистрирован: 2012.08.16, 13:11
Откуда: КМВ
Контактная информация:

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

Сообщение XAKEPEHOK »

Работаю на 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);
и все нормально работает, только вот я считаю, что я чего-то недопонимаю.

Пример не реальный. Он лишь иллюстрирует проблему, когда одна модель может редактироваться из нескольких разных мест, в каждом из которых можно изменять только определенные значения
А как поступаете вы?
Последний раз редактировалось XAKEPEHOK 2014.02.17, 13:21, всего редактировалось 1 раз.
Большинство неправильных шагов совершаются стоя на месте
Аватара пользователя
XAKEPEHOK
Сообщения: 38
Зарегистрирован: 2012.08.16, 13:11
Откуда: КМВ
Контактная информация:

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

Сообщение XAKEPEHOK »

В общем, я видимо все правильно понимаю. Решил следующим образом:

Переопределил ActiveRecord

Код: Выделить всё

abstract class ActiveRecord extends CActiveRecord {

  private $_settedFields = [];

  
    /**
   * Метод для массового присваивания атрибутов, который присваивает только атрибуты, заданные в $names
   * @param array $values ассоциативны массив attributeName => value
   * @param array|string $names массив со списком атрибутов, которые надо присвоить
   */
  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];
      $this->_settedFields[$name] = $name;
    }
  }

  /**
   * Валидация только присвоенных атрибутов
   * @param bool $clearErrors
   * @return bool
   */
  public function validateSetted($clearErrors=true)
  {
    return parent::validate($this->_settedFields,$clearErrors);
  }

  /**
   * Валидация и сохранение присвоенных атрибутов
   * @return bool
   */
  public function saveSetted()
  {
    if ($this->validateSetted(true)) {
      $this->save(false);
      return true;
    }
    return false;
  }

} 
теперь в rules параметры on и except указываю крайне редко. Сохранение данных в контроллере выглядит примерно так:

Код: Выделить всё

$model->setAttributesByNames($_POST['User'], array('login', 'email'));
$model->saveSetted();
или так

Код: Выделить всё

$model->setAttributesByNames($_POST['User'], array('login', 'email'));
$model->password = CPasswordHelper::hashPassword($_POST['password']);
$model->saveSetted();
Большинство неправильных шагов совершаются стоя на месте
Аватара пользователя
samdark
Администратор
Сообщения: 9489
Зарегистрирован: 2009.04.02, 13:46
Откуда: Воронеж
Контактная информация:

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

Сообщение samdark »

Кучи rules можно избежать если вынести разные сценарии использования в отдельные модели форм. Посмотрите advanced application в Yii2. Это не всегда оправдано, но именно в этой ситуации может спасти.
Аватара пользователя
XAKEPEHOK
Сообщения: 38
Зарегистрирован: 2012.08.16, 13:11
Откуда: КМВ
Контактная информация:

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

Сообщение XAKEPEHOK »

Разбиение на модели форм действительно помогает, но не во всех случаях. У меня в некоторых моделях оно внесет только лишнюю путаницу, в то время как в других наоборот все упрощает. В общем спасибо вам за ответ. Я уже было думал, что понимаю сценарии и правила валидации не верно. Буду использовать своё решение с расширением AR и ждать Yii2
Большинство неправильных шагов совершаются стоя на месте
Аватара пользователя
XAKEPEHOK
Сообщения: 38
Зарегистрирован: 2012.08.16, 13:11
Откуда: КМВ
Контактная информация:

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

Сообщение XAKEPEHOK »

Александр, очень интересно ваше мнение по поводу такого решения. Идея позаимствована из Yii2

Расширяем 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()
Большинство неправильных шагов совершаются стоя на месте
Ответить