В данном маленьком топике я хотел бы рассказать об одном очень простом рецепте (который многим из вас, вероятно знаком) в контексте фреймворка Yii. Речь идет о динамическом изменении правил валидации формы — когда правила валидации изменяются в зависимости от выбора пользователя вашего приложения, сделанного, например, путем выбора значения из списка или установленного чекбокса.
На применение данного решения меня натолкнула практическая необходимость при написании одного простенького Контакт-Центра с веб-интерфейсом. Чтоб не придумывать примеры, вкратце поясню применение данного рецепта на примере этого же КЦ. Как известно, основная деятельность операторов КЦ заключается в приеме звонков от клиентов и в исходящих звонках клиентам. По результатам звонка оператор должен занести в систему определенные данные — результат разговора с клиентом, на основании которого определяется статус тикета и дальнейшая логика работы с ним (назначение повторного звонка, любой другой процедуры, закрытие тикета и т.д.). Проблема заключается в том, что некоторые поля формы (читай — атрибуты модели) должны быть заполнены или незаполнены в зависимости от «родительского» статуса. Например, очевидно, что поле «Результат разговора с клиентом» не имеет смысла заполнять, если технический статус звонка — «Недозвон/Неверный номер» ( технический статус должен выставлять оператор, а не система, поскольку звонок может состояться, но получателем звонка ВДРУГ может оказаться не клиент — ошибка в номере и т.д. ). В то же время, если выставлен технический статус «Успешный дозвон» — поле «Результат разговора с клиентом» обязательно должно быть заполнено. Снова-таки: в зависимости от результата разговора может появиться необходимость в заполнении дополнительных полей — например поля «Дата повторного звонка» при статусе «Повторный звонок».
Естественно, я хотел бы, чтобы в зависимости от значения определенного поля формы менялись и правила валидации для всей формы. На практике эта задача очень проста в применении, но очень полезна в таких случаях, как мой.
Итак:
1. Создаем несколько сценариев для вадидации формы звонка.
Сначала создадим правила для тех полей, которые необходимы во всех сценариях:
public function rules(){
//...
array('call_status', 'required', 'on'=>'callSubmit, invalidNumberSubmit, validCallSubmit, successCallSubmit...' //...
'message'=>'Поле "{attribute}" не может быть пустым.'),
}
Думаю первое правило не вызывает вопросов — технический статус звонка — это значение верхнего уровня в форме — оно необходимо в любом сценарии.
Продолжим идти от общего к частному:
//.. Inside rules() method
array('talk_status', 'required', 'on'=>'successCallSubmit, secondCall,...') /* здесь список сценариев, связанных с успешным дозвоном и статусами разговора */
Далее, определяем правила для более частных случаев:
//.. Inside rules() method
array('new_call_date', 'required', 'on'=>'secondCall') /*дата повторного звнока при статусе "Повторный звонок"*/
Таким образом мы можем перебрать все необходимые правила валидации — как в общих случаях, так и в каждом частном. Дело остается за малым: сделать так, чтобы правила валидации менялись в зависимости от выбранных значений в форме. Как вы уже наверное догадались, для этого мы будем использовать CActiveForm, так как этот виджет из коробки позволяет просто и ясно проводить ajax-валидацию.
Создадим для примера простенькую форму:
<?php $form=$this->beginWidget('CActiveForm', array(
'id'=>'call-submit-form',
'enableAjaxValidation'=>true,
'clientOptions'=>array(
'validateOnChange'=>true,
'validateOnSubmit'=>true
),
//всяческие настройки виджета
)); ?>
Из интересных нам опций необходимо отметить такие: 'enableAjaxValidation', 'validateOnChange','validateOnSubmit'. Полагаю, предназначение этих опций не вызывает вопросов и понятно из их названий. Продолжим:
<div class='control-group'>
<?php echo $form->labelEx($model,'call_status'); ?>
<?php echo $form->dropDownList($model,'call_status',
CHtml::listData( CallStatuses::model()->findAll(),'id','title' ) ); ?>
<?php echo $form->labelEx($model,'talk_status'); ?>
<?php echo $form->dropDownList($model,'talk_status',
CHtml::listData(TalkStatuses::model()->findAll(),'id','title' ), )); ?>
<?php echo $form->error($model,'call_status'); ?>
<?php echo $form->error($model,'talk_status'); ?>
</div>
<div class='control-group'>
<?php echo $form->label($model,'new_call_date'); ?>
<?php //Поле выбора даты повторного звонка
$this->widget('zii.widgets.jui.CJuiDatePicker', array(
'attribute'=>'new_call_date',
'model'=>$model,
//...
),
));
?>
</div>
Не буду утомлять вас большим количеством полей, думаю трех и так достаточно для примера. Все, что нам осталось сделать, — это собственно реализовать функционал для динамического изменения правил валидации (читай — сценария).
На самом деле, в основе этого функционала лежит простейшая логика: нужно каждому значению поля сопоставить сценарий, по которому будет валидироваться форма после выбора этого значения (в данном случае при выборе значения из списка, созданного методом CActiveForm::dropDownList() ). Это очень простое решение, и я не стану зацикливаться на деталях и создавать реализацию в ООП-манере, ведь главное донести идею.
Вместо этого я для простоты помещу соответствующий код прямо в метод performAjaxValidation() класса контроллера. Ниже немного измененный код стандартного метода performAjaxValidation() и метода, который принимает форму:
//Inside controller
public function actionSaveCall(){
/*Изначально создаем модель со сценарием 'callSubmit', который считает необходимым всего одно поле - 'call_status' - технический статус звонка. */
$model=new Calls('callSubmit');
$this->performCallAjaxValidation($model);
if( isset($_POST['Calls']) ) {
$model->attributes = $_POST['Calls'];
if( $model->save() )
$this->redirect( Yii::app()->user->returnUrl );
}
}
protected function performAjaxValidation($model)
{
if(isset($_POST['ajax']) && $_POST['ajax']==='calls-form')
{
$callStatusesScenarios = array( Calls::CALL_FAIL=>'validCallSubmit', Calls::SUCCESS_CALL=>'successCallSubmit', Calls::WRONG_NUMBER=>'invalidNumberSubmit');
if( !empty($_POST['Calls']['call_status']) && !empty( $callStatusesScenarios[ $_POST['Calls']['call_status'] ] ) ){
$model->setScenario ( $callStatusesScenarios[ $_POST['Calls']['call_status'] ] );
}
echo CActiveForm::validate($model);
Yii::app()->end();
}
}
Как вы видите из кода, все, что я делаю — это просто меняю сценарий в зависимости от пришедшего значения поля 'call_status'. Теперь, если оператор выберет в форме статус дозвона «Успешный дозвон», сценарий модели при валидации изменится на 'successCallSubmit', который ожидает от формы теперь еще и статус разговора — в результате, выбрав «Успешный дозвон», оператор уже не сможет засабмитить форму, пока не заполнит поле «Результат разговора». В то же время, если статус дозвона будет «Неправильный номер», форма засабмитится без всяких дополнительных полей. По тому же принципу можно действовать и далее — со статусами разговоров и их дополнительными полями и т.д.
Применение такого подхода будет полезным в таких ситуациях, когда количество и виды необходимых для заполнения полей будут меняться в зависимости от выбора пользователя. Прошу прощения за немного каламбурное изложение, и успехов вам в разработке на замечательном фреймворке Yii.
Автор: LayneBuchyn