Динамическая валидация в CActiveRecord

Иногда бывает так, что одну и ту же модель нужно валидировать по разному в зависимости от значений некоторых полей. Выдумаем такой пример - есть модель Worker у которой есть поля опыт и размер зарплаты.

class Worker extends CModel {  
    public $experience;
    public $salary;

    public function attributeNames()
    {
        return ['experience', 'salary'];
    }
}

Теперь есть задача валидировать размер зарплаты в зависимости от размера опыта. Если опыт от 0 до 5, то границы зарплаты от 1000 до 5000. Если опыт больше 5, то границы зарплаты от 5000 до 10000. В простом случае (если бы поля не зависели друг от друга) мы бы написали простые правила для валидации:

public function rules()  
{
    return [
        ['experience, salary', 'required'],
        ['salary', 'numerical', 'integerOnly' => true, 'min' => 1000, 'max' => 10000],
    ];
}

В нашем случае напрашивается такое решение:

public function rules()  
{
    $minSalary = 1000;
    $maxSalary = 5000;

    if ($this->experience > 5) {
        $minSalary = 5000;
        $maxSalary = 10000;
    }

    return [
        ['experience, salary', 'required'],
        ['salary', 'numerical', 'integerOnly' => true, 'min' => $minSalary, 'max' => $maxSalary],
    ];
}

Но работать это будет только когда вы напрямую присваиваете значения атрибутам не пользуясь для этого свойством attributes. А, например, в таком случаем ваше правило отработает неправильно:

$worker = new Worker();
$worker->attributes = [
    'experience' => 8,
    'salary' => 6000,
];

$worker->validate();

Проблема в том, что метод rules вызывается только один раз, чтобы создать список валидаторов и затем всегда используется уже сгенерированный список генераторов который хранится в приватном поле $_validators. А в приведенном примере список валидаторов был сгенерирован когда происходило присвоение атрибутов модели - он нужен чтобы узнать какие атрибуты разрешены для заполнения.

Первое же простое решение которое приходит на ум - это всегда генерировать список валидаторов перед выполнением валидации. Несмотря на то что поле $_validators приватное, манипулировать списком валидаторов все таки можно:

public function validate($attributes = null, $clearErrors = true)  
{
    $vl = $this->getValidatorList();
    $vl->clear();
    $vl->mergeWith($this->createValidators());

    return parent::validate($attributes, $clearErrors);
}

Мы переопределили метод validate в нашей модели и теперь при каждой валидации создаем список валидаторов. Единственный минус этого решения - страдает производительность.

Другой способ, на мой взгляд более правильный, это написать собственный inline валидатор:

public function rules()  
{
    return [
        ['experience, salary', 'required'],
        ['salary', 'numerical', 'integerOnly' => true],
        ['salary', 'salaryValidator'],
    ];
}

public function salaryValidator($attribute)  
{
    $minSalary = 1000;
    $maxSalary = 5000;

    if ($this->experience > 5) {
        $minSalary = 5000;
        $maxSalary = 10000;
    }

    if ($this->$attribute < $minSalary) {
        $this->addError($attribute, '...');
    }

    if ($this->$attribute > $maxSalary) {
        $this->addError($attribute, '...');
    }
}

Таким образом не происходит постоянной перегенерации всего списка валидаторов и логика по валидации зарплаты вынесена в отдельный метод и не замусоривает метод rules.